会话分类(dsh-session-categories)
给 DSH Web 界面加一层「会话分类」:把历史会话划进分类、在分类下直接开新会话,并在每轮对话结束后用 AI 判断这条会话该归到哪一类。
A DSH Web plugin that groups chat sessions into categories, lets you start a new session inside a category, and asks the model for a category (and a better title) after each turn. Host half in
index.js(no dependencies), client half inclient.js(no build step).
安装见 安装,实现细节见 ARCHITECTURE.md。
实现说明
架构、存储结构、Host/Client 各自的功能逻辑、关键取舍与已知边界,见 ARCHITECTURE.md。
用起来是什么样
- 侧边栏图标列多一个 分类 图标(和「工单」「定时任务」并排),点开是一个居中弹窗(点遮罩或 Esc 关闭)。
- 弹窗右上角只有三个入口:工作区选择、新建分类、归档(档案盒图标)、⚙ 设置。「自动分类提示」开关挪进了设置里。
- 归档是独立视图:归档后的分类不会出现在主列表里;点归档图标切到「已归档」视图(图标变成 ←、顶部有提示条),点 ← 或提示条里的「返回分类」切回来;分类行上的按钮在归档视图里是「恢复」(档案盒 + 向上箭头)。归档分类不参与新建会话和自动分类。
- 弹窗顶部是搜索框:按会话标题 / 会话 id / 工作区名 / 分类名过滤,命中处高亮;搜索时只留有结果的分类、不分页。Esc 清空。
- 侧边栏按分类筛选:不用打开面板。侧边栏「工作区」分区正上方有一个漏斗按钮(插在导航列表和工作区之间),并且常驻:它插在滚动容器外面,会话列表滚到哪儿它都不动,也不会盖住任何一行会话,点开是分类清单(全部 / 各分类 / 未分类,带条数)。点开清单是即时的:分类筛选只需要「归属」,不等扫全部会话的那次
/state;条数先按归属表显示,会话清单在后台回来后再换成准确值(未到之前显示·),不影响点击。选中后仍然用原生会话列表:先把「展开其余 N 个会话」全部点开(原生每次只 +5,且 watermark 是组件内部 state,外部改不了,只能连点;展开期间把列表临时设成visibility:hidden,点完再显示,观感上是一次性全部展开),再只把不匹配的行display:none。展开后的 watermark 在本次会话里保留,后续筛选不会再点。这样字号、行高、左侧分类色条、时间、工作区标题与分组全部是原生的,工作区也照常显示;按钮高亮表示筛选中,点开清单可以「清除筛选」。实现上需要往侧边栏 DOM 里插一行入口(sidebar.workspaces是整块可替换的插槽,没有「标题上方」的位置),并用 500ms 一次getElementById巡检在 React 重渲染后补回去(不用 MutationObserver:流式输出时 mutation 太密,回调会被叫几百次/秒)。 - 待处理建议:输入框工具行里的建议 chip 只在那条会话打开时才出现,容易“挂着建议却找不到”。所以面板右上角有一个
待处理建议 N按钮,默认收起不占地方,点开才在顶部展开清单。一条会话占一行,行里最多两块建议,各自独立采纳:点归入 ● 分类名就是采纳这条分类、点右侧▾换成别的分类(弹完整分类列表)、点改名 新标题就是采纳这个标题;行尾的忽略只作用于整条会话(两条建议一起丢掉)。默认列 4 行(「展开其余 N 条」看全部)。 - 调用记录:点页脚的金额(或设置 → 花费统计 → 查看调用记录)打开「调用记录」弹窗:每次模型调用的时间、会话标题、判断的是分类还是标题、有没有结论、花了多少钱,最多留 300 条,可一键清空(清空不影响花费总账)。
- 搜索框下面是一条 分类跳转条:每个分类一个 chip(色点 + 名称 + 条数),点一下直接滚到那个分类。
- 每个分类是一块带分类色书脊的卡片,标题 14px 加粗、水平居中(色点 + 名称 + 分类色条数),滚动时标题吸顶,所以永远知道自己在看哪一类。
- 排序全按时间:分类按「最近一条会话的活动时间」倒序,分类内的会话也按活动时间倒序;空分类排在最后(按创建时间倒序),最下面是「未分类」。跳转条与分类块的顺序始终一致。
- 每个分类下默认只列 10 条,多的是「展开全部 N 条」;空分类压成一行灰标题,不占地方。
- 原生侧边栏的会话行也会跟着变:行首出现该会话的分类色条(竖条,不是圆点 —— 圆点是原生的「有新回复」,会看混),鼠标悬停的卡片里也会写分类名(卡片底色是 DSH 写死的深色
#2C2C2E,所以这一行用浅色字,跟随主题取色反而会深字压深底看不见);行尾多一个「移动到分类」按钮,点开直接选。 - 输入框工具行(
+、权限那一行)里会多出建议 chip:归入 ● 分类名 ✕、标题建议 新标题 ✕。点分类名即归类,点 ✕ 即忽略。不浮动、不量坐标,所以不会盖住输入框,也不会飘到屏幕别处。
三个能力
- 划入分类:面板里点会话行尾的按钮、或原生会话行尾的按钮,展开一排分类 chip 后点一下即可(没有拖拽:原生列表是别人的组件,拖拽落点无法可靠接管)。一个会话只属于一个分类。
- 在分类下新增会话:分类标题右侧的
+,用面板顶部选中的工作区新建一条空白会话,创建后自动打开并归入该分类。 - AI 分类:
- 自动:每轮对话结束(
turn/end且正常完成)时,若这条会话还没分类、也没被忽略过,后台判断一次;判断不出就不打扰。设置里可以整体关掉。 - 不重复花钱问同一件事:一次判断没有结论(模型说「与所有分类无关」或置信度不够)时,这条会话至少隔 3 轮才会再问,连续问空 5 次就停止自动判断;等你新增/改名/归档/删除分类后,或手动点一次判断,才重新开始。已有待处理提示的会话也不再重复判断。
- 如果结论和之前不一致,新旧候选都留在 chip 和面板的待处理建议里,由你选。
- 自动:每轮对话结束(
会话标题
- 判断标准是「能不能靠它找回来」:标题在列表里是唯一的线索,所以必须同时写清对象(这条会话在弄的那个东西)和具体操作或结论(改了什么、查了什么、修了什么、问了什么、生成了什么)。只有动作没有对象的标题——「查看X会话」「生成SQL」「检查一下」「问题排查」「执行完成」——一律算不合格,会被要求重起。
- 「对象」不限于业务:系统、模块、接口、文件、脚本、配置、数据表、页面、报错、日志、文档、需求、某个概念,都算对象。纯技术活、查资料、问概念的会话一样能判,也不要求每条会话都有业务信息;内容很轻的会话(只问一个概念)把「问的是什么」写清即可,模型被明确禁止为了凑「业务感」编造没出现过的系统名/分校/班级/订单。
- 看的是整条会话,而且是逐轮的:判断标题时给模型的是「每一轮:你的请求 + 这一轮助手的回复」(首轮 + 最近 20 轮),既能看出整条会话在干什么,也能看出每轮到底做成了什么 —— 只看最后一轮会停在「执行完成,检查一下」,只给用户的话又看不出结果。注入的工作区规则、运行时快照、时间戳通知不算你的话,已剔除。
- 自动建议:每
标题检查频率轮判断一次。标题不合格时,输入框工具行里会多一个 chip:标题建议 <新标题> ✕,点标题即改名,点 ✕ 则这条会话永久不再提示。 - 连续「不用改」会自动拉长间隔:同一会话连续被判为「标题够贴切」时,检查间隔按 1× → 2× → 4× → 8× 递增(到 8 倍封顶),一旦给出新建议就恢复原频率。挂着一张待处理的标题建议时也不会重复判断。这样不会为同一个「不用改」的结论反复付钱。
- 手动:原生会话「…」菜单 →
重新生成标题,弹窗里会先给一个建议、可以直接改,确认后走 DSH 官方sessionController.rename。 - 改名后这条会话的标题来源变成
user,DSH 和本插件都不会再覆盖它。 - 已经手动改过名的会话(source =
user)不再参与自动检查。
提示的生命周期
- 一旦生成,提示胶囊会一直留在输入框左侧,刷新页面也在。
- 点「加入」→ 归类、提示消失。
- 点「忽略」→ 该会话永久退出自动分类(手动把它移进/移出分类会重置这个标记)。
- 分类被删除时,指向它的提示和归属一起清掉,会话回到「未分类」。
设置面板
面板右上角 ⚙ 打开,所有项都落 state.json。设置按用途分成 基础 / 判断依据 / 会话标题 / 花费统计 四组:
| 项 | 说明 |
|---|---|
| AI 自动分类 | 每轮结束后是否判断 |
| 会话行显示分类标记 | 关掉后侧边栏不显示分类色条(悬停卡片仍然显示) |
| 筛选清单排序 | 侧边栏「按分类筛选」清单里分类的顺序:按创建(默认,位置稳定)/ 按活跃(分类下最新会话倒序,空分类沉底,跟分类页一致,但会随新消息变动)。不影响分类页本身 |
| 花费统计档位 | 当日 / 本周(周一起算)/ 本月,页脚也能直接切 |
| 对话轮次 | 每次判断时带上最近 N 轮对话(滑块 1~20),点「全部」= 整条会话。1 就是只判断当前这一轮 |
| 标题检查频率 | 滑块 0~20:每 N 轮看一眼标题还贴不贴切,不贴切就在输入框旁弹建议;0 = 关闭。已分类的会话同样会检查。设多少就按多少执行(1 = 每轮都看),不会因为「连续不用改」自动拉长间隔;想省钱就把这个数调大 |
| 最低置信度 | 低于它的建议不提示 |
内置提示词
提示词现在不可配置,由插件内置:
- 分类提示词:只看你给的对话内容判断归属;一次请求同时顺带判断标题是否该更新。
- 标题提示词:单独判断标题时使用(已分类会话的定期检查、手动「重新生成标题」)。要求标题同时具备「对象」与「具体操作」,并给了「差标题 → 好标题」的示例(含纯技术/纯问答的样例);两样都写清才返回空数组,内容太轻看不出来时也允许返回空数组,不许编造。
- 素材模板:
- 会话 id=… | 标题:… | 对话:{conversation}{priorLine}。分类时conversation由「对话轮次」决定,只含用户消息和 AI 最后一条回复;判断标题时换成逐轮摘要(首轮 + 最近 20 轮,每轮包含该轮助手回复)。两种情况都不含思考与工具调用。
本地自检
改完客户端代码先跑一遍冒烟脚本。它做两件事:
- 用 DOM stub 真正执行
apply()(抓注入阶段的问题); - 把注册到槽位里的 11 个组件各渲染一次,喂一份贴近真实的 store 快照(分类/会话/归属/建议/用量都有内容,弹窗与筛选清单也摆上数据),所以「组件渲染体里引用了不存在的变量」也能抓到。
node scripts/smoke-client.mjs # Client:apply 无警告 + 11 个组件各渲染一次(含 lite data 分支)
node scripts/check-host.mjs # Host:16 项行为自检,用临时 DSH_HOME 真跑 apply
npm run check # 两个一起跑
check-host.mjs 覆盖的都是踩过的坑:状态文件损坏后的降级与恢复、删空分类不被 seed 复活、跨站写请求被拒(403/415)、
/hint 采纳用户真正选中的分类、素材字数预算与注入消息过滤、兜底标题跳过 <system-reminder>。
之前就踩过两次:一次大段替换把三个辅助函数连带删了,node --check 只看语法,插件一加载就整块消失;另一次是只跑 apply 时,CategoriesPanel 里写错一个标识符脚本照样报「✅ 成功」。
安装
# Release 资产(推荐:版本固定、无需构建)
dsh plugin --profile <profile> add https://github.com/CleverYyt/dsh-session-categories/releases/download/v0.2.0/dsh-session-categories-0.2.0.tgz
# 跟随 main 分支最新代码
dsh plugin --profile <profile> add github:CleverYyt/dsh-session-categories
# 本机改代码:直接挂本地目录,改完刷新页面就生效
dsh plugin --profile <profile> add link:$PWD
装完重载插件(或刷新页面),侧边栏图标列会多出一个「分类」图标。
dsh plugin只是把参数转发给 profile 目录下的 pnpm,所以 pnpm 支持的写法(link:、github:、git URL、npm 包名、https://…tgz)都能直接用。
⚠️ npm 上的
dsh-session-categories不是本仓库。那个裸名已被别人占用(ht719/dsh-session-categories,2026-08 发布 0.1.0),所以本插件没有、也不该占用这个裸名;若将来发 npm,会发在 scope 下(见「仓库与分发」)。
从 DSH 插件市场(Discover)安装
DSH 目前没有统一的官方市场:插件市场是用户自己添加的索引源(一个 format 1 的 JSON),Web 端在「插件 → 发现」里读它。本仓库已经带了一份索引:
https://raw.githubusercontent.com/CleverYyt/dsh-session-categories/main/market/index.json
用它的步骤:
- 侧栏 插件 → 找到「实验性插件」旁的 更多 → 启用 自定义插件市场;插件页随后出现 已安装 / 发现 两个标签页。
- 在 发现 里添加索引源,粘贴上面那行 URL。
- 索引里会出现「会话分类」,点安装即可(安装参数由索引里的 git commit 固定,不经 npm)。
维护约定:
- 索引格式(format 1)见 DSH 的
@deepseek-ai/dsh-plugin-market-index;market/index.json必须能被它的parseMarketIndex接受,否则「发现」会直接报索引无效。 versions[].source用git源,commit必须是包含该版本代码的 40 位小写 SHA。发新版本时同步改version、publishTimestamp(Unix 秒)、commit。- 图标用内联的
data:image/svg+xml;base64,…(上限 32 KiB 解码后),不依赖外链托管。
仓库与分发
- 源码:https://github.com/CleverYyt/dsh-session-categories
- 没有构建步骤:
index.js/client.js就是最终产物,改完直接提交。 - 未发 npm:
files已把运行时文件(index.js/client.js/cordis.patch.yml/locale/icon.svg)、两份文档与自检脚本列全,private: false、engines也写好了。 - 发 npm 必须带 scope(如
@cleveryt/dsh-session-categories):裸名dsh-session-categories已被他人占用。scoped 包默认私有,发布时要npm publish --access public;package.json里再加"publishConfig": { "access": "public" },并把dsh.bundle/dsh.client保持不变(DSH 只看这两个字段,不看包名)。
数据存在哪
$DSH_HOME/storages/dsh-session-categories/state.json
不写任何会话日志,所以升级 DSH、换模型、重装插件都不会动这个文件;删掉它等于重置分类。
配置项
都在 cordis.patch.yml 的 config 下,改完重载插件生效:
| 键 | 默认 | 说明 |
|---|---|---|
seedCategories |
["工作","排查","学习"] |
只在第一次运行(state.json 不存在)时写入。已有数据不会被覆盖。 |
provider / model |
空 | AI 分类用的模型。留空 = 跟随运行时默认模型。 |
minConfidence |
0.35 |
低于该置信度的建议直接丢弃。只是首次运行的默认值。 |
costRange |
day |
花费统计默认档位(day / week / month)。只是首次运行的默认值。 |
面板设置改过之后以
state.json为准,改cordis.patch.yml不会覆盖用户选择。
接口
Host 半在 /dsh-session-categories/api/* 下提供 JSON 路由,Client 半只 fetch:
| 路由 | 方法 | 用途 |
|---|---|---|
/state |
GET | 分类 + 归属 + 提示 + 会话清单 + 模型可用性 + 设置 + 花费档位 + 模板默认值 |
/hints |
GET | 轻量轮询:待处理提示与分类名 |
/calls |
GET / POST | 「钱花在哪」的调用明细;POST {action:"clear"} 清空 |
/category |
POST | create / rename / archive / delete / reorder(reorder 暂未接 UI) |
/assign |
POST | 批量改归属(categoryId: null = 移出) |
/new-session |
POST | 在分类下新建会话 |
/hint |
POST | accept / dismiss |
/title |
POST | suggest(现算一次)/ accept(改名)/ dismiss |
/settings |
POST | 自动分类开关、行内标记、花费档位、对话轮次、标题检查频率、置信度、筛选清单排序 |
/classify-session |
POST | 维护接口:对单条会话强制判断一次(清掉节奏记录),无 UI |
/rejudge |
POST | 维护接口:清空待处理建议后按当前素材重算(limit 1~50,默认 20),无 UI |
所有写接口都要求
Host是回环地址(挡 DNS rebinding)、同源Origin、Content-Type: application/json;读接口也校验 Host。
已知边界
- 分类不改变原生侧边栏的分组结构:原生列表仍然按工作区分组,分类是一层平行视角。想在侧边栏里长出分类分组,需要整体替换
sidebar.workspaces(要重建搜索、置顶、归档、菜单),本插件没做。 - 会话清单来自
sessionQuery,只列当前 profile 可见的会话,排除子代理会话。 - 自动判断每轮最多触发一次;同一会话正在判断时又来一轮会被合并掉(不会再起一次调用),真要因新内容改判,下一轮结束会再触发。连续多轮且一直分不进去时,仍可能每轮花一次(有 3 轮冷却 + 5 次上限兜底)。
- 会话清单(
/state)冷扫是秒级的(91 条约 1.7 秒,随会话数线性增长),但正常使用时看不到它:- 内存缓存 5 秒 + 过期先给旧值、后台刷新;
- 清单落盘做种子(
sessions-cache.json),进程起来后第一次打开面板也是毫秒级; - 客户端启动空闲时预取一次,鼠标悬停到「分类」图标也会预取。
侧边栏筛选清单从不依赖它(分类筛选只依赖归属)。想知道冷扫耗时构成:
GET /api/state?timing=1。
No comments yet. Be the first to write one.