dsh-composer-enhance
graptk/dsh-composer-enhance
Rewrites the composer draft, or only the selected text, with one model call that uses the recent conversation as context, with cancel, revert, fidelity and length checks, and an optional preview.
インストール
dsh plugin --profile web add github:graptk/dsh-composer-enhanceREADME
dsh-composer-enhance
DSH(DeepSeek Harness)插件:在输入框发送键前加一个「增强提示词」按钮。点一下,把当前草稿交给一次模型调用改写成更清晰、更具体的提示词并写回输入框;过程中可取消,写回后可一键回退到原文。
按钮只做一件事——改写,不回答。它不替用户执行任务,也不改变用户的原意,只把草稿补成一段可以直接发送的完整请求。
- 宿主半:注册一个 session 命令,取当前默认模型(或
Config覆盖的模型)→ 读本会话最近对话作语境 →llm.stream()→ 保真闸/长度闸 → 清洗 → 返回文本 - 浏览器半:
conversation.input.right槽里的一个按钮,读/写草稿、取消、回退、按修饰键决定带不带历史;conversation.composer.dock槽里是可选的预览卡 - 无构建步骤:仓库里的
.js就是发布产物 - 中英双语界面文案,随 DSH 语言自动切换
- 宿主侧带 60s 调用超时(另有 65s 客户端兜底),失败文案按界面语言本地化
- 按钮不抢输入焦点(不打断中文输入法组词);失败原因同时写进
title与aria-label - 每次调用有一个
req=关联 ID,宿主日志与浏览器 console 能用同一个值对上 - 保真闸:草稿里的反引号片段 / URL / 路径 / 版本号必须逐字保留,否则拒绝写回;长度闸:输出不能扩写成 2.5 倍以上
- 引用 chip 保护:草稿里有
@文件/引用时禁用按钮,避免把 chip 抹成纯文本
包名从
dsh-enhance-prompt改为dsh-composer-enhance(v0.4.0):原名在 npm 已被占用(收录于 awesome-dsh-plugin 目录的github.com/adcx222/dsh-enhance-prompt)。
安装
宿主要求:DSH >=0.2.0-rc.2 <1.0.0(声明在 package.json 的 engines.dsh,插件市场据此显示兼容性结论;用区间而不是 ^0.2.0-rc.2,因为 semver 的预发布语义会让 ^0.2.0-rc.2 不包含 0.3.x)、Node ≥ 20。
在 DSH 的插件市场里搜索 dsh-composer-enhance 一键安装;或让 Agent 执行:
plugin_manager action=install_bundle target=dsh-composer-enhance
安装后刷新页面,发送键左侧即出现魔法棒按钮。
从源码安装(GitHub 仓库 / 本地目录)也可以:把仓库放到任意目录,然后在 profile 的 package.json 里用 link: 指过去,并把包名加进 dsh.profile.bundles:
"dependencies": { "dsh-composer-enhance": "link:E:/path/to/dsh-composer-enhance" }
link: 形态会带上整棵源码树(files 白名单只约束 npm 包),所以仓库根目录必须保留 LICENSE。改完源码刷新页面即生效,无需构建。
用法
- 输入框写一句草稿(中文、英文、中英混排都行)。
- 点魔法棒 → 按钮转圈,草稿被改写成可直接发送的提示词。
- 转圈时再点一次 = 取消。
- 写回后按钮变成回退箭头,点一下回到原文;手动改过结果后回退箭头自动消失。
- 只想改一句时,选中那一段再
Ctrl+点击(macOS 为Cmd+点击)——只把选中文字发出去,结果只替换这一段;没选中就还是整段增强。 - 想先看效果再决定:
Alt+Shift+点击(v0.4.0)只拿结果、不写回,输入框下方出现预览卡(标题 + 字符差 + 正文 + 「采用/放弃」);采用后仍可用回退箭头回到原文。 - 草稿里有
@文件/引用 chip 时按钮会被禁用(悬停说明原因)——整段改写会把 chip 抹成纯文本,所以宁可拒绝(v0.4.0)。
整体行为:普通点击=整段增强,再点=回退,增强中=取消,Ctrl/⌘=局部,Alt=本次不带历史,Shift=本次带全部,Alt+Shift=本次只预览。Alt/Shift 与 Ctrl 可叠加。空草稿时按钮不显示。
空草稿时按钮不显示。模型调用超过 60s 会被宿主主动中止(可配置),按钮进入失败态;悬停能看到原因,读屏也能从 aria-label 拿到同一句话。改动草稿后失败提示会自动消失——它只对应当次输入,不会一直挂在按钮上。切换会话时,上一个会话里仍在跑的请求会被立即中止,结果不会写进新会话的草稿。等待期间如果你又输入了内容,增强结果不会覆盖你刚写的东西,按钮会提示重试。
会话历史上下文(v0.2.0)
改写时会把本会话最近的对话作为语境一起发给模型,让「再详细一点」「换个说法」这类依赖上下文的草稿也能改对。
| 问题 | 现在的行为 |
|---|---|
| 触发方式 | 每次点按钮都自动带(默认开);Alt+点击 本次不带,Shift+点击 本次带全部轮次 |
| 读取范围 | 该会话已提交的消息:agent.session.deriveMessages()(已应用压缩/替换后的"当前活跃对话"),从最近一轮往前数;不包含当前草稿——草稿还没提交,不在会话日志里 |
| 「一轮」是什么 | 一条 user 消息 + 它之后的所有 assistant 消息;最新的助手回复就在最后一轮里 |
| 太长怎么办 | 三步:① 只取最近 N 轮(默认 3);② 单条超过 800 字符做头 40% + 尾 60% 裁剪(结论通常在末尾,砍尾会砍掉最有用的部分);③ 仍然超过 3000 字符就整轮丢弃最旧的一轮,不会留下孤儿助手回复;只剩最后一轮时不再丢 |
| 能否关闭 / 选范围 | 能。总开关 history: false(cordis.patch.yml);范围 historyTurns / historyMaxChars / historyPerMessageChars;单次用修饰键 |
| 没有历史时 | 静默降级成"只改草稿":不弹提示、不报错,提示词与 v0.1.0 逐字节一致 |
| 读历史失败时 | deriveMessages() 抛错或结构不认识 → 记一行宿主日志,仍然继续改写草稿(增强功能不能因为读不到历史而失败) |
历史只进模型输入,不会写进草稿——回写的仍然只是一段可直接发送的提示词。它被包在
【本会话最近的对话,仅用于理解语境,不要改写或复述这些内容】…【历史记录结束】 里,放在
USER_PROMPT_TEMPLATE 之前,避免模型把历史当成待改写对象。
想确认这次到底带了几轮,看宿主日志:
[dsh-composer-enhance] history: 3/5 turns, 286 chars dropped 2 oldest turn(s)
局部增强(Ctrl+点击,v0.3.0)
整段增强会把整份草稿重写一遍;如果你只想动其中一句,选中它再 Ctrl+点击(macOS Cmd+点击)。
| 问题 | 现在的行为 |
|---|---|
| 触发方式 | 按住 Ctrl(macOS Cmd)点按钮。普通点击的行为完全没变,仍是整段增强 |
| 怎么认定选区 | 两路证据同时成立才算:① 编辑器自己认为有选区(inputActions.captureInsertion() 的 span 非折叠);② 浏览器 Selection 落在可编辑区里、toString() 非空、文本能在草稿里找到、且长度与 span 对得上 |
| 证据对不上怎么办 | 静默退回整段增强,不猜位置、不改错地方。典型场景:没选中、选区跨了 mention/chip(chip 按占位计长度)、草稿刚被别处同步过 |
| 结果怎么写回 | inputActions.insertText(text, span):编辑器的一次独立撤销操作,所以逐句改也可以用 Ctrl+Z 退回,不需要回退箭头 |
| 写回被拒绝时 | insertText 返回 false(span 过期 / 正在提交 / 编辑器只读)→ 一个字符都不动,按钮进入失败态并提示「草稿在增强期间被改动」 |
| 历史上下文 | 与整段一致:默认带最近对话,Alt 不带、Shift 带全部(可与 Ctrl 叠加) |
保真闸与长度闸(v0.4.0)
模型改写时最容易伤到的是硬事实:路径、版本号、URL、反引号里的代码片段。改错一个字符,用户拿去执行就报错。
| 问题 | 现在的行为 |
|---|---|
| 提取什么 | 四条规则:反引号片段、https?:// URL、带扩展名的路径/文件名、语义化版本号(v1.2.3 / 1.2.3-rc.1)。去重后最多 40 条,长度 ≥2 才算 |
| 怎么判 | 结果里必须逐字包含每一条硬事实;缺一条就整次失败 |
| 失败后 | 结果不写回草稿,按钮进失败态并列出缺失项(最多 5 条);日志 reason=fidelity fidelity=missing:N |
| 关掉它 | fidelity: false(cordis.patch.yml / Config) |
| 长度闸 | 草稿 ≥80 字符时,结果超过 maxExpansionRatio(默认 2.5,可配 1–6)即拒绝,日志 reason=too-verbose expand=<实际>><上限>;短草稿不受比例约束(否则一句「写个周报」很容易被误判) |
闸门是硬失败而不是"提示一下":宁可让用户再点一次,也不默默把半截事实写回输入框。想保留被拒的结果,用 Alt+Shift 预览模式自己判断。
可选预览(Alt+Shift+点击,v0.4.0)
| 问题 | 现在的行为 |
|---|---|
| 触发方式 | Alt+Shift+点击(普通点击仍是直接写回,未改) |
| 显示在哪 | 输入框卡片下方(conversation.composer.dock 槽)的一张卡:标题、字符差 ±N chars、结果正文、「采用 / 放弃」 |
| 采用后 | inputActions.setDraft(结果),之后按钮的回退箭头仍可退回原文(预览阶段不留回退点) |
| 放弃后 | 草稿一个字符都不动,卡片消失 |
| 会话切换 | 预览卡只属于产生它的会话;切走后不再显示 |
| 为什么不做成默认 | 多一次确认点击会削弱"一键更快"的差异;保真闸已经把"悄悄改坏"的风险挡住了 |
配置项一览(v0.4.0)
cordis.patch.yml 里给默认值,Config schema 让插件管理器/Config Inspect 能渲染表单。
| 键 | 默认 | 作用 |
|---|---|---|
enabled | true | 总开关;关掉后点按钮直接返回「已被插件配置禁用」,不调模型 |
temperature | 0.3 | 采样温度(0–2) |
maxTokens | 2048 | 输出预算;思考 token 也计入 |
timeoutMs | 60000 | 宿主侧单次调用超时 |
history | true | 是否带最近对话作语境 |
historyTurns | 3 | 取最近几轮(1–12) |
historyMaxChars | 3000 | 历史总字符预算 |
historyPerMessageChars | 800 | 单条历史裁剪长度(头 40% + 尾 60%) |
fidelity | true | 硬事实保真闸 |
maxExpansionRatio | 2.5 | 长度闸倍数(1–6) |
provider / model | '' | 只给本插件覆盖模型;两个都填才生效(日志 modelSource=config),只填一个忽略 |
类型写错(例如 maxTokens: "2048")会让插件激活失败——这是导出原生 schema 的代价,已写进已知限制。
它是怎么跑起来的
┌─ 浏览器 ───────────────────────────────────────────────┐
│ conversation.input.right 槽(发送键前、模型选择器之前) │
│ <EnhanceButton> │
│ useInput(s => s.draft) 读草稿(+ chip 数) │
│ inputActions.setDraft(text) 整段写回(可回退) │
│ inputActions.insertText(text,sp) 局部写回(Ctrl/⌘) │
│ conversation.composer.dock 槽 │
│ <PreviewDock> Alt+Shift 预览卡:采用 / 放弃 │
└───────────────┬────────────────────────────────────────┘
│ ctx.remote.commands.execute(sessionId, '/enhance-prompt <base64url>', [], signal)
│ ← 已生成的稳定 Remote(首参 SessionId 由网关适配成 Agent,第 4 参是取消信号)
┌───────────────▼────────────────────────────────────────┐
│ Host 半 index.js │
│ ctx.commands.register({ name:'enhance-prompt', │
│ recordInput:false, … }) │
│ → enabled:false 直接拒绝(reason=disabled) │
│ → decodeRequest() 取回草稿 + 本次历史开关 + 关联 ID │
│ → agent.session.deriveMessages()(已提交的消息) │
│ → selectHistory() 选轮/裁剪/预算 → 历史上下文 │
│ → 取模型:Config 覆盖 ⇄ 会话默认(日志 modelSource=) │
│ → 取消信号 + 宿主超时才合并成一个 signal │
│ → ctx.llm.stream({ system, messages, temperature }) │
│ → clampEnhancedText() 清洗 │
│ → 保真闸 / 长度闸(失败=不写回,reason=fidelity|too-verbose)│
│ → console.info(req=… reason=… fidelity=… expand=…) │
│ → { kind:'success', text } → CommandExecution │
└────────────────────────────────────────────────────────┘
两个关键设计决定:
- Client→Host 走「session 命令」而不是自定义 Remote。
ctx.remote.<命名空间>是 typert/tsdown 在 DSH 构建期生成的产物,第三方手写包无法凭空获得;而commands/execute是已经生成好的稳定 Remote,浏览器半只需拼一行命令字符串就能调到宿主逻辑。 代价:每次增强会在聊天流里留下一个命令节点(command/run+command/done)。命令生命周期是 log-only,不进模型历史、不加 token;recordInput:false保证草稿原文不写进command/run.args。 - system 提示词走
GenerateOptions.system,不塞消息。 请求态消息类型RequestUserInput只允许role:'user',塞 system 消息会被拒。
提示词策略
增强行为完全由 prompts.js 的两个常量决定。它们分工不同,必须成对修改——任何一条语言规则都只应存在于两份提示词中,不应只改一处。
SYSTEM_PROMPT:改写的规则书
| 段落 | 作用 |
|---|---|
| 角色与输出契约 | 说明结果会逐字覆盖草稿,因此只能是一段可直接发送的提示词 |
TASK | 把目标钉死在「改写」而非「回答」 |
ANALYSIS PROCESS | 三步:评估草稿(目标、歧义与缺口、指令清晰度、缺失的上下文)→ 套用提示词工程原则(明确指令、补齐上下文、显式参数与约束、定义输出格式、匹配语气与复杂度、去冗余)→ 成文并守住原目标 |
Do NOT 段 | 不索要教程、不索要代码片段、不建议草稿未提及的技术栈/库/文件名、不解释 HOW、不回答问题 |
IMPORTANT CONSTRAINTS | ①语言匹配最高优先级 ②约 800 字符且不超原文约 3 倍 ③只输出提示词本身 |
| 两个范例 | 覆盖「极短草稿」与「自带改写指令的草稿」两种典型输入 |
USER_PROMPT_TEMPLATE:每次请求实际发出的用户消息
内嵌 {input} 占位符,由 buildUserPrompt(input) 填入草稿。
| 段落 | 作用 |
|---|---|
CRITICAL PRIORITY - LANGUAGE CONSISTENCY | 六条语言规则,与 system 提示词第 1 条约束同源,形成双重锁定 |
ENHANCEMENT REQUIREMENTS | 八条产出要求:只输出正文、禁止元注释、保留原意、必须实质增强、已清晰则轻润色、不得烂尾、不加无关要求、自成完整提示词 |
EXAMPLES | 中文 / 英文 / 中英混排 / 模糊任务 四类正例 |
BAD / GOOD 输出对比 | 直接对照「写出语言分析」与「输出纯提示词」,压制最常见的失败模式 |
三条不可动摇的规则
- 语言匹配是最高优先级,且语言判断只能影响行为、绝不能出现在输出里——
BAD OUTPUT EXAMPLE专门压制这一点,system 与 user 两份提示词各自重申一遍。 - 结果原样替换草稿,因此必须自成一段可直接发送的提示词:没有前言、没有 markdown 围栏、没有引号、没有 label。
- 不做实质增强视为失败;已经写清楚的草稿只做轻润色,不改变意图。
改提示词时只需要动这两个常量。buildUserPrompt() / cleanupEnhancedText() / clampEnhancedText() 是纯函数,不承载策略;但它们是规则 2 的最后一道防线——cleanupEnhancedText() 会去掉首尾引号、模型偶尔自作主张加的 Enhanced prompt: 之类自述 label,以及恰好包住全文的 code fence(只在围栏内再没有围栏时才脱,避免破坏草稿里本就存在的代码块)。
错误与边界
宿主失败一律是稳定的英文 Enhance prompt: … 家族,浏览器半用前缀匹配翻译成本地化文案;认不出来的文案原样透出,把真实报错交给用户,而不是吞成一句"增强失败"。
| Host 返回 | 触发条件 | 界面显示(中文) |
|---|---|---|
Enhance prompt: empty input. | 草稿为空,或载荷解码失败 | 请先输入内容 |
Enhance prompt: input too long (N > 8000 characters). | 草稿超过 8000 字符(浏览器侧也会先拦一次) | 内容过长,无法增强 |
Enhance prompt: no default model selected. | 没有可用的默认模型 | 请先在模型选择器里选中一个模型 |
Enhance prompt: timed out after 60s. | 超过 timeoutMs | 增强超时,请重试 |
Enhance prompt: cancelled. | 用户取消 | 不显示(调用方按 aborted 直接返回) |
Enhance prompt: empty result. | 模型没吐回任何可用文本(且不是被 max-tokens 截断) | 模型没有返回可用内容 |
Enhance prompt: fidelity check failed (missing: a, b). | 保真闸:结果丢掉了草稿里的硬事实(最多列 5 条) | 改写丢了原文的关键信息(如路径/版本/链接),已放弃本次结果 |
Enhance prompt: result too long (N chars vs M input chars, limit 2.5x). | 长度闸:结果超过 maxExpansionRatio | 改写结果明显比原文长太多,已放弃本次结果 |
Enhance prompt: disabled by plugin config. | 总开关 enabled: false | 增强提示词已被插件配置禁用 |
Enhance prompt: model ran out of output budget (max-tokens, maxTokens=N). | 思考过程把 maxTokens 用完了,正文为空 | 模型的输出额度被思考过程用完了,请重试或提高 maxTokens |
Enhance prompt failed: model stream ended with error. / … with aborted. | 适配器用终止性 finish 块报错(不抛异常) | 增强失败 — model stream ended with error. |
Enhance prompt failed: <detail> | 模型调用抛错 | 增强失败 — <detail> |
结果为空时怎么查
宿主日志里有三种证据,按顺序看:
| 日志 | 含义 | 处理 |
|---|---|---|
history: off / no-session / derive-failed / empty / ok | 本次到底有没有把对话带进去,卡在哪一层(no-session = 命令没拿到会话,derive-failed = 读会话抛错) | 预期要带历史却没带,看这一行就够了 |
model hit the output token budget before producing text finish=max-tokens text=0 chars inputTokens=… outputTokens=… reasoningTokens=… | 思考把预算吃光 | 提高 maxTokens,或换一个思考量更小的模型 |
model returned no usable text finish=stop text=0 chars … | 模型正常结束但没给正文 | 换模型重试;再复现就把这条日志贴出来 |
表里都是宿主返回的文案;另有一类是浏览器半自己的提示:action.draftChanged(请求期间你改动了草稿,结果不覆盖新输入,请重试)。
三条长度/时间上限分属两个运行时,改动必须同步(浏览器半是独立脚本,无法共享宿主常量):
| 位置 | 常量 | 值 | 作用 |
|---|---|---|---|
prompts.js | MAX_OUTPUT_CHARS | 1600 | 结果硬上限的下限;宿主实际按 min(8000, max(1600, 原文长度)) 放宽,长草稿不会被砍短 |
index.js | MAX_INPUT_CHARS | 8000 | 宿主侧草稿上限 |
client.js | MAX_TEXT_LENGTH | 8000 | 浏览器侧草稿上限,发送前先拦 |
index.js | DEFAULT_TIMEOUT_MS | 60000 | 模型调用超时 |
关于载荷:草稿是任意多行文本,走命令行必须先编码,这里用 UTF-8 → base64url,字节数约膨胀 4/3。8000 个 ASCII 字符 ≈ 10.7K 字符命令行;8000 个汉字(UTF-8 3 字节)≈ 32K 字符命令行。命令节点是 log-only,不进模型历史。
日志与排障(v0.3.1)
每次点击会在两端各留一行日志,用同一个 req= 关联:
# 浏览器 console(client.js)
[dsh-composer-enhance] req=ab12cd start whole chars=42 history=auto
[dsh-composer-enhance] req=ab12cd ok ms=1830 text=57 mode=whole
# 宿主 console(index.js)
[dsh-composer-enhance] req=ab12cd ok reason=ok ms=1830 provider=deepseek model=chat finish=stop in=1900 out=90 reasoning=0 text=57 history=ok/2of2 historyChars=310 historyMessages=4 maxTokens=2048 modelSource=session fidelity=ok expand=57<=105
[dsh-composer-enhance] req=7c1d2e fail reason=fidelity fidelity=missing:2
字段含义:
| 字段 | 含义 |
|---|---|
req= | 本次点击的关联 ID(6 位 base36)。v2 载荷新增可选字段 id;旧载荷没有时宿主补一个 |
ok / fail | 成/败;失败了看 reason= |
reason= | ok、empty-input、too-long、no-model、cancelled、timeout、client-timeout、stream-error、stream-aborted、max-tokens、empty-result、fidelity、too-verbose、disabled、draft-changed、insert-rejected |
ms= | 端到端耗时 |
finish= | 模型流终止原因:stop / max-tokens / error / aborted |
in= / out= / reasoning= | 输入 / 输出 / 思考 token;? 表示模型没报 usage |
history= | <reason>/<带入轮数>of<总轮数>,例如 ok/2of2、off/0of0、no-session/0of0、empty/0of0、derive-failed/0of0 |
historyChars= / historyMessages= | 历史占用字符数 / 参与挑选的纯文本消息条数 |
modelSource= | session(用会话模型)或 config(用 Config 里的 provider+model 覆盖) |
fidelity= | ok 或 missing:N(保真闸缺了几条硬事实) |
expand= | <结果长度><=<上限>(通过)或 <结果长度>><上限>(被长度闸拒绝) |
排障顺序:① 用 req= 在两端 grep 到同一行,确认是同一次点击;② 看 reason= 落在哪一层;③ fail 且 finish=max-tokens → 提高 config.maxTokens;④ history=*/0of0 → 对照历史开关与修饰键。
目录结构
| 文件 | 职责 |
|---|---|
package.json | 声明 dsh.bundle.patch(Host 行)与 dsh.client(浏览器半);exports['./client'] 指向手写 client.js;engines.dsh 给出宿主版本区间 |
cordis.patch.yml | 把本包插进 profile 的 Loader 图,并带上可调参数:- insert: [{ id, name, config: { temperature, maxTokens, timeoutMs } }] |
index.js | Host 半:export const inject + export function apply(ctx, config),注册命令、实现 enhanceText() |
prompts.js | 提示词资产与结果清洗(buildUserPrompt / cleanupEnhancedText / clampEnhancedText / MAX_OUTPUT_CHARS) |
client.js | 浏览器半:window.__ModuleLoader__.load({ id, factory }),注册 locale、conversation.input.right(按钮)与 conversation.composer.dock(预览卡)两个槽,并把宿主错误文案本地化 |
locale/{zh,en}.json | 插件管理器展示元数据(meta.title / meta.description)。不是 Client 字典——字典内联在 client.js 里,两处用途不同、不重复注册 |
icon.svg | 插件图标(≤256KiB、相对路径、单色 currentColor) |
README.en.md / README.md | 英文 / 中文说明。npm 只自动带 README.md,英文版靠 files 白名单显式带上 |
LICENSE | MIT 许可。仓库根目录必须有;npm 包的 files 里也显式包含 |
test/ | 离线验证脚本,零依赖、不联网、不进 npm 包:verify-host.mjs(宿主半)、verify-client.mjs(浏览器半)、verify-manifest-yaml.mjs(引导期 YAML)、live.mjs(opt-in 真模型回归,无 Key 时跳过) |
.github/workflows/ci.yml | CI:Node 20/22 矩阵跑 npm run check + npm test |
CHANGELOG.md | 版本变更记录 |
.gitignore | 忽略 node_modules/、*.tgz 等本地产物 |
三个必须遵守的契约
- bundle 契约:
package.json→dsh.bundle.patch→ YAML- insert:一行;Host 半导出apply(ctx, config)与可选inject(本包为['commands','llm','agentDefaultModel'])。所有注册都写在ctx.effect(() => …)里,返回清理函数。 - browser 契约:
dsh.client = { platform:'web', immediately:true, inject:[…] }+ 导出./client;client.js必须是纯 JS(无 JSX/TS/import),__ModuleLoader__.load的id必须等于包名,只能require('react'),不得 require 任何@deepseek-ai/dsh-client-*包;样式只能用 Theme 的--dsw-alias-*token;可见文案走ctx.locale.register。 - 槽契约:
ctx.slots.inject(SLOT, () => ctx.slots.register({ name, id, order, locale, inject }, Component))。conversation.input.right是 list 槽、replaceRisk: none;组件自动获得sessionId/useInput/inputActions/t等标准 props。
安装与验证
# 1) 安装
plugin_manager action=install_bundle target=dsh-composer-enhance
# 2) 确认行已生效(看 application / warnings,不看"没报错")
plugin_manager action=list_plugins
# 3) 免审批地确认槽里出现了占用者(occupants 里应出现 id=dsh-composer-enhance)
cordis_inspect_query platform=client provider=Slots method=listSubTree
input={"root":"conversation.input.right"}
然后:刷新页面 → 发送键左侧出现魔法棒按钮 → 输入一句中文草稿 → 点按钮 → 按钮转为转圈(再点即取消)→ 草稿被替换且语言保持一致 → 按钮变为回退箭头 → 点击回到原文;手动改过增强结果后,回退箭头自动消失。
回归输入(同时验证语言一致性与"只改写不回答"):
| 输入 | 期望 |
|---|---|
做个登录页面 | 中文,补全校验/错误态/移动端等具体要求,不实现代码 |
explain what this code does | 全英文,不解释 HOW、不编造技术栈 |
这段代码有 bug,can you help me fix it? | 保持中英混排,不塌成单一语言 |
修复它 | 中文,只补上下文/范围,不擅自指定框架 |
| 已经写得很完整的长草稿 | 轻润色,不改变意图,长度不超原文约 3 倍 |
| 空草稿 | 按钮不显示 |
| 临时断网/把模型选成不可用的 provider | 按钮进入失败态并给出可读原因,不改变草稿 |
开发与验证
node test/verify-host.mjs # 57 项:manifest / 文件清单、prompts.js 纯函数、index.js 宿主半(历史、超时、取消、错误、配置收敛、Config schema、关联 ID 与日志、载荷自识别、保真闸与长度闸、总开关、模型覆盖)
node test/verify-client.mjs # 31 项:模块与两个槽注册、locale、宿主文案本地化、点击成功/失败/超长、焦点与失败态清理、会话切换、局部增强、重入锁与关联 ID、chip 保护、预览
node test/verify-manifest-yaml.mjs # cordis.patch.yml 用真实 yaml 解析器过一遍(本机没有 yaml 依赖时自动 SKIP)
node test/live.mjs # opt-in 真模型回归;没有 DEEPSEEK_API_KEY 时打印跳过、退出码 0
npm test # 前三个一起跑(不联网)
npm run check # 三个 JS 产物的 node --check
三个离线脚本只用 Node 标准库、不联网、不需要 npm install。改动源码或提示词后先跑它们再发版;prepublishOnly 还会在 npm publish 前对三个 JS 产物做一次语法检查。test/live.mjs 需要 DEEPSEEK_API_KEY(可选 DEEPSEEK_BASE_URL / DEEPSEEK_MODEL),它验的是提示词资产在真模型上的行为,不碰 DSH 插件链路。
常见调整点
- 换提示词:只改
prompts.js的两个常量,其余不动;两份提示词里的语言规则必须保持同步。 - 换模型/温度/超时:改
cordis.patch.yml该行的config.temperature/config.maxTokens/config.timeoutMs;index.js的readOptions()会把越界或非数字的值回落到DEFAULT_*。这些键现在也通过Configschema 暴露出来了,插件管理器 / Config Inspect 里能直接看到它们的类型与默认值。 - 调历史上下文:同一行的
config.history(总开关)、config.historyTurns(1–12)、config.historyMaxChars(200–12000)、config.historyPerMessageChars(100–4000);取舍算法在index.js的selectHistory(),纯函数、被test/verify-host.mjs直接覆盖。 Configschema:index.js用@deepseek-ai/schemastery声明 12 个可调项的类型与默认值(温度/预算/超时、四个历史键、enabled/fidelity/maxExpansionRatio、provider/model)。这是dependencies里唯一一个运行时依赖;解析不到时自动退化成"不导出Config",插件照常加载。- 保真闸与长度闸:规则在
index.js的FIDELITY_PATTERNS/missingFidelityFacts()/expansionVerdict(),都是纯函数,被test/verify-host.mjs直接覆盖。想放宽就调config.maxExpansionRatio(1–6);不想要就直接config.fidelity: false。 - 预览卡:
client.js的PreviewDock+ 模块级previewStore(发布/订阅),挂在conversation.composer.dock槽、order: 20。想改样式只动client.js顶部的 CSS 字符串。 - 总开关与模型覆盖:
config.enabled: false让按钮直接返回"已被配置禁用";config.provider+config.model同时给才会覆盖会话模型(日志modelSource=config)。 - 思考档位:
agentDefaultModel.currentSelection()返回{ provider, model, reasoningEffort? },代码已把它透传进ctx.llm.stream。 - 载荷与超时上限:
client.js的MAX_PAYLOAD_CHARS(65536,v2 载荷 base64url 后的长度)与CLIENT_TIMEOUT_MS(65000);index.js的MAX_INPUT_CHARS(8000)必须与client.js的MAX_TEXT_LENGTH一致——两个运行时分属浏览器与 Node,无法共享常量。 - 同时暴露给 Agent:解开
index.js末尾注释的ctx.tools.register,与按钮共用同一个enhanceText(ctx, request, signal, options, agent)(官方规范要求 UI 动作与 Agent 工具不得各写一套逻辑;enhanceText()直接收{ text, history? },工具入口不需要任何命令行编码)。 - 想改为「紧贴发送键」的展开态交互:把槽换成
conversation.input.activity(single 槽,replaceRisk: shadows-shipped-ui),并实现onActiveChange的展开/释放。
已知限制
- 思考型模型吃预算:DSH 默认开启思考,思考 token 也计入
maxTokens(默认 2048)。如果日志出现finish=max-tokens且正文为空,说明预算被思考用完,提高config.maxTokens(上限 8192)或换思考量更小的模型。 - 客户端兜底超时是固定 65s:
client.js的CLIENT_TIMEOUT_MS必须比宿主config.timeoutMs(默认 60000)长,否则客户端会抢在宿主超时文案之前报"增强超时"。把宿主超时调到 65s 以上时,请一并改这个常量(两个运行时不共享常量)。 - 连点不会发起第二次调用:增强中再点按钮仍是"取消"(原有语义);渲染还没刷新时的重复 click 由
controllerRef重入锁挡掉。 - 聊天流会出现一个命令节点;这是复用
commands/execute的必然后果。 - 历史上下文取的是
deriveMessages()的已提交消息,当前草稿不在其中;也就是说"这次输入的草稿"本身不会被当成历史的一部分(避免把同一段文字算两遍)。 - 历史只带 user/assistant 的纯文本块:工具调用、工具结果、图片块都被丢弃——它们对"改写草稿"没有帮助,还会吃掉预算。
- 保真闸是硬失败,不是提示:结果丢了硬事实就整次丢弃、不写回;想保留被拒结果只能用
Alt+Shift预览模式自己判断(fidelity: false可关,但不建议)。 - 长度闸只对 ≥80 字符的草稿生效:太短的草稿没有稳定的"合理倍数",容易被误判,所以短草稿只受输出硬上限约束。
- 预览不会自动采用:
Alt+Shift之后必须点卡片上的「采用」,切走会话或刷新页面后待确认结果会丢失(它只在内存里)。 - 引用 chip 会被保护但不会被处理:草稿里有
@文件/引用时按钮直接禁用,不会替你展开引用内容再用上(竞争对手@huanhuan123/dsh-prompt-optimizer会读引用内容,这是有意不跟进的:读文件涉及权限边界与额外延迟)。 - 配置会被 schema 校验类型:导出
Config后,cordis.patch.yml里写错类型(例如maxTokens: "2048"字符串)会让插件激活失败,而不是像以前那样静默回落。取值范围仍由readOptions()钳制,所以只有"类型写错"才会报错,改回数字即可恢复。 - 局部增强依赖真实选区:
Ctrl+点击只在"编辑器认的 span"和"浏览器 Selection"两路证据都对得上时才生效;选区跨 mention/chip、草稿刚被别处同步、输入法还在组词时都会静默退回整段增强。它是可选加速路径,不是默认路径。 - 局部增强没有回退箭头:局部写回是编辑器的一次独立撤销操作,回退用编辑器本身的
Ctrl+Z;insertText被拒绝(span 过期 / 正在提交 / 只读)时一个字符都不动,只报错。 - 增强结果写回整段时仍是
setDraft(text)替换草稿;局部模式才会走captureInsertion()+insertText(text, span)。 - 两个 8000 与 1600 分属两个运行时,无法共享常量,改动需按上表同步。宿主侧的结果上限会随原文长度放宽(
min(8000, max(1600, 原文长度))),所以很长的草稿不会被砍到 1600。 useInput((state) => state.draft)会随每次按键重渲染按钮。组件本身需要草稿文本来发起请求和判断空态,无法用更窄的选择器规避;按钮很小,实测影响可忽略,故未做特殊处理。- 界面文案内联在
client.js,locale/*.json只服务插件管理器元数据;两者不是"同一份文案的两个副本"。 - 仓库根目录已经放了
LICENSE(files白名单只约束 npm 包,git /link:安装会带上整棵树);版权行目前是dsh-composer-enhance contributors,正式对外发布前建议改成你自己的名字或组织名。
License
MIT