DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

xianrui69 /

xianrui69/dsh-quick-phrases

Verified

DeepSeek Harness client plugin: quick-phrase chip bar above the composer + /-triggered phrase menu

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: master@0f7a6bf1

dsh-quick-phrases

DeepSeek Harness(DSH)客户端插件:输入框上方的快捷短语条 + / 触发短语菜单。

🇬🇧 TL;DR — A DeepSeek Harness client plugin that adds a quick-phrase chip bar above the composer and a /-triggered phrase menu (phrases group pinned to the top). Pure client-side, host-file persistence, no build step.

当前版本 v0.7.5。v0.1.0 曾带着 2 个 UI 瑕疵以"封存状态"开源求助,v0.2.0 两个都已修复(见 修复记录);v0.3.0 新增每条短语独立的 / 菜单开关;v0.3.1 修复"隐藏快捷条后管理入口随之消失";v0.4.0 把「快捷短语」挂进 DSH 设置对话框(settings.section slot);v0.5.0 管理界面改为 Token 面板式浮动窗——标题栏拖拽、放到哪记到哪;v0.6.0 新增每条短语独立的 ▬ 快捷条开关(不上条的短语仍可 /名称+回车 展开);v0.7.0 chips bar 本身可拖拽——整个快捷条变成浮动窗口,拖动到任意位置、位置持久化(学 Token 面板);v0.7.1 修复拖拽冲突 UX —— 管理面板不再是独立浮动窗,而是附加在 chips bar 上方作为一个整体,拖动时两者统一移动,彻底解决 v0.7.0 中两个独立 fixed 窗口互相干扰的问题;v0.7.2 修复 bar 位置跳动 —— 锚定 barPos 到 bar 本身位置,管理面板用 absolute 定位出现在 bar 正上方,打开/关闭时 bar 位置不再跳动;v0.7.3 相对锚点定位 —— barPos 改为相对输入框的偏移量,监听布局变化(ResizeObserver + MutationObserver + 定时器后备),自动跟随对话区移动,解决开关右侧面板时 bar 位置不同步的问题;v0.7.4 修复拖拽与跟随冲突 —— 拖拽期间暂停跟随循环,落盘时写入新偏移量,恢复布局跟随且不抢回拖拽位置;v0.7.5 管理窗支持点击别处关闭 —— 热区按整个浮动单元(面板 + 快捷条)判定,拖条子/点 chip 不误关,设置页浮动窗行为不变。另有一个多窗口并发写的架构缺陷尚未修复(真实踩过坑),见 已知缺陷。


✅ 功能(真实环境验证过)

功能 说明
快捷短语条 v0.7.0 浮动可拖拽:整个条子变成浮动窗口(position: fixed),拖动到任意位置、位置持久化(宿主文件 + localStorage,学 Token 面板),首次加载默认底部居中;点击 chip 把短语填入输入框;每条短语可用 ▬ 开关单独控制是否上条(默认显示);取消勾选「显示快捷条」后仍保留一枚低透明度 ⚙ 常驻入口(悬停变清晰),不会把自己埋掉
➤ 点击即发送 每条短语可独立开启;点击后延迟验证草稿确实写入再提交(防抢跑发出空/旧草稿),chip 上带 ➤ 标记
/ 短语菜单 输入 / 出现「短语」分组,置顶于命令/技能等全部内置组(order: -1),★置顶排最前,按名称/内容模糊过滤;仅显示带 / 标记的短语(每条独立开关,默认不进菜单,保持菜单清爽)
/名称 + 回车 草稿为 /短语名 时,提交自动展开为短语内容(matchEnter 纯文本路径);此显式命令路径对全部短语生效,不受 / 开关限制
浮动管理窗 输入框旁「⚙ 管理」打开浮动窗口(学 Token 面板):拖标题栏放到任意位置,位置持久化(宿主文件 + localStorage,重开还在原处);增删改 / ★置顶 / ➤自动发送 / / 是否进斜杠菜单 / ▬ 是否上快捷条 / ↑↓排序 / 显示开关 / JSON 导入导出 / 恢复默认;关闭方式:✕ / Esc / 点击浮动单元(面板 + 快捷条)以外的任意处
设置页入口 DSH「设置」对话框左侧导航**「快捷短语」(settings.section slot,机制同 Token 面板):点进去自动弹出同一个浮动管理窗**,快捷条隐藏时也永远可管理
宿主文件持久化 短语表存 $DSH_HOME/storages/dsh-quick-phrases/phrases.json,原子写入(tmp+rename),跨重启、跨浏览器可靠;首次运行自动从旧 localStorage 键迁移;宿主不可达时回退 localStorage
安全边界 纯客户端数据,不进会话日志、不影响模型上下文;宿主路由带同源 fence(loopback + Origin/Sec-Fetch-Site 校验)

