dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH Web GUI 插件:在模型选择器左侧加一个「AI 优化输入」按钮,点击后用可自定义的提示词优化输入框里的内容,并把结果写回输入框(撤销见路线图 P3)。
设计依据、座位/接口证据与分阶段计划见 DESIGN.md。
| 阶段 | 内容 | 状态 |
|---|---|---|
| P0 | 骨架:座位注册 + 按钮出现在模型左侧 | ✅ 已在 Web GUI 目视确认(2026-09-10) |
| P1 | 宿主半:/api/dsh-input-optimizer/optimize + ctx.llm.stream() 一次性调用 | ✅ 已实现 |
| P2 | 前后端接线:读草稿 → POST → setDraft + CAS + 取消 + 失败提示 | ✅ 已实现 |
| P3 | 撤销栈 + CAS 校验 + 撤销按钮(含强制还原、10 层深度) | ✅ 已实现 |
| P4 | 设置页:配置模型(名称 + 调用参数)与提示词,持久化并即时生效 | ✅ 已实现 |
| P5.0 | 可运行性:dev 依赖软链脚本 + 真机安装验收 | ✅ 已实现(2026-09-11) |
| P5.1 | 缺陷修复:日志全丢、/check 契约判型、未知终态误判失败、405 误报、保存假成功、组合层区间、样式归属 | ✅ 已实现(2026-09-11) |
| P5.1b | 真机事故:设置页恒显示「设置服务不可用」——命名空间注册是激活时读一次,输给了 settings 服务就绪的竞态 | ✅ 已修复(2026-09-11,含真框架集成测试)。0.2.0 起该服务已移除,整段注册逻辑随之下线,见 P15 |
| P5.2 | 规则单一来源:区间/上限/预设由 /catalog 下发,客户端不再维护会漂移的镜像 | ✅ 已实现(2026-09-11) |
| P5.3 | 预设菜单:输入框旁 ▾ 菜单,选中后请求带 presetId | ✅ 已实现(2026-09-11) |
| P5.4 | 信任判定改走框架 ctx.connection.requestRejection()(能力路由要浏览器会话) | ✅ 已实现(2026-09-11) |
| P5.5 | 并发闸门:同会话单航班(409)+ 全局并发上限(429) | ✅ 已实现(2026-09-11) |
| P5.7 | 工程化:Biome lint、约定守卫、真 React 渲染测试、CI | ✅ 已实现(2026-09-11) |
| P5.6 | 流式回填:/optimize/stream(SSE)边生成边替换草稿,失败还原原文 | ✅ 已实现(2026-09-11) |
| P6.1 | 多选优化风格:▾ 下拉框里勾选「精简 / 转规格」,可叠加,请求带 styleIds | ✅ 已实现(2026-09-11) |
| P6.2 | 逐风格提示词:设置页为每个风格单独配提示词(内置 ← 组合配置 ← 设置页) | ✅ 已实现(2026-09-11) |
| P6.3 | 打开插件配置文件:设置页一键用编辑器打开 cordis.patch.yml,失败有明确提示 | ✅ 已实现(2026-09-11) |
| P6.4 | Web 启动耗时排查:基线 1886ms/12 轮,插件边际成本 ~5ms,无阻塞(见 .perf/README.md) | ✅ 已完成(2026-09-11) |
| P7.0 | 系统提示词可见:设置页展示默认系统提示词(内置/组合层),可查看、可一键"以默认为基础编辑" | ✅ 已实现(2026-09-11) |
| P7.1 | 追加提示词:设置页可新增多条命名提示词,输入框旁 ▾ 菜单随时切换,落盘并即时生效 | ✅ 已实现(2026-09-11) |
| P7.2 | 恢复默认配置:一键还原全部用户设置(含追加提示词与内置覆盖),带确认 | ✅ 已实现(2026-09-11) |
| P8 | 风格并入清单:精简/转规格成为追加提示词的内置条目(单选切换),不再单独多选;styleIds 仅为旧客户端兼容保留 | ✅ 已实现(2026-09-11) |
| P9 | 命名与语义对齐:设置页改为「系统提示词 / 追加提示词」,追加条目只追加、不替换系统提示词 | ✅ 已实现(2026-09-11) |
| P10 | 设置界面重设计:输入框默认隐藏(点「编辑」才展开)、行距压到最小、四个分组 + 一行摘要同屏可见 | ✅ 已实现(2026-09-11) |
| P11 | 进度与思考过程:优化中显示「阶段 + 耗时 + 字数」,并把模型的思考增量透传成「思考过程」面板 | ✅ 已实现(2026-09-11) |
| P12 | 控件收敛:工具行只留 ✦ 与 ▾,撤销与思考回看收进 ▾ 菜单的「本次调用」分区;▾ 常驻并带"可撤销"角标;成功不再弹提示 | ✅ 已实现(2026-09-11) |
| P13 | 通用输入角标:主输入框之外的每个输入窗口(页面上的 textarea / 显式报名的控件)也挂一枚 ✦ 角标,点击即优化该输入框,成功后可 ↶ 撤销 | ✅ 已实现(2026-09-13) |
| P14 | 思考强度:设置页「调用参数」里可配优化时用的思考强度(内置默认 low);宿主按模型公布的能力核对后才透传,模型不支持则回落适配器默认 | ✅ 已实现(2026-09-13) |
| P15 | 适配 dsh 0.2.0 设置通道:ctx.settings.register / settingsScope 已移除,改为插件 Config schema 自动投影——表单只服务 .volatile() 字段,且框架交给 apply 的是只读引用;组合层与用户层分键、用户层字段每次请求现读 | ✅ 已实现(2026-10-03,含表单可服务性的结构断言) |
| P5.7b / P5.7c / P5.8 | typecheck(缺 tsc)、vitest+jsdom、芯片保留 | ⬜ 待做(见「下一步」与「工程化」) |
检查:lint 零发现 · 约定守卫 30 条 · 测试 73 + 82 + 6 + 15 = 176 例,全绿
(npm run verify = lint + 全部检查;npm test 会先自动补齐 dev 依赖链接;
test/settings-activation.mjs 在当前 dsh 安装下按 SKIP 计,见「工程化」)。
运行前提:仓库里没有
node_modules时,npm test与link:方式安装后的运行时都跑不起来 (原因与自动修法见「开发循环」)。
package.json 双半声明:main(lib/index.js) + exports["./client"] + dsh.client/bundle
biome.json lint 配置(只 lint 不 format,见「工程化」)
cordis.patch.yml bundle patch + 组合层配置(设置页的用户值优先于它)
lib/index.js 宿主半:6 条路由(一次性 JSON + 流式 SSE + 目录/试调 + 打开配置文件)+ LLM 调用 + 并发闸门
lib/settings.js 宿主半:插件 Config schema(用户层字段标 `.volatile()`,框架据此投影成设置表单)
lib/policy.js 策略层:零依赖,配置校验/信任围栏/生效配置解析/风格提示词分层/追加提示词/显示思考过程/思考强度(可独立单测)
lib/client.js 浏览器半:输入框按钮 + ▾ 统一菜单(撤销 / 思考回看 / 追加提示词 / 预设)+ 撤销栈 + 进度行与思考面板 + 通用输入角标(P13)+ 设置页(手写 __ModuleLoader__ bundle)
lib/types/*.d.ts 对外类型
.perf/ Web 启动耗时基准脚本与测量报告(README.md 有方法与原始数据)
scripts/check-guards.mjs 约定守卫:把踩过的坑变成可自动检查的规则(30 条)
scripts/dsh-packages.mjs 定位 dsh 安装与其中的宿主包(脚本与测试共用)
scripts/link-dev-deps.mjs 把宿主依赖软链进本仓库(`npm test` 前自动跑;CI 里自动跳过)
scripts/lint.mjs 找 Biome 并跑 lint(仓库内 / 全局安装都能用)
test/smoke.mjs 宿主半冒烟测试(73 例)
test/client.smoke.mjs 浏览器半冒烟测试(82 例:接线、控件收敛、流式回填、进度行与思考面板、统一菜单、撤销栈、设置页紧凑布局与思考强度)
test/client.react.mjs 真 React 渲染测试(6 例:真 react/react-dom SSR,含"不得有 React 警告")
test/client.universal.mjs 通用输入角标测试(15 例:自带小 DOM 替身,覆盖扫描/定位/遮挡/写回/取消/手改/失败/回退/动态挂摘/卸载)
test/settings-activation.mjs 真框架集成测试(7 例;dsh 0.2.0+ 已移除 `dsh-settings-file`,当前安装下整体 SKIP,待按新通道重写)
.github/workflows/ci.yml CI:lint + 约定守卫 + 五个套件(Windows)
DESIGN.md 设计依据:座位/接口证据、撤销追加提示词、提示词分层、风险清单
LICENSE MIT
前置:dsh 0.2.0-rc.2+,profile 为 web(即 ~/.dsh/profiles/web)。
第 1 步(两种方式都要做):把本包装进 profile,让包名能被解析到:
dsh plugin --profile web add link:F:\dsh\dsh_BetterInput
第 1.5 步(link: 专用,容易漏):在本仓库建好宿主依赖的软链:
npm run link-deps # = node scripts/link-dev-deps.mjs,自动定位 dsh 安装目录
为什么运行时也需要它:
link:在 profile 里放的是符号链接,Node 按 realpath 解析模块, 于是lib/index.js的import '@deepseek-ai/dsh-llm'是从本目录往上找node_modules的——$DSH_HOME/profiles/node_modules那个镜像根本不在解析路径上。缺了它宿主半会ERR_MODULE_NOT_FOUND(boot 直接失败),不是"只有测试受影响"。 从 npm 正式安装(非link:)时包会被拷进 profile 的node_modules,镜像在解析路径上,无需这一步。
⚠️ Windows 上不要在 patch 里写绝对路径当插件名:Loader 对非
./非cordis:的 specifier 直接交给import(name),D:\...会被当成 URL schemed:. 解析而失败; 以.开头的相对 specifier 又是相对 profile 目录(在 C: 盘)解析的,跨盘也无解。 所以唯一可靠的方式是「装进 profile 的 node_modules,再用包名引用」。
dsh plugin --profile web add 已经自动把包名追加进 dsh.profile.bundles(实测 0.1.2-rc.1,
无需手改 JSON)。只有在手工拷贝/手改依赖的场景才需要自己往 dsh.profile.bundles 里加一行
"dsh-better-input"——而且绝不要重复添加:同一个 bundle 会被组合两次,插件的
(kind: 'exact', path) 路由重复注册会直接抛错(webserver: duplicate exact route ...),表现为起不来。
然后重启 dsh web。本包的 cordis.patch.yml 会作为 bundle 层被自动应用(dsh.bundle.patch)。
在第 1 步之后,把下面这段拷进 ~/.dsh/profiles/web/cordis.patch.yml(该文件里已有一个 mcp-everything 的同构例子),再重启:
- insert:
- id: better-input
name: 'dsh-better-input'
config:
systemPrompt: |
你是提示词工程师……
这段内容与本包自带的 cordis.patch.yml 等价;用方式 A 时改的是包里那份,用方式 B 时改的是 profile 那份。
改动宿主半(
lib/index.js/lib/policy.js/lib/settings.js)或改安装方式后必须重启dsh web; 只改lib/client.js时靠 HMR,刷新即可。
http://127.0.0.1:3080,输入框工具行右侧、模型选择器紧左边出现 ✨ 按钮(data-dsh-better-input="better-input",可用 DevTools 搜到)。空输入时按钮为禁用态。better-input: mounted /api/dsh-input-optimizer/optimize
(ctx.get('logger') 恒为 undefined,所以这一行只有在用 ctx.logger 时才真的会出现)。直接打路由:curl.exe -s -X POST http://127.0.0.1:3080/api/dsh-input-optimizer/optimize `
-H 'content-type: application/json' `
-d '{"text":"帮我把那个脚本弄一下,快点"}'
# → {"text":"...优化后的提示词...","modelUsed":{"provider":"...","model":"..."}}
非本机来源(远程 IP / 异源 Host / Sec-Fetch-Site: cross-site)一律 403。
3. P4(设置页真的可用):重启后
curl.exe -s http://127.0.0.1:3080/api/dsh-input-optimizer/catalog
# 必须看到 "settings":{"available":true,...}
宿主日志里应出现 better-input: mounted /api/dsh-input-optimizer/optimize (+stream/catalog/check/open-config)
(0.2.0 起不再有 settings namespace "better-input" registered 那行——表单由框架按 Config schema
自动投影,本插件不再登记命名空间)。
设置页保存后,当前 profile 的 Cordis patch(~/.dsh/profiles/web/cordis.patch.yml)里应出现
该 entry 的 config:,键是用户层存储键(userSystemPrompt / userTemperature / …);
catalog 的 sources.* 相应从 config/default 变为 settings。
4. P8/P9(风格并入清单 + 系统提示词 / 追加提示词):✨ 右侧的 ▾ 菜单最上方是追加提示词区:
「不追加」+ 精简 / 转规格(内置条目,● = 当前启用)。点「转规格」→ 选中标记移动、提示"已切换"
(activeProfileId 落盘);点 ✨ 优化,请求体不带 styleIds,效果 = 系统提示词 +
本次额外要求(转规格):…(/catalog 的 effective.profileId === 'spec')。
到设置页「追加提示词」给「转规格」填正文并保存,再点 ✨:追加的那段换成你写的文字
(系统提示词本身不变);清空再保存则回到内置追加文案。
5. P6.3(打开配置文件):设置页底部点「打开插件配置文件」→ 用编辑器打开
F:\dsh\dsh_BetterInput\cordis.patch.yml(设置页上同时显示这个绝对路径)。
可用 node .perf/verify-open-config.mjs 走同一条链路做命令行验收。
6. P7(默认系统提示词可见 + 追加提示词切换):设置页「系统提示词」区点「查看默认系统提示词」→
展开宿主下发的默认正文;「追加提示词」区点「新增追加提示词」、填名称与正文、保存;
到输入框旁 ▾ 菜单点这条(● 移到它上面、提示"已切换")→ 点 ✨ 优化,DevTools → Network 里
这次请求虽不带追加提示词信息,但宿主 profile patch 里的 activeProfileId 已变、
模型收到的 system 里多出 本次额外要求(名称):…(/catalog 的 effective.profileId 同步更新,
sources.prompt 仍如实标注基底来自哪一层)。
「恢复默认配置」点一下会先弹确认,确认后追加提示词、自定义系统提示词、内置条目覆盖全部清空,
/catalog 的 effective.sources.prompt 回到 config/default。
7. P10(设置界面重设计):打开 设置 → 输入优化:
cordis.patch.yml 里配了非内置 id 的 presets 时,▾ 菜单下半部分
列出它们;点某一项 → 请求体里带 presetId(可用 DevTools 的 Network 面板确认)。
一个都没配时只有追加提示词区,界面与从前一致。POST /optimize 需要浏览器会话(页面 cookie 由 dsh web 打印的
带 token 的 URL 换取)。命令行只做排查时用只读路由:curl.exe -s http://127.0.0.1:3080/api/dsh-input-optimizer/catalog # 元数据:免会话(仅环回)
curl.exe -s -X POST http://127.0.0.1:3080/api/dsh-input-optimizer/check `
-H 'content-type: application/json' -d '{"provider":"leihuo","model":"deepseek-v4.1-flash"}'
# POST /optimize 不带 cookie 会得到 401 unauthorized(这是有意的:凭据可能来自环境变量,
# 本机其它进程不该能借这条路由花掉它)。要从命令行调它,就把浏览器 DevTools →
# Application → Cookies 里那条 dsh 会话 cookie 用 `-b "<name>=<value>"` 带上。
curl 看 /catalog 的 effective.showReasoning === false 佐证);event: reasoning(若有思考)与 event: delta,
且任何思考文本都不会出现在草稿或撤销记录里。[data-dsh-better-input-wrap] 的直接子按钮);data-dsh-better-input-badge="better-input"),
只读/禁用/隐藏的输入框没有角标;滚动页面或缩放窗口时角标跟着输入框走;data-dsh-better-input-skip 属性即可;非 textarea 的控件想接上,
加 data-dsh-better-input-host;textarea)。npm run verify # lint + 约定守卫 + 五个套件(推荐)
npm test # 约定守卫 + 宿主半 + 浏览器半 + 真 React + 通用角标 + 真框架集成
node test\smoke.mjs # 宿主半 73 例:生效配置、信任判定、并发闸门、六路由全链路、SSE 分帧、思考透传、追加提示词(含内置种子)、打开配置文件、Config schema 与 volatile 引用现读
node test\client.smoke.mjs # 浏览器半 82 例:座位、控件收敛、流式回填、进度行与思考面板、统一菜单、撤销栈、设置页紧凑布局
node test\client.react.mjs # 真 React 6 例:真 react/react-dom SSR 渲染(含"不得有 React 警告")
node test\client.universal.mjs # 通用角标 15 例:扫描/定位/遮挡/写回(原生 setter + input 事件)/取消/手改/失败还原/旧宿主回退/动态挂摘/卸载
node test\settings-activation.mjs # 真框架集成 7 例:需要 @deepseek-ai/dsh-settings-file(0.2.0+ 已移除,当前安装下 SKIP)
node .perf\measure-startup.mjs 12 # Web 启动耗时基准(12 轮冷启动;详见 .perf/README.md)
各项检查覆盖什么/不覆盖什么、以及 typecheck 为何还没上,见「工程化」一节。 宿主半测试与运行时都需要
@deepseek-ai/dsh-llm与@deepseek-ai/cosmokit(用户层字段是 volatile 引用,宿主半用isVolatile解引用)可见,设置 schema 还需要@deepseek-ai/schemastery;集成测试还需要@deepseek-ai/cordis;真 React 套件需要配对好的react/react-dom。npm test的pretest会自动建这些软链(npm run link-deps); 脚本会在$DSH_HOME/profiles、~/.dsh/profiles、nvm 安装目录里找 dsh,找不到才报错 (也可用$env:DSH_INSTALL_ANCHOR显式指定含node_modules的目录)。
| 场景 | 行为 |
|---|---|
| 点击 ✨ | POST /api/dsh-input-optimizer/optimize/stream(默认;/optimize 是回退),body { text, sessionId, presetId? }(styleIds 仅为旧客户端兼容保留),成功后 setDraft 写回 |
| 工具行里有什么(P12) | 只有两个控件:✦ 优化(生成中 = 取消)与 ▾ 菜单。撤销、思考回看、追加提示词、预设全在 ▾ 里,见下「控件收敛」一节 |
| ▾ 菜单的「本次调用」分区 | 撤销(有可撤销记录时出现)与「查看思考过程(n 字)」(本次调用收到过思考时出现)。这一区随"上一次调用的产物"出现/消失,不依赖 /catalog |
| 可撤销角标 | 有可撤销记录时 ▾ 右上角有个 5px 圆点——撤销住进菜单后,菜单收起时这是唯一的提示 |
| ▾ 菜单里的「追加提示词」 | 单选:「不追加」+ 内置条目(精简 / 转规格)+ 设置页里保存的各条追加提示词,选中的那条接在系统提示词之后。切换即写进设置(activeProfileId)落盘,下一次优化生效;菜单不收起,选中项带 ● 标记 |
| 内置追加提示词(精简 / 转规格) | 追加提示词清单的常驻种子:未被覆盖时,它的正文就是该风格的追加要求原文(与旧"勾选风格"逐字节一致);在设置页改了它就是改这段追加文案。不可删除 |
| 追加提示词(设置页) | 内置条目与自定义条目在同一个清单里:改内容、单选「启用」;可新增多条命名追加提示词。系统提示词永远在场,追加只增不改。见下「追加提示词」一节 |
预设菜单(▾) | 与内置条目同 id 的预设不列进预设区(它们是内置条目的追加文案来源);其余预设点一次跑一次,请求带 presetId,宿主把该预设的 prompt 追加到 system。菜单向上弹出,点外面或 Esc 收起 |
| 草稿超过宿主上限 | 本地直接提示「草稿过长(n/上限)」,不发请求(上限来自 /catalog 的 limits) |
| 流式回填 | 默认走 SSE:增量到达即改写草稿(80ms 节流);失败/中断会还原原文(详见下节) |
| 优化中的进度 | 按钮旁常驻一行「阶段 + 已耗时 + 已收字数」(等待模型 / 思考中 / 写入中),耗时由本地计时器推进;按钮图标带呼吸动效。不提供百分比——maxOutputTokens 是上限不是目标,任何比例都是编的(详见「进度与思考过程」) |
| 思考过程 | 模型返回思考内容时,按钮上方自动展开「思考过程」面板(实时跟随、可滚动);收尾自动收起,之后从 ▾ 菜单的「查看思考过程(n 字)」回看。可在设置页「调用参数」里关掉(关掉后宿主根本不发思考内容) |
| 同会话重复请求 | 宿主返回 409 busy-session(多标签页同时点同一会话时可见),提示「这个会话已经在优化中了」 |
| 全局并发打满 | 宿主返回 429 too-many-requests(默认上限 4,可用 maxConcurrentCalls 调),提示带上限值 |
| 生成中再点 | 取消(abort;宿主侧同时取消上游模型调用,不产生费用累积) |
| 生成中用户继续打字 | 流式下用**"本次调用里我们写过的每一版文本"集合**做 CAS(draftRev 每次写入都会推进,不能当基线):当前草稿落在集合之外即认定用户手改 → 中止本次并提示「草稿已变化」 |
| 成功后 | 不弹提示(P12):草稿被替换是肉眼可见的;「还能撤销」由 ▾ 上的圆点表示。只有截断这类"界面上看不出来"的结果才提示 |
| 撤销 | 在 ▾ 菜单的「本次调用」分区里点一次;CAS 通过才回退到优化前草稿(判据是"当前草稿仍等于 after")。草稿被手改过时第一次点击只武装(菜单项文案变成「强制还原原文」,菜单保持展开),再点一次强制还原 |
| 撤销深度 | 每会话 10 层,可连按逐层回退;按会话隔离,最多保留 20 个会话(LRU) |
草稿含 @引用 芯片 | 拒绝发起(整体 setDraft 会把芯片拉平成纯文本),提示先删掉芯片。注意 occurrences 只覆盖引用芯片,/命令 是纯文本、不在其中 |
| 输入机非空闲(提交/裁决中) | 按钮禁用 |
| 宿主报错 | 直接展示宿主返回的 message(如「草稿 9001 字,超过上限 8000 字」);403 有专门文案;404/405 按「宿主路由未挂载」提示 |
| 宿主半没挂载 | 客户端提示「宿主路由未挂载(插件宿主半未启用?)」——实测这种情况下 POST 拿到的是 405 空体(SPA fallback 先拦非 GET/HEAD),不是 404 |
| 通用角标挂在哪(P13) | 页面上每个够格的 textarea(以及带 data-dsh-better-input-host 的元素)右上角一枚 ✦;只读/禁用/被 data-dsh-better-input-skip 覆盖/本插件自己界面里的不挂。详见「通用输入角标(P13)」 |
| 通用角标点击 | 读写该输入框(原生 setter + input 事件)→ 同一套宿主路由与流式回填 → 成功后多出 ↶ 撤销角标;生成中再点 = 取消 |
| 通用角标的撤销 | 按元素隔离(弱引用,元素被移除即回收);文本被动过时第一次点击只武装,与主按钮同语义 |
版本适配(真机踩坑记录):已安装的 dsh 0.1.2-rc.1 对
conversation.input.left/right调的是renderSlot(name, {}),没有 owner props——所以本插件一律通过框架注入的useInput读输入状态, 不读props.input(新版本源码才把InputZone传给这两个座位)。sessionId来自ui-session的 kit 合并,缺包时回落到全局单栈(有 CAS 兜底)。详见DESIGN.md的 R-3 / R-13。
提示文本用 GUI 的设计令牌上色(--dsw-alias-state-{success,warn,error}-primary),令牌缺失时回落 currentColor。
点击 ✨ 后不再"转圈等一整段":宿主走 SSE 把文本增量推过来,客户端边收边写回草稿, 所以你能看着草稿被逐句替换。要点:
POST /optimize/stream(SSE,默认走它)与 POST /optimize(一次性 JSON,回退)。
两者准入条件完全一致(共用同一段校验:信任判定、字数上限、并发闸门、生效配置),
否则"流式那条更松"就会变成绕过口子。response.body、或网络层失败时,
客户端自动改用一次性 JSON——流式是增强,不该在任何环境里变成新的失败面。draftRev,所以不能再拿它当基线(那会把自己写的东西
判成"用户改过")。改为记住"本次调用里我们写过的每一版文本":当前草稿落在集合之外才算用户手改,
此时立刻中止(宿主侧随之取消上游)并提示「草稿已变化」。error: timeout 事件,
而客户端自己断了就不再往那条 socket 写。点 ✨ 之后,"等模型"那一段原先完全没有反馈:按钮只是把 ✨ 换成一个静止的图标,草稿要等到模型 吐出第一个文本 token 才会动。推理模型的前几秒到几十秒就是这样白白静着的——看起来像卡死。 P11 补上两件事:
按钮旁常驻一行进度,槽位与结果提示共用(生成中显示进度,结束后按需显示失败/截断提示):
等待模型 4.2s ← 已发出请求,还没收到任何事件
思考中 8.7s · 312 字 ← 已收到思考增量(计数 = 已收思考字数)
写入中 12.1s · 156 字 ← 已开始回填草稿(计数 = 已写进草稿的字数)
waiting(已发出请求、还没收到任何事件)→ thinking
(首个思考增量)→ writing(首个文本增量)。等待期没有任何事件,本地时钟是唯一的进度来源,
所以耗时由客户端自己算——服务端心跳帧解决的是"连接是否活着",那条 fetch/abort 已经覆盖,
多一种帧只是多一处会漂移的契约。maxOutputTokens 是上限而不是目标,任何"进度条"都是编的数字。
能诚实给的是阶段、耗时与计数。@keyframes dsh-bi-pulse):静止的图标本身就是"卡死"的观感来源之一。模型返回 reasoning-delta(例如 DeepSeek 推理模型的 reasoning_content)时,按钮上方自动展开面板,
实时跟随最新思考(真实 DOM 上自动滚到底),收尾自动收起,之后从 ▾ 菜单的「💭 查看思考过程(n 字)」
回看(P12 之前那是按钮行里的一个独立图标)。
reasoning
事件——而不是"发到浏览器再藏起来"。思考正文(模型的推理内容)因此不出宿主。write() 只由文本增量与 done 帧触发,宿主装配权威文本时也只取 text 块
(两条都有守卫钉着)。撤销栈同理:只有"优化前 → 优化后"的草稿,没有思考。reasoning 事件会直接忽略(只认 delta/done/error);
新客户端配老宿主则收不到这个事件,只有进度行——思考是增强,不是新的失败面。
回退用的一次性 JSON 路由(/optimize)不带思考内容:那条路本来就没有"流"可言。一次带思考的成功优化之后,输入框工具行里同时挂着 4 个控件(撤销 / 思考 / ✦ / ▾), 再加上进度或提示行与弹出的面板——在一个还要放附件、模型选择器、上下文计量与发送键的行里, 这就是"拥挤"的来源。P12 把工具行收到 2 个:
✨ ▾ ← 静止态就是这两个
✨ ▾• ← • = 有可撤销记录(5px 圆点)
点 ▾ 后(菜单向上弹出、右对齐、超高时内部滚动):
┌ 本次调用 ────────────────────┐
│ ↶ 撤销上次优化 │ ← 有可撤销记录时才出现
│ 💭 查看思考过程(312 字) │ ← 本次调用收到过思考时才出现
├ 追加提示词 ──────────────────┤
│ ○ 不追加(只用系统提示词) │
│ ● 精简 │
│ ○ 转规格 │
├ 预设 ────────────────────────┤
│ 更短 │
└──────────────────────────────┘
四个功能的新路径:
| 功能 | 路径 | 备注 |
|---|---|---|
| 优化输入 | 点 ✦ | 不变;生成中再点 = 取消 |
| 优化选项(追加提示词 / 预设) | 点 ▾ → 对应区点选 | 两个区的内容与语义一字未改,只是排在「本次调用」之后 |
| 撤销上次优化 | 点 ▾ → 「↶ 撤销上次优化」 | 执行成功随即收起菜单;草稿被手改过时菜单项变成「↶ 强制还原原文」且菜单保持展开,再点一次即可 |
| 查看思考过程 | 点 ▾ → 「💭 查看思考过程(n 字)」 | 菜单关、面板开;面板里「收起思考过程」回原状 |
要点:
/catalog 的
profiles/presets——目录读取失败(离线、宿主半缺失)时菜单整个消失的话,一次成功的优化之后就
再也找不到撤销入口,而主按钮看起来一切正常。现在的条件是
hasInvocation || profiles.length > 0 || oneShotPresets.length > 0,有守卫钉着。onUndo() 返回"是否真的执行了还原"。执行了 → 收起菜单;
只是武装了强制还原 → 菜单留着。这是"菜单里做两步确认"能成立的前提。setDraft 打断正在跑的那次流式调用
(CAS 判成"用户手改",结果被丢弃)。现在撤销在菜单里,而菜单在生成中本来就是禁用的。↶ / 💭 直接写在文案里。菜单项本来就以文字为主,
塞 14px 图标既要对齐、又给每个图标多留一条降级路径,而文字前缀在任何图标集下长得一样。data-dsh-better-input-undo / data-dsh-better-input-thinking-toggle
沿用旧名字(外部排查脚本与测试都按它们定位),只是位置从工具行移到了菜单里。主输入框(composer)之外的输入窗口不在任何跨插件契约里:官方与第三方插件各自渲染自己的
自由文本控件,既没有统一座位,也没有共享的草稿 store——客户端的 bundle 也不能 import 别的插件。
所以 P13 换了一条唯一共同可用的契约来覆盖"大部分输入窗口":它们最终都是页面上的 <textarea>。
┌─────────────────────────────────────────┐
│ 这个插件窗口里的多行输入框… ✦ │ ← 角标:点一次 = 优化这段文本
│ │ (生成中 = 取消;成功变绿)
└─────────────────────────────────────────┘
↶ ← 成功后多出的撤销角标
| 项 | 规则 |
|---|---|
| 挂给谁 | 页面上的 textarea(DSH 里"人话输入"的标准控件);另外任何带 data-dsh-better-input-host 的元素都算输入窗口(非 textarea 的第三方控件可自己报名) |
| 不挂给谁 | 只读 / 已禁用 / 尺寸太小(< 80×28)或不在视口内(角标存在但不显示)/ 带 data-dsh-better-input-skip 的元素或其子孙 / 本插件自己的界面(设置页与主按钮工具行) |
| 角标位置 | 宿主右上角(视口坐标),滚动、缩放、DOM 变化后重排;被对话框/抽屉盖住时隐藏(用 elementFromPoint 判宿主中心点的命中元素,取不到就当可见);页面最多 40 个宿主(第三方页面可能有几十个 textarea) |
| 点击 | 读宿主的当前文本 → 走与主按钮相同的宿主路由(/optimize/stream,旧宿主自动回退 /optimize)→ 边生成边写回;生成中再点 = 取消 |
| 写回 | 平台原生 value setter + 冒泡 input 事件。第三方输入窗口几乎都是 React 受控组件,直接赋值会被 value tracker 忽略,组件 state 停在旧文本、下一次渲染把结果覆盖回去(表现为"点了没反应") |
| 撤销 | 成功后宿主旁出现 ↶(按元素隔离,弱引用);文本在优化后被动过时第一次点击只"武装"(换成「再点一次强制还原」),与主按钮同语义 |
| 失败 / 手改 | 与主按钮同文案:空输入 / 超长(本地拦下,不发请求)、流中途失败还原原文、用户手改立刻中止并提示「草稿在优化过程中被修改」 |
| 逃生口 | data-dsh-better-input-skip(一行 HTML 关掉某个输入框;给"挂了反而碍事"的插件用)、data-dsh-better-input-host(接入非 textarea 控件) |
| 会话键 | 通用角标没有会话上下文,按元素给一个稳定合成 id(data-dsh-better-input-session 可显式指定):同一输入窗口单航班,全局并发上限照旧(宿主的 maxConcurrentCalls) |
为什么不做成"每个插件各自集成":那要求每个输入窗口的作者改自己的代码——正是"下载很多插件"这个 前提最不可能发生的事。DOM 层方案不依赖任何插件配合,代价是没有类型契约:所以给了跳过标记与 显式报名两个开关,并把"受控组件写回"这条最容易踩的坑固化成一个约定守卫。
边界(v1 明确不做):contenteditable(Lexical 那类编辑器,含主输入框)不挂角标——整体替换
富文本节点的风险远高于收益;只优化选中片段、把通用角标的优化历史并入会话撤销栈也都不在范围内。
另外两处已知覆盖不到,以及一处会滞后(写在这里,免得被读成"全都能覆盖"):
document.querySelectorAll 不穿透它们,所以扫不到;MutationObserver + 滚动/缩放事件,没有挂 ResizeObserver——
宿主在"没有 DOM 变化、也没有滚动/缩放"的情况下自己改变尺寸时,角标会滞后到下一次事件;为什么不复用主按钮的组件:主按钮的进度行 / 思考面板 / 菜单都是 React 座位组件(依赖
useInput 与 setDraft),而通用角标拿不到任何 React 树位置,也不该往别人的组件树里塞节点。
两者共用的是更该共用的那一层:路由调用、流式分帧、CAS 与撤销语义、词典文案。
P6.1/P6.2 时代的「优化风格」是可多选的改写口味:▾ 菜单里勾选「精简 / 转规格」(复选框、可叠加、
不落盘),逐风格提示词走"设置页 ← 组合配置同 id 预设 ← 内置文案"三层覆盖,请求带 styleIds。
P8 起这套东西并入了「追加提示词」:精简/转规格成为清单里的内置条目(见下节), 同一个清单、同一个 ▾ 菜单、同一个设置页区块,单选。变更点:
styleIds;宿主仍兼容旧版浏览器半(styles 清单照发、styleIds 照收,
追加语义与旧版逐字节一致),两个方向的升级/回退都不破坏。stylePromptConcise/stylePromptSpec(遗留设置字段)← cordis.patch.yml 同 id 预设 ← 内置文案。/catalog 的 styles 字段只为旧客户端存在;新客户端读 profiles(含内置条目,带 builtIn 标记)。系统提示词是基底,追加提示词是接在基底之后的那一条:设置里的系统提示词(见下节)永远在场,
▾ 菜单选中的追加提示词以 \n\n本次额外要求(名称):正文 接在它后面。两者在设置页里是两个区块,
关系一目了然——这也是 P9 把旧名「提示词 / 提示词方案」改成「系统提示词 / 追加提示词」的原因:
旧名里两个词都含"提示词",说不清谁是基底、谁是附加。
cordis.patch.yml 的
systemPrompt → 内置默认。追加提示词不会替换它(P9 之前自定义方案是整体替换,语义与名字不符)。▾ 菜单):列出全部条目(● = 当前启用),
点哪条就把 activeProfileId 写进设置落盘,下一次优化即生效,重启/刷新后仍保持;
写入失败(mutate 不 reject)时选中标记回退并给出错误提示——与设置页保存同一条"写后自查"的规矩。styleIds)→ 预设。
固定顺序保证"同一组选择无论怎么点出来,system prompt 都逐字节相同"。promptProfiles,每项 {id,name?,prompt}),
加上 activeProfileId 记录启用项。设置通道的 path ops 支持对单字段 set 任意 JSON 值,整表一次 set 天然原子;
当前 profile 的 Cordis patch 里看到的就是这两个人类可读的字段。/catalog 的 profiles 行只含 id/名称/来源/是否内置(由 profileRowsOf 生成,守卫钉住);
设置页编辑的正文来自用户自己的设置镜像,与"风格/预设正文不下发"是同一条规矩——
唯一的例外是 defaults.systemPrompt(内置/组合层的默认系统提示词)有意下发,见下。想要"整段换成完全不同的系统提示词":追加语义下做不到按条目替换,但可以直接改「系统提示词」 区块(自定义开关 + 正文,全局生效),或把那段话作为追加条目写进去——追加内容在指令序列里更靠后, 对模型的实际影响通常更大。
设置页「系统提示词」区块底部有「查看默认系统提示词」:展开后显示当前默认的系统提示词全文
(内置文案,或 cordis.patch.yml 里配置的 systemPrompt——即你什么都没自定义时会用的那一段)。
设置页底部有「打开插件配置文件」,一键用编辑器打开本插件的组合层配置 cordis.patch.yml
(就是 package.json 里 dsh.bundle.patch 声明的那个文件)。
lib/index.js 用自己的模块位置推出包根
(link: 安装指向仓库、正式安装指向 profile 的 `node_modules