youli42/dsh_BetterInput

dsh_BetterInput

仓库尚未提供可用的公开描述。

未分類

dsh-better-input

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.4Web 启动耗时排查:基线 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.8typecheck(缺 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 scheme d:. 解析而失败; 以 . 开头的相对 specifier 又是相对 profile 目录(在 C: 盘)解析的,跨盘也无解。 所以唯一可靠的方式是「装进 profile 的 node_modules,再用包名引用」。

方式 A:bundle(与已上架的第三方插件一致)

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)。

方式 B:直接 patch insert(不想动 bundles 时)

在第 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,刷新即可。

  1. P0:刷新 http://127.0.0.1:3080,输入框工具行右侧、模型选择器紧左边出现 ✨ 按钮(data-dsh-better-input="better-input",可用 DevTools 搜到)。空输入时按钮为禁用态。
  2. P1:路由挂上后宿主日志会打印 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(设置界面重设计):打开 设置 → 输入优化:

  • 默认视图里一个输入框都没有,只有操作条 + 四个分组标题 + 各自一行摘要 + 追加提示词清单; 同屏应当能看完全部主要设置(不用滚到底部找「保存」——它在最上面)。
  • 点「系统提示词」右侧的「编辑」→ 开关与正文框出现;再点「收起」→ 控件消失。
  • 在收起状态下直接点「保存」(若当前配置不合法)→ 出错的分组会自动展开并显示红字。
  • 「追加提示词」清单常驻:直接点某一行的单选即可切换;点该行「编辑」才出现名称/正文框。
  1. P5.3(预设菜单):cordis.patch.yml 里配了非内置 id 的 presets 时,▾ 菜单下半部分 列出它们;点某一项 → 请求体里带 presetId(可用 DevTools 的 Network 面板确认)。 一个都没配时只有追加提示词区,界面与从前一致。
  2. P5.4(宿主会话):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>"` 带上。
  1. P11(进度与思考过程):在输入框里写一段草稿、点 ✨:
    • 立刻出现「等待模型 0.0s」并每秒在涨(这就是原先完全静默的那一段);
    • 模型开始输出后变成「写入中 n.ns · N 字」,草稿被逐句替换;
    • 用的是推理模型时,中间会经过「思考中 n.ns · N 字」,按钮上方自动展开「思考过程」面板并实时滚动;
    • 到设置页「调用参数」取消勾选「显示思考过程」并保存,再点 ✨:只有进度行,不再有面板 (宿主侧也可用 curl 看 /catalog 的 effective.showReasoning === false 佐证);
    • DevTools → Network 里那条 SSE 请求应当按序出现 event: reasoning(若有思考)与 event: delta, 且任何思考文本都不会出现在草稿或撤销记录里。
  2. P12(控件收敛):同一次带思考的优化之后,
    • 工具行里只有 ✦ 与 ▾ 两个按钮(DevTools 里数一下 [data-dsh-better-input-wrap] 的直接子按钮);
    • 菜单收起时 ▾ 右上角有一个小圆点(= 有可撤销记录);成功不再闪「已替换为优化结果」;
    • 点 ▾:「本次调用」分区在最上面,含「↶ 撤销上次优化」与「💭 查看思考过程(n 字)」; 点撤销 → 草稿回到优化前且菜单收起;点思考 → 菜单收起、面板打开(面板里「收起思考过程」可再收起);
    • 用手改一下草稿再点撤销:菜单项变成「↶ 强制还原原文」且菜单不收起,再点一次才真的还原;
    • 断网(或让宿主半不可用)后刷新,再成功优化一次:▾ 仍然出现,撤销照样可达 (这条钉的是"菜单不该依赖 /catalog")。
  3. P13(通用输入角标):打开任意一个不是主输入框的自由文本输入窗口——例如拿 「ask_user_question」的自由作答框、消息反馈框,或第三方插件的表单多行框:
    • 该输入框右上角出现一枚 ✦ 角标(20px,data-dsh-better-input-badge="better-input"), 只读/禁用/隐藏的输入框没有角标;滚动页面或缩放窗口时角标跟着输入框走;
    • 写点人话、点角标:角标呼吸(生成中再点 = 取消),文本边生成边替换; 成功角标变绿并多出一枚 ↶,点 ↶ 还原优化前的文本;内容在优化后被动过时, 第一次点 ↶ 只提示"再点一次强制还原";
    • 手动改一下正在生成的输入框 → 立刻中止 + 提示「内容在优化过程中被修改」;
    • 有插件的输入框不想被挂角标 → 给那个元素(或任一祖先)加 data-dsh-better-input-skip 属性即可;非 textarea 的控件想接上, 加 data-dsh-better-input-host;
    • 主输入框不会多出第二枚角标(它是 Lexical contenteditable,不是 textarea)。
  4. 检查:
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。