🔧 v0.7.1 修复:统一浮动单元

v0.7.0 引入的两个独立浮动窗口(chips bar + 管理面板)导致拖拽冲突 —— 两者各有独立的 position: fixed 定位、独立的 pointer capture 和位置存储,拖动一个会干扰另一个。

v0.7.1 修复思路(2026-08-27):

  • 根因:ManageWindow 和 QuickPhrasesBar 各自是独立 fixed 窗口,各有独立拖拽处理器(onHeadPointerDown/onBarPointerDown),当管理面板打开时两组 pointer 事件监听器同时活跃 → pointer capture 冲突 → 拖动行为互相干扰。
  • 修复方案:管理面板不再是独立浮动窗,改为附加在 chips bar 容器内(flexbox 布局,panel 在上、bar 在下),整个容器作为唯一的拖拽单元(qp-bar-container)。拖拽逻辑只应用于容器,panel 和 bar 跟随容器一起移动。
  • 技术细节:
    1. 新增 .qp-bar-container(position: fixed,包含 panel + bar);
    2. 移除 ManageWindow 的独立定位和拖拽逻辑(它现在只是一个普通面板);
    3. 移除 store.panel 位置存储,只保留 store.barPos(容器的唯一位置);
    4. 拖拽时阻止在 panel 内部交互元素(inputs、textarea、buttons 等)上触发,避免误拖;
    5. 设置页的管理窗(StandaloneManageWindow)仍保留独立浮动 + 拖拽(位置存 localStorage 独立键,不写 store),与 chips bar 管理窗是两个不同的入口。
  • 用户体验:点击「⚙ 管理」后,管理面板出现在 bar 正上方,拖动 bar(或 panel 标题栏)移动时两者作为一个整体同步移动,不再出现"拖 A 影响 B"的错位感。关闭管理面板后 bar 保持原位。

🔧 v0.7.2 修复:位置锚定到 bar

v0.7.1 修复了拖拽冲突,但引入新问题:打开/关闭管理面板时 chips bar 位置会跳动。

v0.7.2 修复思路(2026-08-27):

  • 根因:v0.7.1 中容器使用 flexbox column 布局,barPos 锚定在容器左上角。当管理面板打开时,容器从该点向下展开(panel 在上、bar 在下),导致 bar 被向下推;或者 CSS 引擎重新计算容器高度时 bar 位置改变。
  • 修复方案:barPos 锚定到 chips bar 本身的屏幕位置,管理面板使用 position: absolute; bottom: 100% 相对于 bar 定位,让它出现在 bar 正上方。
  • 技术细节:
    1. 容器不再使用 flexbox,直接作为定位容器;
    2. bar 是容器的正常流子元素,容器的 left/top 就是 bar 的左上角位置;
    3. panel 使用 position: absolute; bottom: 100%; left: 0; margin-bottom: 8px 相对于容器定位;
    4. 打开 panel 时,它向上生长(出现在 bar 上方),bar 的屏幕位置完全不变;
    5. 关闭 panel 时,只是移除绝对定位元素,bar 位置同样不变。
  • 用户体验:点击「⚙ 管理」后,管理面板直接出现在 bar 正上方,bar 保持原位不动;关闭时 bar 也不会跳动;拖拽时 bar + panel 作为整体移动。

🔧 v0.7.3 修复:相对锚点定位

v0.7.2 解决了打开管理面板时的跳动,但 bar 仍使用固定视口坐标。当 DSH 布局改变(如开关右侧面板)时,bar 停留在原像素位置,不会跟随对话区移动。

