dsh-auto-title
Verifieddsh-auto-title · v1.7.0 · MIT · Web UI
DSH plugin: model-generated session titles that follow the latest task, remember manual renames, persist their settings, and never fail silently.
Install
dsh plugin add dsh-auto-title Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-auto-title
English | 中文
给 DeepSeek Harness 用的、真正能跑起来的模型自动会话标题插件:跟随最新任务、尊重手动改名、并且会把自己的失败报出来。
关于语言:本文件是中文说明,英文版见 README.md。插件自身的文案跟随软件语言:设置页分区、页面消息、以及「设置 → 插件」里显示的名字,都来自向宿主 locale 服务注册的
{ zh, en }两套字典。下文引用界面文案时按它实际出现的语言引用,旁边给出另一种语言。
DSH 自带的自动标题只看首条人工消息(截前约五个词)。dsh-auto-title 换成从最近几条人工消息生成标题,会话在聊什么,标题就是什么;你手动改过的标题永不被覆盖;设置存在 profile 里;而且——不像一个「静默失败」的 provider——生成不出来时,它会把原始错误文本摆到设置页上给你看。
为什么需要它
DSH 自带 @deepseek-ai/dsh-session-title-first-prompt-llm(loader 条目 session-title-llm),它有三个容易踩空的地方:
- 标题 provider 是单槽位。再注册一个会直接被拒:
session-title provider "<id>" is already registered。 - provider 需要一条模型路由。如果它只从会话里读路由,而会话还没记过 request header,路由就是空的。
- provider 抛错是静默的。 宿主只写一行
session "<id>": automatic title generation failed: <error>日志,兜底标题照旧显示——界面看上去有标题,其实那只是你第一句话的前五个词。
本插件照着宿主源码写成,专门堵这三个洞:主动接管槽位;模型路由按固定回落链解析,全空时报错会逐条列出试过哪些环节;最近一次失败的原文留在你能看到的地方。
它会做什么
跟随最新任务。 每条符合条件的人工消息都会用最近的消息重算标题。
绝不覆盖你的标题。 会话标题一旦带上
source.kind === "user",本插件立刻停手。子会话只命名,不生成标题。 带
parentSession、origin === "subagent"、或delegationDepth > 0的会话一律拿到子会话 | <父会话标题>(英文语言设置下是sub-session | <父标题>),完全不调用模型:它那条"人工"消息其实是某个父代理写的提示词,硬生成只会为每次子代理运行花一次调用换来一句没信息量的标题;而直接跳过,列表里留下的就是宿主把那段提示词截成 40 来字节的兜底标题。父会话读不到、或父会话当前的标题本身就是宿主兜底的首句(那是被截到 40 来字节的散文,不是标题)时,只留标记本身;标签和宿主当前显示的一样就不再重复写事件。这类计入"跳过",不算失败。解析模型而不是瞎猜。 顺序:插件设置 → 宿主给的路由 → 会话
request/header→modelSelection/model/requestContext投影 →agentDefaultModel.currentSelection()。全空时,报错里会逐个点名试过的环节。标题外形固定可读:
对象 | 类型 | 摘要。第一格是这条会话属于哪个域,同一个域永远用同一个词,同一批会话在列表里能对齐。六个域每个安装都有,就是这个宿主和它自己的子系统:
模型输出 English 中文 覆盖范围 dsh-coreDSH core DSH 本体 宿主本身:版本、打包、依赖钉版、文档 dsh-pluginDSH plugin DSH 插件 插件的开发、打包、发布与排障 dsh-gatewayDSH gateway DSH 网关 渠道接入:Web GUI、Telegram/Messenger 通道 dsh-uiDSH UI DSH 界面 界面、侧栏、主题、外观 dsh-memoryDSH memory DSH 记忆 记忆与知识库:mnemon、文档、归档 dsh-skillDSH skill DSH 技能 技能、工具、提示词 这六个就是随插件发布的全部词表。除此之外,插件只认一份环境探测表:它记着每种工具的名字、说明和探测线索,但探测表里的域一个都不存在——只有本机真的出现证据,插件才会把那个域建成一条记录(名字、说明,以及插件为它准备的几个写法一起进来),从此它和别的域一样能改名、能加写法、能删掉。线索来自你装的条目、你连的 MCP 连接器、你最近工作过的目录名,插件启动时读一遍(只在本机读):
模型输出 中文名 本机出现什么才创建 telegramTelegram telegram/messenger连接器或条目ankiAnki anki连接器roamRoam roam连接器zoteroZotero zotero连接器ledger记账 ledger/记账条目network网络 proxy/clash/vpn/mihomo/sing-boxgithubGitHub github/git-panel/gitlab条目obsidianObsidian obsidian条目notionNotion notion连接器或条目dockerDocker docker/compose/container除这两张表之外,域还有两条来路:环境探测(上面那张表,本机有证据才建域)和你在设置页上手动新建。真正的词表按你本机的用法生长:模型在对话里写了一个词表里没有的主题词(比如「小红书」),插件先把它记成候选;等它在两个不同会话里独立出现之后才建域,从下一次起它进提示词、标题里也用印出来的那个词。模型把已有域写成别的说法(比如把
DSH 插件写成「自动标题插件」)时,这个说法作为别名归到那个域上,不必新建域;别名同样参与摘要去重。两个上限都由你自己定、都不封顶:默认 24 个域、每域 8 个别名,设置页里可以填任意数字,填 0 就是「不列出任何域」或「不再记新写法」(约 3–6 个汉字或单词的候选词才收,纯数字、带标点的短语不收)。同一个写法被两个域同时认领时,设置页会把它列成冲突——因为包含匹配会因此对这两个域都失效;被上限挡在提示词外的域也会在那里列出来,而一个词够格建域却被上限挡住时,候选行会说明原因。学习模式可以在设置页上关掉;整份词表就是本机dsh_auto_title.json里的一段设置——没有遥测,没有任何东西离开这台机器。这份词表也能手动改:每个域都能在设置页上改名字与说明、加删本机写法,也能新建一个域(插件自带的名字改不了,只能给它加写法)。改名不会换掉域在文件里的 key,所以别名、冲突与匹配规则都照旧;改出来的名字不能是别的域已经认的词,也不能跟别的域的词重叠,否则当场拒绝并说明原因。没有探测到的工具就等于不存在:模型写Obsidian时,本机没有 obsidian 域,那个词就是没人认领的词——可以被「笔记软件」收作写法,也可以由你自己新建一个 Obsidian 域。词表里没有合适的域时,模型被要求整段省略、只答
类型 | 摘要;模型自己编的对象词会被丢弃而不是印出来——插件永远不会凭空造一个域。摘要若以对象词开头重复一遍,会把重复的部分剪掉:Telegram | 修复 | Telegram 网关修复变成Telegram | 修复 | 网关修复。日期不再是标题的一部分——列表本来就显示时间,而且续聊会让它跳到新的一天。第二格是这条会话在做的动作。它也出自一份封闭的词表,但这份词表是一套由你选的类型方案。内置两套,设置页上的类型方案卡片用来在它们之间切换,也可以自己写一套。
默认是 22 词方案——一行一个动作,配一句把它和邻近动作分开的判据;下面的顺序就是模型被要求优先采用的顺序:
模型输出 English 中文 在做的动作是 fixfix 修复 原本正常,现在坏了,把它弄回正常 testtest 检验 验证真伪、可用、达标:抽检、试做、模拟考 reviewreview 评审 对已有成品或一段过程复核并给出结论 cleanclean 清理 移除不需要或多余的:大扫除、清缓存、清仓、退订 makemake 制作 把原料、零件、素材、代码变成成品 writewrite 撰写 成品是以文字为主的:报告、文档、文章、论文 deliverdeliver 交付 东西已做好,动作是交给接收方:发货、上线、交作业 teachteach 教学 让具体的人或模型学会:带徒弟、讲题、培训 learnlearn 学习 掌握自己原本不会的知识或技能:练习、刷题、备考 researchresearch 调研 到外部找信息、样本、候选选项 analyzeanalyze 分析 判断手里的数据说明了什么:统计、核算、归因 communicatecommunicate 沟通 把信息传达给具体对象:通知班组、联系物业、问老师 discussdiscuss 讨论 开放交流,还没有定论 decidedecide 决策 在现成的选项里选定一个 planplan 规划 排出怎么做、何时做、要多少资源 optimizeoptimize 优化 能用,但想更快、更省、更稳、更短 maintainmaintain 维护 让一直正常的东西继续正常:保养、巡检、备份 organizeorganize 整理 重排让东西更好找:清点、梳理、收纳 storestore 存档 收起来留着以后用:入库、备份、存钱 convertconvert 转换 内容不变,只换形式、语言或单位:翻译、换算 monitormonitor 观察 持续盯着看变化,目的是及时知道 adminadmin 事务 跑流程办手续:填表、预约、缴费、报销、报工 另一套内置方案是原来的 10 词方案,留着是因为早先的版本写的就是这些词:
feature功能、fix修复、optimize优化、refactor重构、test测试、docs文档、release发布、config配置、explore探索、discuss讨论。它等于 22 词里的软件工程那一半、但保留了只对工程精确的那几个词(refactor说的就是optimize的精确版,docs之于write、release之于deliver、config之于机器上的admin),所以只写程序的环境仍然好用。选择只存成
dsh_auto_title.json里的一个 id(typeScheme),不是词表的副本:切换方案只写一个字段,普通保存任何别的设置都不会碰它。id 为空、不认识、或指向一套已删掉的方案时,一律回落到 22 词默认。切换从下一次生成标题起生效——提示词按当时生效的那套方案现拼,不用重启;已经写好的标题是会话日志里的文本,永远不回写。解析器对两套内置方案的词都认,不管当前生效的是哪一套,所以换方案不会把旧标题弄坏。自建方案 2–48 个词。每个词必须是单个 token(不能有空格,多词永远匹配不上),带一个 key 和一个中文标签,还可以带一句判据——设置页会显示它,也会把它写进给模型的提示词。词与词不能重复;一个词也不能是某个域的域词或它的任一写法——两格是从两端匹配的,被两边同时认领的词会让两边都失效。方案名 1–48 个字符,不能与别的方案名或内置名重复,最多存 12 套。编辑一套方案时,正在用的那套保持在用;删掉正在用的那套则回落到默认。设置页会带着原因拒绝:
invalid-name、reserved-name、duplicate-name、word-count、invalid-word、duplicate-word、domain-conflict。方案里没有任何一个词合适时,模型被要求整段省略、只答
对象 | 摘要——没有other这个兜底词。模型没写类型词时,摘要原样使用——自定义提示词不会因为词表而被卡住;一个两套方案里都没有的词会被丢掉,而不是被拼进摘要。设置落盘在 profile 自己的存储域里(
dsh_auto_title.json),带 revision 比较并交换,陈旧页面不会静默覆盖新设置。失败可见。 最近一次失败的原文、时间戳、会话 id 出现在设置页和插件自己的 RPC 上,并且可以一键清除。
可手动重算。 任意单个会话都能强制重算,结果或错误原文直接返回。
运行要求
- DSH
0.2.0-rc.2(desktop/web/headless/tuiprofile)。 - Node.js ≥ 22(用 DSH 自带的那份即可)。
本插件 import 的 @deepseek-ai/*(dsh-llm、dsh-session-title、dsh-storage-domain、dsh-timeout、dsh-util-values)全部由宿主在运行时注入:它们不在 npm 上、版本由宿主掌握,所以不写进 dependencies;但它们写进了 peerDependencies,因为宿主正是拿这份名单来决定「软链进来的插件可以借用哪些宿主自带副本」。版本范围刻意用 *:宿主会用 semver 校验 @deepseek-ai/dsh-* 这类 peer,范围不满足就直接跳过整个 bundle,第三方插件写死运行时版本会在下次 DSH 升级时被禁用。
安装
把包加进 profile,并用 DSH 自带的 pnpm 安装:
$profile = "$env:USERPROFILE\.dsh\profiles\desktop"
Set-Location $profile
# 先在 package.json 的 dependencies 里加 "dsh-auto-title",
# 再把它追加到 dsh.profile.bundles,然后:
& "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\primary-runtime\dependencies\node\bin\node.exe" `
"$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\pnpm\bin\pnpm.mjs" install --trust-lockfile
从源码目录装(link:)。 profile 可以直接指向工作树而不是 tarball。这时有两件事必须成立,且都落在链接真实指向的那个目录上:插件自己的依赖(zod)必须能从那里解析到——在该目录跑 pnpm install,或给它一个 node_modules;以及上面那批宿主包必须在 peerDependencies 里,因为宿主只把「插件自己的 package.json 点过名」的那些包交给软链插件使用。
然后重启 DSH。本插件自带的 patch 层会禁掉宿主自带的 provider;因为这个 patch 属于本 bundle,卸载或停用本 bundle 时它随之消失,宿主自带的 provider 会自动恢复——不改宿主任何文件,也不会留下「槽位没人管」的状态。
重启前可以先核对组合树:
dsh --profile <name> --dump-config
应当看到本 bundle 插入了自己的一行:
# == dsh-auto-title
- id: auto-title
name: dsh-auto-title
以及宿主自带的 provider 被关掉:
# == @deepseek-ai/dsh-base, patched by dsh-auto-title
- id: session-title-llm
name: '@deepseek-ai/dsh-session-title-first-prompt-llm'
disabled: true
设置页
打开 设置 → 自动标题(或 设置 → 插件 里本插件那一页)。
| 设置 | 含义 |
|---|---|
| 生成时机 | 「每条提示词」=每条符合条件的人工消息都重算;「仅首条提示词」=本插件给该会话生成过一次后就停。 |
| 标题语言 | 「跟随消息」自动判断,也可强制 中文 / English。 |
| Provider / Model | 两个都留空=跟随当前会话模型;两个都填=显式钉死;只填一个会被拒绝。 |
| 标题提示词 | 替换内置指令。输出格式约定始终附加,所以结果一定可解析。留空即恢复内置。 |
| 学习模式 | 词表怎么生长:「自动」=本机证据与对话里长出来的词直接启用;「需确认」=只进候选列表,等你在下面点;「关闭」=完全不学(已学到的保留)。 |
同一页还会列出最近会话及其标题来源(手动 / 自动生成 / 宿主兜底)、每行一个重新生成按钮,以及上次失败的原始错误文本与时间戳;最下面的「域词表」区就是词表的操作台:
- 已启用的域,每行带来源徽章——内置 / 环境探测 / 会话生长 / 手动——以及它接受的写法;可删的行(当前域之外的)右边有删除按钮;每行还有一个「编辑」按钮,打开这一行的编辑器:域名(标题第一格印的就是它)、说明(每次生成都会送给模型的那一行),以及本机为它记下的写法(加一个、删一个都行,打字后直接保存也会算进去)。保存会把这一行整套写回;被拒时编辑器不关,你输入的字留在框里并附上原因。名字由插件决定的域(内置六域)在编辑器里名字是只读的,只能改写法;重名、撞到别的域已有的写法、以及会跟别的域的词重叠的词都会被拒绝。这一节的标题旁边还有「新建域」:填名字、说明(留空自动写一行)和写法,就得到一个来源为「手动」的域,和探测到的域一样能改能删;域数到上限时这个按钮会禁用并说明原因;
- 待处理的候选,每个词给「并入「某域」」/「新建域」/「暂不显示」(只在本地隐藏,不动数据);
- 环境探测面板:读过的连接器、条目与工作目录数量、已启用项、失败原因,以及「启用探测到的域」;
- 底部是上限与重置词表(会问一次再执行)。
再下面「类型方案」区是第二格的操作台:
- 一个选择器列出两套内置方案和你写的每一套(带词数),并显示正在用的那套的名字与来源(
内置/自己写的); - 正在用的那套的词与判据,以及一行「标题里会显示成」的示例;
- 自建方案的新建(复制当前)、编辑、删除——方案按
词 | 中文 | 判据一行一个编辑;保存被拒时编辑器不关,你的输入留在框里并附上原因(… is already taken by …); - 一句说明:切换只影响下一次生成,已写好的标题不会变。
RPC
设置页通过 GET/POST /api/dsh-auto-title 与宿主半通信。响应统一是 {ok:true,value} 或 {ok:false,error:{code,message}}。
| action | 请求体 | 说明 |
|---|---|---|
| (无,GET) | — | 快照:设置、provider 状态、存储状态、最近会话、上次失败/成功。 |
save |
{expectedRevision, settings} |
对 revision 做比较并交换;陈旧 → revision-conflict,非法 → invalid-settings。 |
reset-settings |
{expectedRevision} |
恢复默认值,同样走比较并交换。 |
clear-status |
— | 清空上次失败与成功。 |
regenerate |
{sessionId} |
强制重算并返回新标题或原始错误;manual-title / skipped 是故意拒绝。子会话也可以重算——那会用父会话当前的标题重新命名它。 |
vocabulary-seed |
— | 启用这次环境探测到的域;返回新快照。 |
vocabulary-promote |
{key, mode, targetKey} |
候选词的出口:mode:"alias" 并入 targetKey(必须是已知域),mode:"domain" 建成新域。超限 → alias-limit / domain-limit,未知候选 → unknown-candidate。 |
vocabulary-forget |
{key} |
关掉一个域(连同它的别名)。内置的六个不在词表记录里,删不掉 → unknown-domain。 |
vocabulary-edit |
{expectedRevision, key, label, scope, spellings} |
改一个域:名字、说明,以及本机为它记下的全部写法(spellings 是整套替换,空数组=忘掉全部)。内置域的名字与说明改不了 → fixed-domain;其余拒绝码:unknown-domain、invalid-label、invalid-scope、invalid-alias、alias-limit、label-conflict(名字被别的域占用,或与别的域的词重叠)、alias-conflict。 |
vocabulary-add |
{expectedRevision, label, scope, spellings} |
新建一个域:key 由宿主铸造(页面猜不到哪些已占用),记录写成 source: "user",scope 留空则取该名字的默认说明。拒绝码同上,另有 domain-limit(域数已达上限)。 |
vocabulary-clear-candidates |
— | 清空候选列表。 |
vocabulary-reset |
— | 重置整份词表:去掉一切生长出来的东西,只留内置六域。 |
type-scheme-select |
{expectedRevision, scheme} |
按 id 切换类型方案;只接受内置或已存的 id,否则 unknown-scheme。只写 typeScheme 这一个字段。 |
type-scheme-save |
{expectedRevision, scheme} |
新建({name, words})或编辑({id, name, words})一套方案并返回新快照。新建的那套立刻成为正在用的;编辑则保持原来在用的那套。拒绝码:invalid-name、reserved-name、duplicate-name、word-count、invalid-word、duplicate-word、domain-conflict、scheme-limit、unknown-scheme。 |
type-scheme-delete |
{expectedRevision, id} |
删掉一套自建方案。内置的 → reserved-scheme;删掉正在用的那套则回落到默认。 |
失败为什么不会静默
在 DSH 里,provider 抛错是看不见的——这是设计如此。所以本插件把「上次生成失败」当成产品状态而不是日志行:
- 每一个非跳过错误都以
{at, message, sessionId}记下并落盘,重启后仍能读到原文; - 设置页原样渲染这段原文,不做转述;
- 有意跳过不含在内——子会话、手动改名不是失败,永远不会污染失败显示;
- 抢不到槽位时,原因与回滚结果同样记录在案,而不是写条日志就算了。
语言与显示名称
- 设置页分区、页面文案、以及全部 RPC 消息都跟随软件语言:客户端半
inject = ['slots', 'locale'],用自己的命名空间向官方 locale 服务注册{ zh, en }两套字典(ctx.locale.register+ctx.locale.bind),切换语言时按需重画。 - 「设置 → 插件」里显示的名字同样跟随软件语言。
locale/en.json(英文根,Auto Session Title)与locale/zh.json(自动会话标题)声明meta.title/meta.description,exports用./locale/*原样映射,目录里每个语言文件都能按本包解析。 - 宿主读取器有三条硬规则,加语言前值得知道:
locale/en.json是锚点——没有它,同目录其它文件一个都不会被读;文件名去掉.json之后不是纯语言 id、或解析出的路径跑出locale/,都会抛错并连带丢掉整个标题;文件在磁盘上但没写进exports也是同样下场——所以这里用通配映射,而不是每个语言写一条。
开发
node --test # 51 个单元测试(覆盖 lib/title-format.js)+ 1 个宿主冒烟测试
node <bundled-node> --check lib/index.js
lib/title-format.js 是纯函数、无副作用,所以提示词、解析器、拼装逻辑都能脱离宿主直接测。
宿主半由 test/host-smoke.test.js 覆盖:把 lib/index.js 真的启起来(它从宿主导入的五个包用 test-support/stubs 里的桩替身,由 test-support/hooks.mjs 解析),再用假请求驱动真实的 RPC handler——于是端点注册、设置文件、revision 守卫、三个类型方案动作、页面会看到的那些拒绝码、重启后重新加载,以及模型真正拿到的契约与最终拼出的标题,全部是对宿主会跑的那份代码做的断言(14 项检查,1 个测试)。它需要 zod(本插件唯一的真实依赖):没有就会自我跳过而不是失败,也可以用 DSH_AUTO_TITLE_ZOD=<zod 入口路径> 指给它。
许可
MIT © WhatCannotBeSaid