流式回填(P5.6)

点击 ✨ 后不再"转圈等一整段":宿主走 SSE 把文本增量推过来,客户端边收边写回草稿, 所以你能看着草稿被逐句替换。要点:

  • 两条路由并存:POST /optimize/stream(SSE,默认走它)与 POST /optimize(一次性 JSON,回退)。 两者准入条件完全一致(共用同一段校验:信任判定、字数上限、并发闸门、生效配置), 否则"流式那条更松"就会变成绕过口子。
  • 自动回退:旧宿主没有这条路由(404/405)、浏览器拿不到 response.body、或网络层失败时, 客户端自动改用一次性 JSON——流式是增强,不该在任何环境里变成新的失败面。
  • 写入节流 80ms:不按 token 写,避免每个增量都触发一次编辑器整体重写(节流期间先攒着, 收尾时一定写最终文本)。
  • 失败不留半截草稿:流中途 error 帧 / 连接被掐断时,把已经写进去的增量还原成原文, 再提示失败原因(提示里带「已还原原文」)。
  • CAS 换了判据:流式下每次写入都会推进 draftRev,所以不能再拿它当基线(那会把自己写的东西 判成"用户改过")。改为记住"本次调用里我们写过的每一版文本":当前草稿落在集合之外才算用户手改, 此时立刻中止(宿主侧随之取消上游)并提示「草稿已变化」。
  • 取消/超时:生成中再点 = 取消(照旧);超时会给客户端一个 error: timeout 事件, 而客户端自己断了就不再往那条 socket 写。

进度与思考过程(P11)

点 ✨ 之后,"等模型"那一段原先完全没有反馈:按钮只是把 ✨ 换成一个静止的图标,草稿要等到模型 吐出第一个文本 token 才会动。推理模型的前几秒到几十秒就是这样白白静着的——看起来像卡死。 P11 补上两件事:

进度行(任何模型都有)

按钮旁常驻一行进度,槽位与结果提示共用(生成中显示进度,结束后按需显示失败/截断提示):

等待模型 4.2s                        ← 已发出请求,还没收到任何事件
思考中 8.7s · 312 字                  ← 已收到思考增量(计数 = 已收思考字数)
写入中 12.1s · 156 字                 ← 已开始回填草稿(计数 = 已写进草稿的字数)
  • 阶段由客户端推导,宿主不发进度帧:waiting(已发出请求、还没收到任何事件)→ thinking (首个思考增量)→ writing(首个文本增量)。等待期没有任何事件,本地时钟是唯一的进度来源, 所以耗时由客户端自己算——服务端心跳帧解决的是"连接是否活着",那条 fetch/abort 已经覆盖, 多一种帧只是多一处会漂移的契约。
  • 不给百分比是有意的:maxOutputTokens 是上限而不是目标,任何"进度条"都是编的数字。 能诚实给的是阶段、耗时与计数。
  • 计数用字数而不是 token:客户端自己就能算准,对用户也更直观。
  • 按钮图标在生成中带透明度呼吸动效(@keyframes dsh-bi-pulse):静止的图标本身就是"卡死"的观感来源之一。

思考过程面板(模型给才有)