v0.7.3 修复思路(2026-08-27):

  • 根因:v0.7.2 中 barPos 存储的是绝对视口坐标 {x, y}。当用户打开/关闭右侧编辑面板时,对话区和输入框水平移动,但 bar 仍固定在原像素位置(position: fixed; left: x; top: y),导致 bar 脱离对话区。
  • 修复方案:barPos 改为相对于**输入框(composer textarea)**的偏移量 {offsetX, offsetY}。监听锚点元素变化(ResizeObserver + MutationObserver + 定时器后备),自动调整 bar 位置 = 锚点位置 + 偏移量。
  • 技术细节:
    1. 挂载时查找锚点元素:从组件向上爬 DOM,找到 textarea[placeholder](composer);
    2. barPos 格式从 {x, y} 改为 {offsetX, offsetY}(相对于锚点左上角的偏移);
    3. 计算 bar 屏幕位置:anchorRect.left + offsetX, anchorRect.top + offsetY;
    4. 拖拽时:拖拽结束后重新计算并保存偏移量(当前位置 - 锚点位置);
    5. 监听机制:
      • ResizeObserver 监听锚点元素尺寸变化;
      • MutationObserver 监听 document.body 的 class/style 变化(捕获布局切换);
      • 定时器后备(300ms):比较锚点的 rect(left/top/width/height),变化时更新位置。
    6. 首次加载时:如果 barPos 为 null(旧版本或新用户),使用默认位置(底部居中绝对坐标),之后拖拽时转换为偏移量。
  • 用户体验:开关 DSH 右侧面板时,chips bar 自动跟随对话区水平移动,保持相对位置不变;拖拽、打开管理面板等操作仍正常;旧数据平滑迁移(v0.7.2 的绝对坐标被忽略,首次拖拽时转换为偏移量)。

🔧 v0.7.4 修复:拖拽与跟随冲突

v0.7.3 的布局跟随正确,但拖拽失效 —— 跟随循环(ResizeObserver / MutationObserver / 300ms 轮询)在 pointer 拖拽期间仍会把 bar 拉回 锚点 + 旧偏移,导致条子几乎拖不动。

v0.7.4 修复思路(2026-08-27):

  • 根因:updatePosition() 在拖拽过程中仍会执行,用 store 里尚未更新的旧偏移覆盖用户正在拖动的屏幕坐标;pointerup 还读了过期的 pos 闭包。
  • 修复方案:dragRef.current !== null 时跳过所有跟随更新;拖拽落盘用 posRef 读取最新屏幕坐标并写入新 {offsetX, offsetY},随后再恢复跟随。
  • 用户体验:拖拽恢复可用,开关侧栏时仍跟随对话区,偏移量持久化不变。

🔧 v0.7.5 交互:点击别处关闭管理窗

用户实测诉求:打开「⚙ 管理」后,想收掉它只能去瞄右上角那枚 ✕ 或记着按 Esc,点一下对话区就顺手关掉才是浮层的常识。

  • 需求口径(与用户拍板一致):只改快捷条入口的 ManageWindow;设置页「快捷短语」自动弹出的 StandaloneManageWindow 保持原样(✕ / Esc 才关),因为它在设置对话框里,点导航项就把自己关掉反而误事。
  • 热区判定绑整壳,不绑面板本体:v0.7.1 起管理面板和 chips 条同属 .qp-bar-container 一个浮动单元,因此「别处」= 该容器之外。落在容器内的一律不关,于是拖条子改位置、点 chip 填草稿、点 ⚙管理 都不会把面板顺手带走。(同 scm-workbench mega menu 空白热区误关闭的教训:存活判断一旦绑在具体块上,同一壳内的正常操作就会被误判为离开。)
  • 实现要点:
    1. 纯函数 isInsideFloatingUnit(target) —— target.closest(".qp-bar-container") !== null,非元素/无 closest 的目标按外部处理;
    2. ManageWindow 内新增一个 document 上的 pointerdown 捕获监听(与既有 Esc 监听并列,卸载时移除);
    3. 用 pointerdown 而不是 click:拖拽结束时浏览器会在容器上补发一次 click(浮动按钮老坑),绑 click 会变成"拖一下条子面板就没了";
    4. 判定函数经 exports.__isInsideFloatingUnit 暴露,scripts/smoke.mjs 用桩元素直接断言,无需 DOM。
  • 为什么点外面关不会丢数据:面板里每个控件的 onChange 都即时走 store.update → replace(同步写 localStorage + 防抖 POST 宿主文件),面板只是这份 store 的视图,关掉不等于"取消"。
  • 本轮明确不做:⚙管理 再次点击仍是"保持打开"(没做成开关切换);点面板外不会顺带收起快捷条;多窗口并发写缺陷见 已知缺陷。

