Wait Minute — 会话延迟消息队列(1.0.2)
这是会话内的延迟发送功能,不是单次自动化任务。消息保存到插件自己的持久化队列,到期后通过 DSH 普通会话发送接口提交原始文本。
不调用 Schedule,不创建自动化任务,不发送 [SCHEDULE REMINDER] 包装。 开发阶段不读取、不迁移、不清理旧版 Schedule 数据。
发送与队列操作
- 在原聊天输入框输入消息。
- 点击发送按钮旁的时钟图标,分别设置小时和分钟,例如
00:05、01:30。 - 点击原发送按钮或在消息编辑器按 Enter。保存成功后清空草稿,消息进入当前会话的延迟队列并显示倒计时。
- 队列中每条消息提供:
- 编辑:修改消息文本,默认保留原发送时间;勾选“重新设置延迟”后,可修改小时/分钟,延迟从本次保存时刻重新计算。保存失败保留编辑草稿。
- 立即发送:不等待倒计时,立即把原文提交到当前会话的普通消息发送流程。成功确认接收后移出延迟队列。若 AI 正忙,遵循 DSH 普通排队发送,不强行停止或插话。
- 取消发送:删除尚未发送的队列消息,不撤回已经提交到会话的消息。
消息到期、编辑、取消和立即发送共享串行操作队列,避免相互竞争产生重复提交。编辑期间若消息已经到期提交,界面保留编辑草稿,但不能把已提交消息改回未发送状态。
队列外观与侧栏状态
- 外观对齐原生对话等待队列:贴合输入框的菜单条、紧凑消息预览与图标操作;单条直接展示,多条默认折叠为数量标题,编辑时展开。
- 空闲会话行显示时钟及延迟消息数量;悬浮详情显示最近计划时间、正在提交或结果待确认的数量。不把会话伪装成 AI 正在执行。
- 原生运行中、待审批及未读完成标识优先保留;这些行的延迟信息仍在悬浮详情中,不覆盖原生状态或 Stop 行为。
- 修改队列后立即读取新状态,其余会话每 5 秒刷新;最后一条消息发送或取消后清除延迟状态。刷新失败保留上次状态。
时间输入
- 一个视觉外框,内部为独立小时/分钟数字段,交互参考 Reka UI,不引入 Vue/Reka 依赖。
- 小时
0–8760,分钟0–59,只接受数字;无效或越界输入不会写入。 - 离开字段后自动补零:
1:5→01:05。 - 鼠标滚轮、
↑/↓调整当前段,到边界停止;左右方向键切换段。 - 支持粘贴短格式时间及全角冒号并自动规范化。
- 时长最短为 1 分钟,
00:00不可发送,不会退化成立即发送。
新会话首条消息
先按 DSH 原流程选择工作区,使新会话输入框可编辑。第一条消息即可直接延迟发送,无需先发送即时消息。
入队后立即发布摘要标题(延迟 · 消息摘要),保存会话创建记录并同步原生 engagement 状态;同时后台发起一次专用的 session-title 模型请求,生成正式标题。该辅助请求只生成标题,不提交普通聊天消息、不执行问题、不伪造轮次,也不阻塞消息入队。优先沿用会话已有请求的模型配置,否则使用当前默认模型;失败或超过 6 秒保留摘要标题。
已有标题不覆盖;生成期间手动改名会优先保留用户标题。该会话不会继续作为“新会话”占位被复用;取消消息、刷新或重连后仍保持已创建状态。
安装
适用于已检查的 DSH 0.2.0-rc.2,不再依赖 Schedule bundle。同时支持两种运行形态,二者插件完全一致,只是配置互相独立:
- 桌面应用:配置名
desktop,目录%USERPROFILE%\.dsh\profiles\desktop。 - 浏览器版(
dsh web):配置名web,目录%USERPROFILE%\.dsh\profiles\web。
插件服务端只用 webServer 与 connection,没有桌面专属依赖;客户端依赖 dsh-client-ui-conversation、dsh-client-ui-renderer、dsh-client-ui-session,三者都由 @deepseek-ai/dsh-web-app 提供。因此装到哪个配置就在哪个形态生效:只装 desktop 时 dsh web 里不会出现延迟功能,反之亦然。
1.0.0:实际运行实现校验
开发目录中的 Host ESM 模块可能被运行时缓存,停用/启用并不保证重新导入修改后的源码,插件页显示的版本号也不代表正在执行的代码。更新请直接重新安装本次 tgz,而不是仅切换开关。
独立队列改用 /api/wait-minute/outbox-v1,不回退到旧接口。响应包含 X-Wait-Minute-Engine: standalone-outbox-v1 和运行版本标识。客户端在任何修改前先做只读校验;旧实现、缺失标识或错误路径将被阻止,不继续创建消息或自动化任务。实际验证必须检查运行接口标识,不能仅查看源码或 bundle 版本。
在 DSH 插件安装入口选择最新 Release 的附件 dsh-wait-minute-<版本>.tgz(当前为 dsh-wait-minute-1.0.2.tgz),或输入项目绝对目录(例如 C:\code\wait-minute)。如果安装工具返回 restart-required,必须完全退出并重新打开 DSH,不能只刷新网页。安装包包含客户端构建产物,无需第三方安装脚本。
启动成功后,GET /.well-known/wait-minute 返回只读的引擎、运行版本和 ready: true,不包含消息、会话、令牌或其他私有信息。该端点在两个存储单元成功打开后才注册,可用于真实启动核验。私有 /api 请求会先被统一身份认证拦截,未经认证的 401 不能证明某条路由存在或后端版本已更新。
通过 dsh 命令安装
dsh plugin --profile <配置名> <参数...> 会把参数原样转发给该配置目录里的 pnpm,因此 add、remove、install、why 都可用。把下面命令里的 desktop 换成 web,就是给浏览器版安装。
# 首次安装或升级到新版本:按标签定位附件
dsh plugin --profile desktop add https://github.com/TZDXF/dsh-wait-minute/releases/download/v1.0.2/dsh-wait-minute-1.0.2.tgz
dsh plugin --profile web add https://github.com/TZDXF/dsh-wait-minute/releases/download/v1.0.2/dsh-wait-minute-1.0.2.tgz
# 重复安装或回退同一版本:先下载附件,再按本地文件安装(可反复执行)
Invoke-WebRequest -Uri https://github.com/TZDXF/dsh-wait-minute/releases/download/v1.0.2/dsh-wait-minute-1.0.2.tgz -OutFile "$env:LOCALAPPDATA\dsh-wait-minute\dsh-wait-minute-1.0.2.tgz"
dsh plugin --profile desktop add "file:$env:LOCALAPPDATA\dsh-wait-minute\dsh-wait-minute-1.0.2.tgz"
# 开发调试:直接从项目目录安装(link)
dsh plugin --profile desktop add C:\code\wait-minute
# 查看当前安装来源与版本,以及卸载
dsh plugin --profile desktop why dsh-wait-minute
dsh plugin --profile desktop remove dsh-wait-minute
安装或更新后必须完全退出并重新打开:桌面应用要完全退出后重开,dsh web 要停掉原进程重新启动。插件页显示的版本号不代表运行时已重新导入新代码。已经装好的配置只需 dsh plugin --profile <配置名> install 核对,正常输出 Already up to date。
地址必须带版本标签,不要写成 releases/latest/download/...。 附件文件名含版本号,latest 只在「文件名里的版本恰好就是当前最新版」时可用:本仓库现在 latest/download/dsh-wait-minute-1.0.2.tgz 返回 200,而 latest/download/dsh-wait-minute-1.0.1.tgz 直接 ERR_PNPM_FETCH_404。一旦发布 v1.0.3,任何写死 latest/...1.0.2.tgz 的命令都会失效;而没有版本号的固定地址又永远命中同一 specifier,add/update 都只回 Already up to date,无法升级。按标签取地址则两者都成立。
已知问题:缓存的附件地址无法被重新解析。 pnpm 11.7.0 在该地址已进入本地 store 缓存时(输出里的
reused 1, downloaded 0)会产出不带integrity的解析结果,随后报ERR_PNPM_MISSING_TARBALL_INTEGRITY;--force、store prune都无效(pnpm store prune保留仍被配置引用的条目)。触发条件是当前配置需要全新解析一个已缓存的地址:全新配置装同一版本、或remove后再add都会失败,而已装好的配置重复执行add/install会先命中Already up to date、不受影响。升级到新版本是全新地址,必然真正下载,因此始终正常。受影响的场景请用上面的本地附件方式。
持久化及失败行为
- 消息位于 Storage kv 独立单元
wait_minute_outbox,会话创建记录位于wait_minute_created_sessions,不出现在自动化任务管理中。 - DSH 的单元/表名称必须匹配
^[a-z][a-z0-9_]*$。插件采用合法的单元名称,并显式等待storage.backend.json注册就绪。 - 关闭浏览器页面不影响已保存消息。DSH 主程序须运行;退出、关机或休眠期间不能准时发送,恢复后处理到期消息。
- 只支持普通文本,不支持附件或斜杠命令。延迟模式下会拦截并提示,而不是悄悄即时发送。
- 持久化成功后才显示创建成功;失败保留原草稿,请先刷新确认再重试。
- “立即发送成功”表示会话确认接收,不表示 AI 已完成执行。
- 发送前先保存发送意图,发送成功保留已发送标记,重复的立即发送请求不会再次提交。
- 崩溃或发送失败导致接收结果不明确时,不自动重发,队列显示错误。请先检查会话记录;手动立即重试会弹出可能重复发送的确认。
- 发送前存储故障时不发送,不做零延迟自动重试,提示修复后手动重试。
- 消息提交与已发送状态写入不是同一个原子事务,极端崩溃窗口不保证 exactly-once;已有不明确结果的消息不会自动重发。
- 禁用本插件会停止独立队列的计时发送;重新启用后恢复。卸载前建议取消未发送消息。
实现与验证
src/outbox.js:独立持久化消息队列、到期计时、编辑/取消/立即发送及失败恢复。src/index.js:通过 DSH Connection 鉴权保护的 JSON API。- created-sessions.js、client-sessions.js:新会话创建状态、即时标题和异步改名保护。
- title-generator.js:专用辅助模型标题请求,不执行聊天问题。
- client-sidebar.js:会话延迟状态、数量及悬浮详情。
- client-core.js、client.js:输入控件、原生等待队列式展示和内联编辑。
scripts/build.mjs:无第三方构建依赖的客户端打包器。
使用 DSH 普通 sessionController.prompt,以 mode: queue 发送 {type: text, text: 原文}。当前运行时缺少公开的 pre-submit/delayed-engagement 接口,因此 composer 拦截和客户端 engagement 桥接按已检查版本固定;不修改 DSH 安装文件,其他版本需重新验证。
node scripts/build.mjs
node --test test/*.test.mjs
126 项自动测试覆盖独立队列不访问 Schedule、原文发送、到期/立即发送竞争去重、编辑时刻、取消、持久化故障、重启恢复、会话隔离、内联编辑、分段时间、原生队列式折叠与行操作、侧栏状态汇总及过期响应保护、即时标题与后台生成/超时/改名保护。测试使用模拟存储、时钟、消息接收接口和 UI 事件,不等同于真实浏览器点击与实际模型执行的完整端到端测试。
发布与自动化
- ci.yml:分支推送与 PR 上构建客户端 bundle 并运行测试(Node 22 与 24)。
- release.yml:推送
v*版本标签时校验标签与package.json版本一致,再构建、测试、打包dsh-wait-minute-<版本>.tgz,上传构建产物并创建 GitHub Release。 - package.yml:手动触发,只打包并上传 tgz,用于打标签前验证安装包。
- Release 附件即安装包;发布说明取自标签对应提交的 RELEASENOTES.md,自动生成的提交列表追加在其后。
标签与 package.json 的 version 必须一致(v1.0.2 ↔ 1.0.2),标签提交还必须位于 master 历史中;否则发布在构建前失败,且不产生 Release。接口回显的运行版本号直接读取 package.json,因此发布时只需修改包版本。完整步骤、版本号规则与失败处理见 .github/RELEASE.md。
许可证
本项目使用 MIT 许可证,完整条款见 LICENSE。
No comments yet. Be the first to write one.