模型返回 reasoning-delta(例如 DeepSeek 推理模型的 reasoning_content)时,按钮上方自动展开面板, 实时跟随最新思考(真实 DOM 上自动滚到底),收尾自动收起,之后从 ▾ 菜单的「💭 查看思考过程(n 字)」 回看(P12 之前那是按钮行里的一个独立图标)。

  • 开关:设置页「调用参数」里的「显示思考过程」,默认开。关掉时宿主根本不发 reasoning 事件——而不是"发到浏览器再藏起来"。思考正文(模型的推理内容)因此不出宿主。
  • 正文绝不进草稿:write() 只由文本增量与 done 帧触发,宿主装配权威文本时也只取 text 块 (两条都有守卫钉着)。撤销栈同理:只有"优化前 → 优化后"的草稿,没有思考。
  • 失败也留着:草稿照旧还原成原文,但思考内容留着可回看——失败原因常常就写在思考里。
  • 上限 20000 字(保留尾部):生成中要跟着最新思考走;被截断时正文开头明确写一句, 面板标题里的累计字数仍如实显示总数(截断了却不说,用户会以为模型就想这么点)。
  • 两个 100ms 节流:思考增量是逐 token 到的,每个都 setState 会让组件每秒重渲染上百次; 面板最多滞后 100ms,而阶段切换与收尾一律立即落地(收尾那次会把节流期间攒下的内容一并落地,不丢字)。
  • 思考面板与 ▾ 预设菜单互斥:两者锚在同一处(按钮上方、右对齐),同时打开会叠在一起。
  • 兼容性:老客户端收到未知的 reasoning 事件会直接忽略(只认 delta/done/error); 新客户端配老宿主则收不到这个事件,只有进度行——思考是增强,不是新的失败面。 回退用的一次性 JSON 路由(/optimize)不带思考内容:那条路本来就没有"流"可言。
  • 为什么不做"思考中"的骨架动画:官方 GUI 的推理行是扫光条,但它背后有完整的推理展示组件; 本插件是第三方条目,只复用设计令牌与既定座位,所以用"真实进度 + 真实思考文本"代替装饰性动画。

控件收敛(P12)

一次带思考的成功优化之后,输入框工具行里同时挂着 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 沿用旧名字(外部排查脚本与测试都按它们定位),只是位置从工具行移到了菜单里。

通用输入角标(P13)

主输入框(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 那类编辑器,含主输入框)不挂角标——整体替换 富文本节点的风险远高于收益;只优化选中片段、把通用角标的优化历史并入会话撤销栈也都不在范围内。

另外两处已知覆盖不到,以及一处会滞后(写在这里,免得被读成"全都能覆盖"):

  • Shadow DOM / iframe 里的输入窗口:document.querySelectorAll 不穿透它们,所以扫不到;
  • 不动尺寸的布局动画:位置重算靠 MutationObserver + 滚动/缩放事件,没有挂 ResizeObserver—— 宿主在"没有 DOM 变化、也没有滚动/缩放"的情况下自己改变尺寸时,角标会滞后到下一次事件;
  • 顺带一个规模上限:一个页面最多挂 40 个角标(超出部分等前面的消失后由下一次扫描补上)。

为什么不复用主按钮的组件:主按钮的进度行 / 思考面板 / 菜单都是 React 座位组件(依赖 useInput 与 setDraft),而通用角标拿不到任何 React 树位置,也不该往别人的组件树里塞节点。 两者共用的是更该共用的那一层:路由调用、流式分帧、CAS 与撤销语义、词典文案。

优化风格与追加提示词的合并(P6.1/P6.2 → P8/P9)

P6.1/P6.2 时代的「优化风格」是可多选的改写口味:▾ 菜单里勾选「精简 / 转规格」(复选框、可叠加、 不落盘),逐风格提示词走"设置页 ← 组合配置同 id 预设 ← 内置文案"三层覆盖,请求带 styleIds。

P8 起这套东西并入了「追加提示词」:精简/转规格成为清单里的内置条目(见下节), 同一个清单、同一个 ▾ 菜单、同一个设置页区块,单选。变更点:

  • 多选叠加取消:精简+转规格不再同时生效——它们是两条独立追加提示词(选其一)。 想要"既精简又条目化",把两段要求写进同一条自定义追加提示词即可。
  • 新客户端不再发 styleIds;宿主仍兼容旧版浏览器半(styles 清单照发、styleIds 照收, 追加语义与旧版逐字节一致),两个方向的升级/回退都不破坏。
  • 逐风格提示词的三层覆盖原样保留,只是现在的身份是"内置条目的默认追加文案": stylePromptConcise/stylePromptSpec(遗留设置字段)← cordis.patch.yml 同 id 预设 ← 内置文案。
  • /catalog 的 styles 字段只为旧客户端存在;新客户端读 profiles(含内置条目,带 builtIn 标记)。