🛠 修复记录(v0.2.0)

v0.1.0 封存时遗留的两个瑕疵,修复思路与过程如下(截图为修复前实况,留作档案)。

瑕疵 1:chips 行与输入卡左边缘不对齐 —— 已修复 ✅

修复前实况(chips 行比输入卡更靠左,水平偏差 ≈ 75px):

瑕疵1:修复前

  • 失败过的静态方案(v0.1.0 时期,供后人避坑):
    1. 照抄官方 GoalBar 对齐公式 calc(100% - 2×side-clearance - 4×dock-inset) → 按视口宽计算,向左溢出;
    2. width: 100% → 仍偏,说明 dock 槽的包含块与输入卡不同源(存在中间包装层)。
  • v0.2.0 修复思路:不再猜结构,运行时实测镜像。挂载后从快捷条向上爬祖先,找到第一个同时包含 composer textarea 的公共容器;再从 textarea 上溯到该容器顶层子节点(即输入卡本体),getBoundingClientRect() 实测其 left/width,把快捷条的 margin-left/width 掰成完全一致。DOM 结构再怎么包,量出来的总是真实几何。
  • 保鲜机制:挂载后 0/120/500ms 三次校准(等布局沉淀)+ ResizeObserver 盯住容器与 textarea 尺寸 + window resize 兜底——字体加载、侧栏开合、窗口缩放都不会再错位。

瑕疵 2:/ 菜单里「短语」分组沉底 —— 已修复 ✅

修复前实况(默认视图看不到短语组,方向键滚到底才出现):

瑕疵2:修复前

  • 根因:菜单分组按 InputTriggerSource.order 升序排列,内置技能源 order: 2,本插件原注册 order: 3 → 沉底。
  • 修复:采用当初候选方案的第一条,order: 3 → -1,短语组置顶于全部内置组(命令/技能)。短语是本插件的核心入口,理应最先被看到。

⚠️ 已知缺陷(求助 🙏)

多窗口同时打开时,新短语可能被旧窗口"回滚"

  • 现象(真实踩坑,2026-08-26):在窗口 A 添加了一条短语,发完消息后这条短语从列表里消失了;而单窗口场景从不复现。
  • 根因:每个 DSH web 页面实例都持有独立的内存状态副本,而持久化是"整表覆盖、最后写入者胜",没有任何冲突检测:
    1. 任一实例 store.replace()(增删改、置顶、恢复默认……都会触发)都会把自己的全量短语表 POST 到宿主文件并同步写 localStorage;
    2. 在添加短语之前加载的旧窗口,内存里仍是老表——它一旦发生任意一次状态写入,就会把宿主文件连同 localStorage 一起覆盖回旧表,新短语即被无声回滚;
    3. 另有一条竞态支线:hydrateFromHost 是异步的且无条件以宿主文件为准,若 GET 在途期间用户恰好完成了添加(防抖 POST 还没发出),响应回来同样会把新数据冲掉。
  • 复现:开两个 DSH web 窗口 → 窗口 A 添加短语 → 让窗口 B 发生任意一次状态写入(或 A 在 B 首次 hydrate 完成前快速添加)→ A 的新短语消失。
  • 临时规避:编辑短语时只保留一个窗口;编辑完成后刷新其他窗口即可同步到最新表。
  • 候选修复方向(欢迎 PR,按性价比排序):
    1. hydrateFromHost 非破坏化:GET 发出时快照本地状态引用,响应回来时发现本地已变化则跳过覆盖、改为把本地推回宿主;
    2. persistToHost 校验 response.ok——当前只 catch 网络异常,4xx/5xx 会被静默当成成功;
    3. 整表覆盖改按 id 并集合并,或 payload 增加 updatedAt 由宿主做时间戳仲裁;
    4. 跨实例收敛:监听 storage 事件或引入 BroadcastChannel,让所有窗口实时跟随最新表。

