@kanadego/dsh-heartbeat 1.6.4 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -30
- package/client.js +52 -28
- package/cordis.patch.yml +39 -0
- package/dist/{chunk-SJUNS2BE.js → chunk-3QJJRXIR.js} +1 -1
- package/dist/chunk-3QJJRXIR.js.map +1 -0
- package/dist/{chunk-TFMQKETS.js → chunk-ON4MSU6E.js} +16 -1
- package/dist/chunk-ON4MSU6E.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/index.js +257 -118
- package/dist/index.js.map +1 -1
- package/dist/{runtime-YHJT6TXF.js → runtime-W2OWJIHY.js} +2 -2
- package/package.json +4 -4
- package/dist/chunk-SJUNS2BE.js.map +0 -1
- package/dist/chunk-TFMQKETS.js.map +0 -1
- /package/dist/{runtime-YHJT6TXF.js.map → runtime-W2OWJIHY.js.map} +0 -0
package/README.md
CHANGED
|
@@ -24,10 +24,48 @@
|
|
|
24
24
|
|---|---|
|
|
25
25
|
|  |  |
|
|
26
26
|
|
|
27
|
+
## 关键概念
|
|
28
|
+
|
|
29
|
+
文档里反复出现几个词,先在这里一次说清:
|
|
30
|
+
|
|
31
|
+
| 词 | 意思 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| **心跳** | 本插件的核心动作——不用你开口,agent 按固定节律自己「醒一次」:做维护、感知环境、决定要不要说话。醒一次叫**一跳**,默认 20 分钟一跳。 |
|
|
34
|
+
| **宿主** | 运行插件的那个程序,即 DSH 本体。心跳是挂在宿主里的插件——模型通道、会话、工具这些能力都由宿主提供。 |
|
|
35
|
+
| **客户端** | 宿主里负责界面的那一侧(Web 界面或者 DSH desktop)。设置页的心跳卡片属于客户端;卡片显示不出来,通常是客户端插件没加载成功,与心跳本体无关。 |
|
|
36
|
+
| **相** | 一跳内部的一个阶段。主循环共七相——维护 → 采集 → 闲逛 → 闸门 → Digest → 反刍备料 → 投递,最后统一由**留痕**收尾。每相结束都会在审计日志留一条 `phase_done`,所以「这一跳停在哪」可以直接查。 |
|
|
37
|
+
| **引擎室** | 跑心跳内部轮次的那个 agent,和你在会话里对话的 agent 是分开的。它只负责采集、反刍、备料,**不直接跟你说话**;真正开口的是你读的那个会话里的 agent。 |
|
|
38
|
+
| **心跳正身** | 心跳自己专用的一个会话,同时也是它的决策现场——每跳的推理轮次都在这里发生。它在会话列表里和普通会话长得一样,日常不用去动它(CLI 的 `sessions list` 会把它单独标出来)。 |
|
|
39
|
+
| **闸门** | 决定「这一跳能不能开口」的纯代码判定层,依次看:静默时段 → 你是不是在忙 → 你在不在场 → 今天说够没有 → 距上次开口够久没。全不通过就安静,并在日志里写下具体原因。它也是省 token 的关键——**判「别说」时,后面的模型调用根本不发生**。 |
|
|
40
|
+
| **观察** | 投递的反向动作——把绑定的会话里你新说的话收进**画像收件箱**,作为了解你的证据。它发生在采集相:只取当时有活 agent 的绑定会话、每跳最多 10 条、每条只留第一句(≤80 字),插件自己注入的消息会被跳过。 |
|
|
41
|
+
| **反刍** | 投递之前的备料步骤:引擎室从素材池挑出最值得说的几条,各压缩成一句话(不带理由、不排序)。名字取自反刍动物把食物嚼细再咽。 |
|
|
42
|
+
| **投递** | 把备好的素材包送进你某个会话的动作。把心跳绑定到常用会话,agent 就会「到你那个房间里说话」。 |
|
|
43
|
+
|
|
27
44
|
## 核心功能
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
插件的一切都围绕**一次心跳**展开:不用你开口,agent 按固定节律自己醒过来,走完一轮判断,再决定要不要说话。整体链路:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
定时器:默认 20 分钟一跳
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
① 维护 ── 清素材池 / 滚动日志 / 判断该不该合并画像
|
|
53
|
+
② 采集 ── 全屏截图(加密落盘)+ 忙闲判定 + 前台窗口类别
|
|
54
|
+
╰─ 观察:把绑定会话里你新说的话收进画像收件箱
|
|
55
|
+
②′ 闲逛 ── 条件触发:agent 自己按兴趣焦点用 web_search 看新东西
|
|
56
|
+
③ 闸门 ── 纯代码判定:静默窗 / 忙时 / 在场 / 每日上限 / 冷却
|
|
57
|
+
╰─ 判「别说」→ 本跳到此为止,一个 token 都不花
|
|
58
|
+
④ Digest ── 拼出「此刻状态」:画像 + 时间 + 素材池 + 待办
|
|
59
|
+
⑤ 反刍 ── 引擎室从素材池挑 ≤3 条,各压成一句话(备料)
|
|
60
|
+
⑥ 投递 ── 拼成素材包,送进你指定投递的那个会话
|
|
61
|
+
⑦ 留痕 ── 写审计日志 + 账本(真正开口了才计数)
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
你读的那个会话 ── 那里的 agent 按当下处境,决定说不说、说哪条
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **心跳节律**:定时唤醒(默认 20 分钟一跳,卡片可调),按七相循环运行——维护 → 采集 → 闲逛 → 闸门 → Digest → 反刍备料 → 投递,末了由留痕收尾。每跳都有审计留痕,沉默必带原因。
|
|
68
|
+
- **分寸闸门**:静默时段、忙时窗口、每日表达上限、开口冷却、在场联动,全部是代码层的独立判定,不靠模型自觉。默认倾向开口,但你正忙或已到深夜时绝不打扰。
|
|
31
69
|
- **状态栏与时间感知**:心跳把"此刻在干什么"(在闲逛 / 看到你在忙 / 在场待着…)注入日常会话——支持动态系统提示词的模型常驻系统提示词(KV-cache 安全追加),其余模型只在用户发起轮次时带一行。精确时间按 20–30 分钟节流注入,任务进行中绝不中途插话。
|
|
32
70
|
- **视觉感知(可选)**:每跳对全屏截图做一次视觉识别(经 [ModLens](https://github.com/liustack/modlens) 外接多模态模型),投递的素材包会带一句「(他此刻大概在:…)」;识别失败时画面与窗口标题一概不进任何提示词——隐私优先,宁可不说。
|
|
33
71
|
- **兴趣闲逛**:按你定义的兴趣范围轮换挑焦点(同一条 3 天冷却),用 `web_search` 看新东西(每次 3~9 轮搜索),结果由代码登记入素材池——模型只负责搜索筛选,不做任何写盘动作。话题库存见底(≤4 条)时自动触发补货闲逛(每天至多 2 次)。
|
|
@@ -111,19 +149,24 @@ node <插件目录>/dist/cli/index.js <命令>
|
|
|
111
149
|
| 症状 | 原因与处理 |
|
|
112
150
|
|---|---|
|
|
113
151
|
| 完全没有心跳(无任何日志行) | 插件没装入:确认 `node_modules` 里插件存在、启动无 "Failed to load plugins" 横幅;`plugin_init` 缺失 = apply 没跑 |
|
|
114
|
-
| 一直沉默 | 先看 `silent` 行的 reason:夜间静默窗 = 正常;白天 = cap 满 / 冷却未过 / 忙时窗口 |
|
|
152
|
+
| 一直沉默 | 先看 `silent` 行的 reason:夜间静默窗 = 正常;白天 = cap 满 / 冷却未过 / 忙时窗口 / `token-saver`(见下条) |
|
|
153
|
+
| 审计出现 `silent reason=token-saver`、心跳整跳不动 | **节省 token 模式**开着,且你已离开(闲置 ≥30 分钟)或锁屏——这是设计行为,回来自动恢复,不是坏了。不想要就关掉那个开关 |
|
|
154
|
+
| psy 分区一直空、画像只在其他分区增长 | psy 开关**默认关闭**:开着时画像合并才收 psy 条目。在设置卡里打开即可 |
|
|
115
155
|
| 闲逛永远秒结束、`tool_policy` 报 `unknown global tool "web_search"` | 预设没挂上(裸 agent):`preset status` → `preset install`,装好后 `tool_policy` 行应是 `preset=mounted(heartbeat) restrict=ok` |
|
|
116
|
-
| 设置页整块没有心跳卡片(数据照常写) | client 注册 id
|
|
156
|
+
| 设置页整块没有心跳卡片(数据照常写) | client 注册 id 与包名不一致被宿主**静默剔除**:三处必须严格等于包名,改完**重启**。若确认 id 没问题,看下一条 |
|
|
157
|
+
| 卡片整块消失 / 节律配置保存不了(`0.1.7` 宿主) | 旧版插件撞上 0.1.7 的接口换代(设置接口变更 + 客户端服务被删):**升级到 v1.7.0** 并重启;v1.7.0 已按三代接口自动适配 |
|
|
117
158
|
| 卡片分区报 `加载失败: … HTTP 405` | RPC 路由没注册成功:审计里应出现 `rpc_registered route=/api/heartbeat`,没有 = dist 未同步,重装/同步后重启 |
|
|
118
|
-
|
|
|
159
|
+
| 节律配置保存不生效、重启后回默认(`0.1.5` 宿主) | v1.5.x 的已知问题(dsh-settings 包缺失 + 0.1.5 API 变更):升级到 v1.6.0 并重启;`0.1.7` 宿主的同类症状见上面「卡片整块消失」那条 |
|
|
119
160
|
| 投递没带「他此刻大概在:…」、审计有 `screen_vision ok:false` | 视觉识别失败:error 字段带 ModLens 具体报错;常见为 provider 未配置或网络超时。识别失败时画面/标题按隐私设计不进提示词,心跳其余功能不受影响 |
|
|
120
161
|
| 结果显示「心跳异常(decision: whenIdle timeout)」 | 反刍轮超时(240s 内没等到模型回话)。v1.6.0 起不再整跳报错:记 `decision_deferred`、本跳按沉默处理、素材自动留到下一跳。若反复出现,看该行 reason 与 ModLens 画面描述是否诱发跑题 |
|
|
121
162
|
| 每跳 `beat_error: … reading 'length'` | 宿主移除了 `Session.events`——插件版本与宿主不匹配,对照〈版本兼容〉换版本 |
|
|
163
|
+
| 引擎室轮次整轮失败、报 `format v4 message requires a producer-owned source kind` | `0.1.7` 的会话日志(V4)拒绝旧版通用的消息来源标记——同样是版本不匹配:升级到 v1.7.0(改用自有来源标记) |
|
|
122
164
|
| 每轮报 `prompt variable "{{model}}" has no value` | agent 没有模型路由(审计 `model` 字段为 `(none)`)——同样是版本不匹配症状 |
|
|
123
165
|
| 素材投递偶尔不成功、`spoke_failed: non-Chinese output discarded` | 目标会话 agent 收到素材包后回合以工具调用收尾,末行是 `</tool_calls>`(纯英文)被旧判定误拦。v1.5 起已修:跳过工具收尾标签、取最后一个含中文的行;整段无中文才拒。升级后重启即可 |
|
|
124
166
|
| 有 `spoke` 但会话里没出现这句话 | 投递目标当时没活 agent 且拉不起来:查 `deliver_target_*` 系列行;再查绑定 `deliver` 是否为 true |
|
|
125
167
|
| **侧栏某个会话行消失了** | 心跳刚往那个"当时没打开"的会话投递过,投完释放临时 agent 的正常副作用——**刷新页面即回**,内容零损失 |
|
|
126
|
-
| 升级 0.1.5 后旧会话打不开(`unexpected member …` / `header version must be 3`) | 会话格式 v3 迁移问题:前者用 `scripts/repair-v0-members.mjs`(预检→修复,自动备份);后者首次访问会自动迁移,属正常 |
|
|
168
|
+
| 升级 `0.1.5` 后旧会话打不开(`unexpected member …` / `header version must be 3`) | 会话格式 v3 迁移问题:前者用 `scripts/repair-v0-members.mjs`(预检→修复,自动备份);后者首次访问会自动迁移,属正常 |
|
|
169
|
+
| 升级 `0.1.7` 后会话目录里多出 `.v4` 文件 | 会话格式 V4 迁移的**正常**结果:首次访问该会话时转换,原 `.v3` 文件保留不删。注意这是**单向**的,降级回旧宿主可能读不了——升级前备份 `~/.dsh/sessions` |
|
|
127
170
|
| 画像疑似损坏 | `profile verify`(只报不修)→ `profile rebuild --check`(看 diff)→ `profile rebuild`(真重建) |
|
|
128
171
|
|
|
129
172
|
完整事件字典、宿主契约备忘与更多症状见 [DESIGN.md §7](DESIGN.md)。
|
|
@@ -138,37 +181,60 @@ dsh plugin --profile web remove @kanadego/dsh-heartbeat
|
|
|
138
181
|
|
|
139
182
|
## 版本兼容
|
|
140
183
|
|
|
141
|
-
| 插件版本 | 适配的 DSH |
|
|
184
|
+
| 插件版本 | 适配的 DSH | 说明 |
|
|
142
185
|
|---|---|---|
|
|
143
|
-
| **v1.
|
|
144
|
-
|
|
|
145
|
-
| v1.6.
|
|
146
|
-
| v1.
|
|
147
|
-
| v1.
|
|
148
|
-
| v1.
|
|
149
|
-
| v1.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
> ⚠️ **升级宿主到 0.1.5 会触发会话格式 v3 自动迁移**(所有旧会话首次访问时被转换)——动用户数据,升级前先备份 `~/.dsh/sessions`。
|
|
186
|
+
| **v1.7.1**(当前) | ≥ `0.1.2-rc.1` | 修复 `0.1.7` 下「心跳模式」预设丢失:预设改为声明式注册,不再依赖文件系统用户根 |
|
|
187
|
+
| v1.7.0 | ≥ `0.1.2-rc.1`(在 `0.1.7-rc.2` 上验收) | 适配 0.1.7 的接口换代(设置接口 / 客户端服务 / 节律配置自持 / 会话消息源);新增 psy 分区开关与节省 token 模式 |
|
|
188
|
+
| v1.6.0 – v1.6.4 | ≥ `0.1.2-rc.1` | 素材闭环 + 视觉识别;修好 `0.1.5` 下节律配置保存不生效。**建议直接用 v1.6.4**:v1.6.3 的设置卡有渲染 bug,别停在那一版 |
|
|
189
|
+
| v1.5.x | ≥ `0.1.2-rc.1` | 反刍投递改版(说不说的决定权交给目标会话里的 agent);v1.5.1 元数据补全 |
|
|
190
|
+
| v1.2.x – v1.4 | ≥ `0.1.2-rc.1` | M7 状态栏与时间注入;`0.1.5` 下须 ≥ v1.2.1,否则卡片 RPC 405 |
|
|
191
|
+
| v1.1 | ≥ `0.1.2-rc.1` | 心跳预设改为自动安装;表达从"沉默是常态"改为分寸优先 |
|
|
192
|
+
| v1.0 | ≤ `0.1.1-rc.2` | 预设需手动复制 |
|
|
193
|
+
|
|
194
|
+
> ⚠️ **升级宿主会触发会话格式自动迁移,且不可降级**——`0.1.5` 起迁到 v3、`0.1.7` 起迁到 v4(首次访问该会话时转换,原文件保留)。这动的是用户数据,**升级前先备份 `~/.dsh/sessions`**。
|
|
154
195
|
|
|
155
196
|
## 版本更新
|
|
156
197
|
|
|
157
|
-
### v1.
|
|
198
|
+
### v1.7.1 · 2026-09-25(修复「心跳模式」预设丢失)
|
|
199
|
+
|
|
200
|
+
**修掉升级 0.1.7 后心跳预设静默失效的问题**:0.1.7 起宿主不再从文件系统用户目录加载 agent 预设,旧的安装方式失效,症状是审计出现 `preset_install skipped-no-root`、界面报 `Unknown agent preset: heartbeat`、闲逛轮报 `tools.restrict unknown web_search`——预设没挂上,心跳 agent 成了"裸 agent",连 `web_search` 都看不见。
|
|
201
|
+
|
|
202
|
+
改为在插件的安装清单里**声明式**注册预设(与官方 standard 预设同机制),重启即注册,「心跳模式」重新出现在 Agent 预设页。`0.1.5` 及更早的宿主仍走原来的文件安装路径,两边都不断。
|
|
203
|
+
|
|
204
|
+
- 预设内容与原先一致:上下文折叠组 + 唯一模型工具 `web_search`(不抓正文);**无 persona 行**——部署人格与心跳提示词已足够,不能被 "You are a coding agent" 盖掉。
|
|
205
|
+
- 升级后重启 DSH 即可,无需手动操作;本版读取的是内置声明,若 `~/.dsh/.agent-presets` 下那份是你手改过的,它不再生效。
|
|
206
|
+
### v1.7.0 · 2026-09-24
|
|
207
|
+
|
|
208
|
+
- **现已适配 `0.1.7-rc.2`**(实机验收通过)
|
|
209
|
+
- **设置接口**:0.1.7 换掉了插件配置界面的机制(改由宿主根据插件声明的字段自动生成表单),旧写法会失效。心跳现在自动探测宿主是哪一代,从三代接口里挑能用的——你跑哪版 DSH 都不用改配置。
|
|
210
|
+
- **客户端服务**:0.1.7 删除了一个客户端旧服务,插件若仍声明它,整个 Web 界面会加载失败(症状:打开只看到「Failed to load plugins」,聊天区都进不去)。已移除该声明;设置卡其余分区不受影响。
|
|
211
|
+
- **节律配置**:不再借宿主的设置接口读写,卡片编辑器直接写插件自己的配置文件,**点保存立即生效**(改间隔会当场重排下一跳),不用重启 DSH。优先级(低 → 高):出厂默认 < 用户设置(`data/settings/ui.json`)< 宿主 patch 覆盖。
|
|
212
|
+
- **会话消息源**:0.1.7 的会话日志升级到 V4,不再接受旧版通用的消息来源标记——旧写法会让心跳轮次整轮写入失败。心跳已改用自有的来源标记,引擎室轮次恢复正常。
|
|
213
|
+
- **psy 分区开关**:设置卡新增 psy 开关(默认关)。开启后画像合并才收 psy 分区条目;关闭时相关 ADD 一律拒收。值写在用户 policy 层(data/settings/policy.json),即时生效。
|
|
214
|
+
- **节省 token 模式**:设置卡新增开关(默认关)。开启后,你离开(键盘/鼠标闲置 ≥30 分钟)或 Windows 锁屏时整跳暂停——不采维护、不闲逛、不反刍、不投递,一个 token 都不花;回来自动恢复。睡眠无需检测:睡眠中定时器本来不走,唤醒后闲置时长已覆盖睡眠期。探测失败一律放行(绝不因探测故障误暂停)。
|
|
158
215
|
|
|
159
|
-
|
|
216
|
+
<details>
|
|
217
|
+
<summary><b>历史版本更新(v1.6.4 及更早)</b></summary>
|
|
218
|
+
|
|
219
|
+
### v1.6.4 · 2026-09-20
|
|
220
|
+
|
|
221
|
+
> ⚠️ **v1.6.3 有 bug,请升级到 v1.6.4**。
|
|
222
|
+
|
|
223
|
+
**修复:设置卡「闲着模式」开关的渲染语法错误**——该语法错误会让脚本加载的客户端 bundle 无法注册,Web 界面报「Failed to load plugins」而无法进入。v1.6.4 修复此错误,设置卡可正常渲染,功能与 v1.6.3 一致。
|
|
224
|
+
|
|
225
|
+
**v1.6.3 的更新内容(并入本版本)——闲着模式(默认关闭)**:当素材池为空、而用户打开了「闲着模式」开关时,心跳改用本跳画像 digest 的话题切面兜底,仍走原有投递链路主动搭话(每日上限、静默时段等闸门照常生效)。未开启时维持原来的「空池即沉默」。
|
|
160
226
|
|
|
161
227
|
- 新增配置项 `heartbeat.idleMode`(默认 `false`),可在设置卡「节律配置」里切换,保存即时生效。
|
|
162
228
|
|
|
163
|
-
### v1.6.2 · 2026-09-20
|
|
164
|
-
|
|
165
|
-
**补丁:画像合并(consolidation)白名单修复**——画像从出生就空的问题根因在 `buildConsolidationPrompt` 从没把 `profile-schema.json` 的分区白名单传给裁决模型:引擎室凭直觉自创分区(如 `background`/`relation`/`preference`)全被 `partition not in schema` 拒绝,导致增量几乎 100% 被拒、画像长期为空。本次:
|
|
166
|
-
|
|
167
|
-
- 提示词注入完整白名单(partition/topic/subTopic + 各格 temporal 允许集),并明确禁止自创分区、evidence ref 必须写成 `cursors.json#<时间戳>` 而非带 `chat#` 前缀。
|
|
168
|
-
- 扩充 schema 槽位(新增 `acg`/`hardware`/`audio`/`writing`/`life`、`heartbeat`/`dsh`/`zcode`、`interaction`/`boundary`、`background`/`social` 等 topic)。
|
|
169
|
-
- 修复 `profile rebuild` 写明文 profile.json 的问题(改回 DPAPI 加密写)。
|
|
170
|
-
- 把历史被拒但有效的高价值观察回填进画像。
|
|
171
|
-
|
|
229
|
+
### v1.6.2 · 2026-09-20
|
|
230
|
+
|
|
231
|
+
**补丁:画像合并(consolidation)白名单修复**——画像从出生就空的问题根因在 `buildConsolidationPrompt` 从没把 `profile-schema.json` 的分区白名单传给裁决模型:引擎室凭直觉自创分区(如 `background`/`relation`/`preference`)全被 `partition not in schema` 拒绝,导致增量几乎 100% 被拒、画像长期为空。本次:
|
|
232
|
+
|
|
233
|
+
- 提示词注入完整白名单(partition/topic/subTopic + 各格 temporal 允许集),并明确禁止自创分区、evidence ref 必须写成 `cursors.json#<时间戳>` 而非带 `chat#` 前缀。
|
|
234
|
+
- 扩充 schema 槽位(新增 `acg`/`hardware`/`audio`/`writing`/`life`、`heartbeat`/`dsh`/`zcode`、`interaction`/`boundary`、`background`/`social` 等 topic)。
|
|
235
|
+
- 修复 `profile rebuild` 写明文 profile.json 的问题(改回 DPAPI 加密写)。
|
|
236
|
+
- 把历史被拒但有效的高价值观察回填进画像。
|
|
237
|
+
|
|
172
238
|
### v1.6.1 · 2026-09-20
|
|
173
239
|
|
|
174
240
|
**补丁:反刍轮超时降级**——决策轮(反刍)在 240s 预算内没等到模型回话时,不再把整跳记成异常:改为记 `decision_deferred` 审计、best-effort 取消孤儿轮次、本跳按沉默处理,素材自动留到下一跳(与表达轮 `spoke_deferred` 同一语义)。同时在反刍提示词里钉死一条纪律:画面与窗口只用于写「他此刻大概在:」,即使画面出现想查的内容也不发起搜索、不调用工具。
|
|
@@ -221,7 +287,7 @@ dsh plugin --profile web remove @kanadego/dsh-heartbeat
|
|
|
221
287
|
|
|
222
288
|
### v1.2.x · 2026-09-11/12
|
|
223
289
|
|
|
224
|
-
适配 DSH `0.1.5-rc.2`:诊断兼容 `assistant/attempt` 事件形态(C19)、会话格式 v3 迁移适配(C20)、新增旧会话修复工具 `repair-v0-members.mjs`;v1.2.1 修复 0.1.5 下卡片 RPC
|
|
290
|
+
适配 DSH `0.1.5-rc.2`:诊断兼容 `assistant/attempt` 事件形态(C19)、会话格式 v3 迁移适配(C20)、新增旧会话修复工具 `repair-v0-members.mjs`;v1.2.1 修复 0.1.5 下卡片 RPC 整体报 405(自定义通道在严格服务解析下不可用,迁移为 `/api` 精确路由,C21)。
|
|
225
291
|
|
|
226
292
|
### v1.1 · 2026-09-10
|
|
227
293
|
|
|
@@ -237,6 +303,8 @@ dsh plugin --profile web remove @kanadego/dsh-heartbeat
|
|
|
237
303
|
- 前身为 [kohaku-heartbeat](https://github.com/Kanadego/kohaku-heartbeat)——v1 脚本外挂形态,跑通"定时唤醒 + 采集 + 分寸表达"闭环后推倒重来为本插件形态。
|
|
238
304
|
</details>
|
|
239
305
|
|
|
306
|
+
</details>
|
|
307
|
+
|
|
240
308
|
## 灵感与致谢
|
|
241
309
|
|
|
242
310
|
- [tomsteve1102/presence-watch](https://github.com/tomsteve1102/presence-watch) — 讨论起点与闸门思想
|
package/client.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// dsh-heartbeat client half (M6): settings-page card.
|
|
2
2
|
//
|
|
3
3
|
// Contract notes (verified against dsh-vision-router + dsh-client-ui-settings
|
|
4
|
-
// types on 0.1.1-rc.2):
|
|
4
|
+
// types on 0.1.1-rc.2; reworked for 0.1.7-rc.2 in v1.7.0):
|
|
5
5
|
// - the client module is applied as a CLIENT-SIDE cordis plugin; the
|
|
6
6
|
// ModuleLoader factory must return an object with an "apply" method;
|
|
7
7
|
// - the settings page renders entries contributed to the 'settings.section'
|
|
8
|
-
// slot; each entry = {name, id, order, label
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// slot; each entry = {name, id, order, label} + a React component (the
|
|
9
|
+
// slot survives 0.1.7; the per-namespace settingsScope service does not);
|
|
10
|
+
// - the rhythm editor rides the /api RPC channel (host persists to
|
|
11
|
+
// data/settings/ui.json and applies live) — no host settings API needed;
|
|
12
12
|
// - custom host data flows through an exact Fetch route under /api:
|
|
13
13
|
// ctx.get('connection').rpc.call('/api', 'heartbeat', { endpoint, ...payload }).
|
|
14
14
|
window.__ModuleLoader__.load({
|
|
@@ -31,13 +31,9 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
const rowList = { display: "flex", alignItems: "center", gap: 8, padding: "3px 0" };
|
|
32
32
|
|
|
33
33
|
function apply(ctx) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} catch (e) {
|
|
38
|
-
console.warn("[dsh-heartbeat] settingsScope unavailable", e);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
34
|
+
// settingsScope was removed in DSH 0.1.7 — never bound here again.
|
|
35
|
+
// The rhythm editor rides the RPC channel (data/settings/ui.json on
|
|
36
|
+
// the host), so the card works identically on every host generation.
|
|
41
37
|
const getConnection = () => {
|
|
42
38
|
try { return ctx.get("connection"); } catch { return undefined; }
|
|
43
39
|
};
|
|
@@ -334,36 +330,53 @@ window.__ModuleLoader__.load({
|
|
|
334
330
|
}
|
|
335
331
|
|
|
336
332
|
// ── 配置 + 账本 ───────────────────────────────────────────────
|
|
333
|
+
// 节律配置走 RPC + 宿主侧 data/settings/ui.json(v1.7.0):
|
|
334
|
+
// 三代宿主(0.1.1 scope / 0.1.5 installSection / 0.1.7 profile
|
|
335
|
+
// 表单)行为统一,保存即时生效、无需重载。
|
|
337
336
|
function ConfigSection() {
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
const
|
|
337
|
+
const [value, setValue] = React.useState(null);
|
|
338
|
+
const [loadErr, setLoadErr] = React.useState(null);
|
|
339
|
+
React.useEffect(() => {
|
|
340
|
+
rpc("config.get").then(setValue, (e) => setLoadErr(String(e).slice(0, 100)));
|
|
341
|
+
}, []);
|
|
342
|
+
const interval = value && Number(value.intervalMin) > 0 ? Number(value.intervalMin) : 20;
|
|
343
|
+
const cap = value && Number(value.maxDailySend) > 0 ? Number(value.maxDailySend) : 3;
|
|
344
|
+
const timeInject = value && value.timeInjectMin === 0 ? 0 : (value && Number(value.timeInjectMin) > 0 ? Number(value.timeInjectMin) : 25);
|
|
345
|
+
const statusbar = !value || value.statusbar !== false;
|
|
346
|
+
const idleMode = !!value && value.idleMode === true;
|
|
347
|
+
const tokenSaver = !!value && value.tokenSaver === true;
|
|
348
|
+
const psyEnabled = !!value && value.psyEnabled === true;
|
|
345
349
|
const [draftInterval, setDraftInterval] = React.useState(interval);
|
|
346
350
|
const [draftCap, setDraftCap] = React.useState(cap);
|
|
347
351
|
const [draftTimeInject, setDraftTimeInject] = React.useState(timeInject);
|
|
348
352
|
const [draftStatusbar, setDraftStatusbar] = React.useState(statusbar);
|
|
349
353
|
const [draftIdle, setDraftIdle] = React.useState(idleMode);
|
|
354
|
+
const [draftTokenSaver, setDraftTokenSaver] = React.useState(tokenSaver);
|
|
355
|
+
const [draftPsy, setDraftPsy] = React.useState(psyEnabled);
|
|
350
356
|
const [status, setStatus] = React.useState("");
|
|
351
|
-
React.useEffect(() => { setDraftInterval(interval); setDraftCap(cap); setDraftTimeInject(timeInject); setDraftStatusbar(statusbar); setDraftIdle(idleMode); }, [interval, cap, timeInject, statusbar, idleMode]);
|
|
357
|
+
React.useEffect(() => { setDraftInterval(interval); setDraftCap(cap); setDraftTimeInject(timeInject); setDraftStatusbar(statusbar); setDraftIdle(idleMode); setDraftTokenSaver(tokenSaver); setDraftPsy(psyEnabled); }, [interval, cap, timeInject, statusbar, idleMode, tokenSaver, psyEnabled]);
|
|
352
358
|
const save = async () => {
|
|
353
359
|
try {
|
|
354
360
|
const di = Math.max(1, Math.min(1440, Math.floor(Number(draftInterval) || 0)));
|
|
355
361
|
const dc = Math.max(1, Math.min(50, Math.floor(Number(draftCap) || 0)));
|
|
356
362
|
const dt = Math.max(0, Math.min(1440, Math.floor(Number(draftTimeInject) || 0)));
|
|
357
|
-
await
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
+
const v = await rpc("config.set", {
|
|
364
|
+
intervalMin: di,
|
|
365
|
+
maxDailySend: dc,
|
|
366
|
+
timeInjectMin: dt,
|
|
367
|
+
statusbar: !!draftStatusbar,
|
|
368
|
+
idleMode: !!draftIdle,
|
|
369
|
+
tokenSaver: !!draftTokenSaver,
|
|
370
|
+
psyEnabled: !!draftPsy,
|
|
371
|
+
});
|
|
372
|
+
setValue(v);
|
|
373
|
+
setStatus("已保存(即时生效,无需重启)");
|
|
363
374
|
} catch (e) {
|
|
364
375
|
setStatus("保存失败:" + String(e).slice(0, 80));
|
|
365
376
|
}
|
|
366
377
|
};
|
|
378
|
+
if (loadErr && !value) return React.createElement("div", { style: hintStyle }, "加载失败:" + loadErr);
|
|
379
|
+
if (!value) return React.createElement("div", { style: hintStyle }, "加载中…");
|
|
367
380
|
return React.createElement(
|
|
368
381
|
"div",
|
|
369
382
|
null,
|
|
@@ -380,11 +393,19 @@ window.__ModuleLoader__.load({
|
|
|
380
393
|
React.createElement("div", { style: rowStyle },
|
|
381
394
|
React.createElement("span", { style: labelStyle }, "状态栏"),
|
|
382
395
|
React.createElement("button", { style: draftStatusbar ? buttonStyle : buttonGhost, onClick: () => setDraftStatusbar(!draftStatusbar) }, draftStatusbar ? "☑ 开启" : "☐ 关闭"),
|
|
383
|
-
React.createElement("span", { style: hintStyle }, "
|
|
396
|
+
React.createElement("span", { style: hintStyle }, "日常会话中的心跳状态感知(会额外消耗token)")),
|
|
384
397
|
React.createElement("div", { style: rowStyle },
|
|
385
398
|
React.createElement("span", { style: labelStyle }, "闲着模式"),
|
|
386
399
|
React.createElement("button", { style: draftIdle ? buttonStyle : buttonGhost, onClick: () => setDraftIdle(!draftIdle) }, draftIdle ? "☑ 开启" : "☐ 关闭"),
|
|
387
400
|
React.createElement("span", { style: hintStyle }, "素材池空时用画像话题兜底主动搭话(闸门仍生效)")),
|
|
401
|
+
React.createElement("div", { style: rowStyle },
|
|
402
|
+
React.createElement("span", { style: labelStyle }, "节省 token 模式"),
|
|
403
|
+
React.createElement("button", { style: draftTokenSaver ? buttonStyle : buttonGhost, onClick: () => setDraftTokenSaver(!draftTokenSaver) }, draftTokenSaver ? "☑ 开启" : "☐ 关闭"),
|
|
404
|
+
React.createElement("span", { style: hintStyle }, "你离开(闲置 ≥30 分钟)或锁屏时整跳暂停,回来自动恢复")),
|
|
405
|
+
React.createElement("div", { style: rowStyle },
|
|
406
|
+
React.createElement("span", { style: labelStyle }, "psy 分区"),
|
|
407
|
+
React.createElement("button", { style: draftPsy ? buttonStyle : buttonGhost, onClick: () => setDraftPsy(!draftPsy) }, draftPsy ? "☑ 开启" : "☐ 关闭"),
|
|
408
|
+
React.createElement("span", { style: hintStyle }, "允许画像记录 psy 分区(警告,开启后模型会主动猜测用户心理并计入用户画像,关闭时画像内容计入内容较少)")),
|
|
388
409
|
React.createElement("div", { style: rowStyle },
|
|
389
410
|
React.createElement("button", { style: buttonStyle, onClick: () => { void save(); } }, "保存"),
|
|
390
411
|
React.createElement("span", { style: hintStyle }, status || "全部参数保存后即时生效,无需重启")),
|
|
@@ -429,7 +450,10 @@ window.__ModuleLoader__.load({
|
|
|
429
450
|
}
|
|
430
451
|
}
|
|
431
452
|
|
|
432
|
-
|
|
453
|
+
// settingsScope deliberately absent: removed in DSH 0.1.7 (its client
|
|
454
|
+
// service no longer exists; declaring it stalls the whole client bundle
|
|
455
|
+
// on a "waiting for service" and blocks the web UI from booting).
|
|
456
|
+
exports.inject = ["slots", "sessions"];
|
|
433
457
|
exports.apply = apply;
|
|
434
458
|
return module.exports;
|
|
435
459
|
},
|
package/cordis.patch.yml
CHANGED
|
@@ -12,3 +12,42 @@
|
|
|
12
12
|
- insert:
|
|
13
13
|
- id: heartbeat
|
|
14
14
|
name: '@kanadego/dsh-heartbeat'
|
|
15
|
+
|
|
16
|
+
# ── Agent preset heartbeat(0.1.7 声明式预设,v1.7.0)─────────────────
|
|
17
|
+
# 0.1.7 起 agentPresets 名册不再挂载文件系统用户根(~/.dsh/.agent-presets),
|
|
18
|
+
# 预设改为组合里的 @deepseek-ai/dsh-agent-preset 声明行(与官方 preset-standard
|
|
19
|
+
# 同款机制)。内容 = assets/presets/heartbeat/agent.cordis.yml 的移植:
|
|
20
|
+
# compaction 组 + 唯一模型工具 tool-web(fetch:false)。
|
|
21
|
+
# 刻意没有 persona 行:部署人格(soul card)+ 心跳提示词已够,绝不能让
|
|
22
|
+
# "You are a coding agent" 盖掉人格。≤0.1.5 宿主继续走文件系统预设自装。
|
|
23
|
+
- insert:
|
|
24
|
+
- id: preset-heartbeat
|
|
25
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
26
|
+
config:
|
|
27
|
+
id: heartbeat
|
|
28
|
+
name: 心跳模式
|
|
29
|
+
description: 心跳 agent 专用:仅 web_search + 上下文折叠;无 shell、无文件、无子代理。
|
|
30
|
+
order: 20
|
|
31
|
+
plugins:
|
|
32
|
+
- id: compaction
|
|
33
|
+
name: cordis:group
|
|
34
|
+
group: true
|
|
35
|
+
isolate:
|
|
36
|
+
compaction: true
|
|
37
|
+
toolResultPruner: true
|
|
38
|
+
config:
|
|
39
|
+
- id: compaction-basic
|
|
40
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
41
|
+
- id: command-compact
|
|
42
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
43
|
+
- id: tool-result-pruner
|
|
44
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
45
|
+
config:
|
|
46
|
+
thresholdChars: 8192
|
|
47
|
+
headChars: 4096
|
|
48
|
+
tailChars: 1024
|
|
49
|
+
- id: tool-web
|
|
50
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
51
|
+
config:
|
|
52
|
+
fetch: false
|
|
53
|
+
searchTimeoutMs: 60000
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/runtime.ts"],"sourcesContent":["// Runtime singleton shared by all modules after the plugin entry initializes.\n\nimport type { PathGuard } from './path-guard.js';\nimport type { WorkspacePaths } from './paths.js';\nimport type { Policy } from '../config/schema.js';\n\nexport interface HeartbeatRuntime {\n paths: WorkspacePaths;\n guard: PathGuard;\n policy: Policy;\n /** Live UI flags (M7): re-read by the RPC status endpoint for the card. */\n flags: {\n statusbarEnabled(): boolean;\n timeInjectMin(): number;\n idleMode(): boolean;\n /** Token-saver (v1.7.0): pause the whole beat while the user is away. */\n tokenSaver(): boolean;\n };\n}\n\nlet runtime: HeartbeatRuntime | null = null;\n\nexport function setRuntime(r: HeartbeatRuntime): void {\n runtime = r;\n}\n\nexport function getRuntime(): HeartbeatRuntime {\n if (!runtime) throw new Error('heartbeat runtime not initialized');\n return runtime;\n}\n\nexport function resetRuntimeForTest(): void {\n runtime = null;\n}\n"],"mappings":";AAoBA,IAAI,UAAmC;AAEhC,SAAS,WAAW,GAA2B;AACpD,YAAU;AACZ;AAEO,SAAS,aAA+B;AAC7C,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,mCAAmC;AACjE,SAAO;AACT;AAEO,SAAS,sBAA4B;AAC1C,YAAU;AACZ;","names":[]}
|
|
@@ -248,6 +248,20 @@ function loadPolicy(guard, configDir, settingsDir) {
|
|
|
248
248
|
assertPolicy(merged);
|
|
249
249
|
return merged;
|
|
250
250
|
}
|
|
251
|
+
function updateUserPolicy(guard, settingsDir, patch) {
|
|
252
|
+
const userPath = guard.assert(path4.join(settingsDir, USER_POLICY_FILE));
|
|
253
|
+
let user = {};
|
|
254
|
+
try {
|
|
255
|
+
user = JSON.parse(fs4.readFileSync(userPath, "utf8"));
|
|
256
|
+
if (!user || typeof user !== "object" || Array.isArray(user)) user = {};
|
|
257
|
+
} catch {
|
|
258
|
+
user = {};
|
|
259
|
+
}
|
|
260
|
+
const merged = deepMerge(user, patch);
|
|
261
|
+
fs4.mkdirSync(path4.dirname(userPath), { recursive: true });
|
|
262
|
+
fs4.writeFileSync(userPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
263
|
+
return merged;
|
|
264
|
+
}
|
|
251
265
|
|
|
252
266
|
// src/ledger/ledger.ts
|
|
253
267
|
import path5 from "path";
|
|
@@ -983,6 +997,7 @@ export {
|
|
|
983
997
|
pruneAuditFile,
|
|
984
998
|
deepMerge,
|
|
985
999
|
loadPolicy,
|
|
1000
|
+
updateUserPolicy,
|
|
986
1001
|
ledgerFilePath,
|
|
987
1002
|
readLedger,
|
|
988
1003
|
appendEntry,
|
|
@@ -1013,4 +1028,4 @@ export {
|
|
|
1013
1028
|
describeInstall,
|
|
1014
1029
|
presetStatus
|
|
1015
1030
|
};
|
|
1016
|
-
//# sourceMappingURL=chunk-
|
|
1031
|
+
//# sourceMappingURL=chunk-ON4MSU6E.js.map
|