追加提示词(P7 / P8 / P9)

系统提示词是基底,追加提示词是接在基底之后的那一条:设置里的系统提示词(见下节)永远在场, ▾ 菜单选中的追加提示词以 \n\n本次额外要求(名称):正文 接在它后面。两者在设置页里是两个区块, 关系一目了然——这也是 P9 把旧名「提示词 / 提示词方案」改成「系统提示词 / 追加提示词」的原因: 旧名里两个词都含"提示词",说不清谁是基底、谁是附加。

  • 系统提示词(基底):设置页第一个区块。取值链:自定义系统提示词开关 → cordis.patch.yml 的 systemPrompt → 内置默认。追加提示词不会替换它(P9 之前自定义方案是整体替换,语义与名字不符)。
  • 追加提示词清单 = 「不追加」+ 内置条目(精简 / 转规格)+ 你自建的条目:
    • 内置条目常驻清单、不可删除;未自定义时正文 = 该风格的追加要求原文 (老三层:遗留设置字段 ← 组合配置同 id 预设 ← 内置文案),所以"选中精简"与旧版"勾选精简" 拼出的 system prompt 逐字节相同;
    • 在设置页给内置条目填了正文 = 改用你写的追加文案(依旧只追加);清空 = 恢复内置文案 (存进设置段的覆盖条目可以省略 name,显示名由宿主按内置标签补齐);
    • 自建条目:名称 + 正文,可增删;正文就是会接在系统提示词之后的那段话。
  • 设置页(设置 → 输入优化 → 追加提示词):
    • 内置行在前(名称固定、可改内容、可启用、不可删除),用户行在后(名称 + 正文 + 启用 + 删除);
    • 「新增追加提示词」加一行;第一项是「不追加(只用系统提示词)」——选它 = 只发系统提示词;
    • 全空的行保存时自动丢弃;填了一半的行(缺名称或缺正文)会在客户端就拦下;
    • 上限 20 条(正文随设置文档整段读写,不给上限就没有刹车)。
  • 使用时切换(输入框旁 ▾ 菜单):列出全部条目(● = 当前启用), 点哪条就把 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(内置/组合层的默认系统提示词)有意下发,见下。

想要"整段换成完全不同的系统提示词":追加语义下做不到按条目替换,但可以直接改「系统提示词」 区块(自定义开关 + 正文,全局生效),或把那段话作为追加条目写进去——追加内容在指令序列里更靠后, 对模型的实际影响通常更大。

默认系统提示词可见(P7.0)

设置页「系统提示词」区块底部有「查看默认系统提示词」:展开后显示当前默认的系统提示词全文 (内置文案,或 cordis.patch.yml 里配置的 systemPrompt——即你什么都没自定义时会用的那一段)。

  • 为什么它能下发:这是部署默认文案而不是用户私密;「系统提示词在设置里可见可编辑」这条需求 正需要它——否则没启用自定义的用户在设置页只能看到一只空输入框。
  • 可一键编辑:「以默认为基础编辑」把默认正文填进输入框并启用自定义开关 (只填不启用会让人以为生效了,所以两步并作一步),之后随意修改保存即可。
  • 恢复默认配置:设置页底部按钮(原「恢复默认」),点击先弹确认框(会清空全部用户设置,不可逆), 确认后清空所有用户字段——包括追加提示词与内置条目的追加文案覆盖——回到内置默认与组合配置的值。 恢复后系统提示词就是上面看到的那段默认文案,追加提示词回到内置文案。

打开插件配置文件(P6.3)

设置页底部有「打开插件配置文件」,一键用编辑器打开本插件的组合层配置 cordis.patch.yml (就是 package.json 里 dsh.bundle.patch 声明的那个文件)。

  • 路径由宿主解析,不是前端拼的:lib/index.js 用自己的模块位置推出包根 (link: 安装指向仓库、正式安装指向 profile 的 `node_modules