安装(本机 profile,离线方式)

# 1. 拷贝包到 profile 的 node_modules
Copy-Item -Recurse <本包目录> "$env:DSH_HOME\profiles\web\node_modules\dsh-quick-phrases"

# 2. 在 "$env:DSH_HOME\profiles\web\package.json" 里注册两处:
#    dependencies 加 "dsh-quick-phrases": "^0.7.5"
#    dsh.profile.bundles 加 "dsh-quick-phrases"

# 3. 重启 DSH web(新插件需要宿主重启才会被发现)

有网络/registry 时也可用官方命令一步完成:dsh plugin --profile web add <本包路径>。

使用

操作 效果
点击 chip 短语内容追加到输入框(空草稿直接填入,否则以空格衔接);开了 ➤ 的短语验证写入后自动发送
输入 / 菜单最顶部出现「短语」分组(仅显示带 / 标记的短语);继续输入可按名称/内容过滤
/继续 + 回车 草稿为该短语名时,提交自动展开为短语内容(对全部短语生效,含未进菜单、未上条的)
点击「⚙ 管理」 打开浮动管理窗(拖标题栏任意移动,位置记住;★置顶 / ➤自动发送 / / 进菜单 / ↑↓排序 / 删除 / JSON 导入导出 / 恢复默认);点面板与快捷条以外的任意处即关闭,也可 ✕ / Esc
打开「设置」 左侧导航点「快捷短语」:自动弹出同一个浮动管理窗,快捷条隐藏时也能改

卸载

从 profile 的 package.json 删除 dsh-quick-phrases(dependencies 与 bundles 两处), 删除 node_modules\dsh-quick-phrases 目录,重启 DSH。短语数据在 $DSH_HOME/storages/dsh-quick-phrases/phrases.json,按需删除。

工作原理

lib/index.js   宿主入口:GET/POST /plugins/dsh-quick-phrases/phrases 路由(同源 fence + 原子写盘)
lib/client.js  客户端 bundle(window.__ModuleLoader__ 格式,手写无需构建):
               ├─ InputTriggerSource(trigger '/', order -1 置顶;candidates 按 slash 标记过滤 + matchEnter 展开发送)
               ├─ conversation.input.dock slot(chips 条 + 管理面板,React 18 + useSyncExternalStore)
               │   └─ v0.7.0 chips bar 浮动可拖拽:fixed 定位 + pointer 事件拖拽 + barPos 位置持久化
               ├─ settings.section slot(设置对话框「快捷短语」导航项 → 自动弹出浮动管理窗)
               └─ 持久化:宿主文件为准,localStorage 缓存/回退,旧键自动迁移

开发

无需构建链——lib/client.js 是手写的最终产物(格式对齐官方 tsdown 产物),改完直接拷贝部署。

node scripts/verify.mjs   # 语法 + 包结构检查
node scripts/smoke.mjs    # 无头逻辑冒烟测试(store / 触发源 / slash 门控 / 持久化迁移 / 浮动单元热区判定,19 项断言)

改动后同步到 profile:

Copy-Item -Recurse -Force -Path ".\*" -Destination "$env:DSH_HOME\profiles\web\node_modules\dsh-quick-phrases\"
# 然后重启 DSH

维护者推送

GitHub token 落盘在 仓库外 的 %USERPROFILE%\.github-token(全局 gitignore 兜底,永远不会被误提交)。 推送时脚本按需注入认证,不写 .git/config:

powershell -File scripts\push.ps1          # 推当前分支到 origin/master
powershell -File scripts\push.ps1 -Branch master

换 token:直接改 %USERPROFILE%\.github-token 那一行即可。

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 0f7a6bf1b806

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout