metame-cli 1.5.2 → 1.5.4
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 +90 -17
- package/index.js +76 -25
- package/package.json +1 -1
- package/scripts/bin/dispatch_to +167 -90
- package/scripts/daemon-admin-commands.js +225 -24
- package/scripts/daemon-agent-commands.js +263 -8
- package/scripts/daemon-bridges.js +395 -6
- package/scripts/daemon-claude-engine.js +749 -582
- package/scripts/daemon-command-router.js +104 -0
- package/scripts/daemon-default.yaml +9 -4
- package/scripts/daemon-engine-runtime.js +33 -2
- package/scripts/daemon-exec-commands.js +8 -5
- package/scripts/daemon-file-browser.js +1 -0
- package/scripts/daemon-remote-dispatch.js +82 -0
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +19 -11
- package/scripts/daemon-session-store.js +26 -8
- package/scripts/daemon-task-scheduler.js +2 -2
- package/scripts/daemon.js +363 -8
- package/scripts/daemon.yaml +356 -0
- package/scripts/distill.js +35 -16
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +131 -0
- package/scripts/docs/maintenance-manual.md +214 -3
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +127 -58
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-agent-manage.js +50 -0
- package/scripts/hooks/intent-engine.js +103 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-hook-config.js +28 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/memory-extract.js +1 -1
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory-write.js +21 -4
- package/scripts/memory.js +55 -17
- package/scripts/publish-public.sh +24 -35
- package/scripts/qmd-client.js +1 -1
- package/scripts/signal-capture.js +14 -0
- package/scripts/team-dispatch.js +176 -0
|
@@ -59,7 +59,7 @@ feishu:
|
|
|
59
59
|
- `/stop`:引擎中性,按 `activeProcesses.killSignal` 停止
|
|
60
60
|
- `/compact`:
|
|
61
61
|
- Claude 会话:正常压缩
|
|
62
|
-
- Codex
|
|
62
|
+
- Codex 会话:返回"暂不支持,请继续同会话"
|
|
63
63
|
- `/engine`:
|
|
64
64
|
- 查询当前默认引擎:`/engine`
|
|
65
65
|
- 切换默认引擎:`/engine claude` 或 `/engine codex`
|
|
@@ -69,7 +69,7 @@ feishu:
|
|
|
69
69
|
- 也支持严格自然语言:`把蒸馏模型改成 5.1mini`
|
|
70
70
|
- `/doctor`:
|
|
71
71
|
- 同时检查 Claude/Codex CLI 可用性
|
|
72
|
-
-
|
|
72
|
+
- 仅在"当前默认引擎对应 CLI 不可用"时判为故障
|
|
73
73
|
- 自定义 provider 下允许任意合法模型名(不再强制 sonnet/opus/haiku)
|
|
74
74
|
|
|
75
75
|
## 5. Agent Soul 身份层
|
|
@@ -88,6 +88,9 @@ feishu:
|
|
|
88
88
|
- 活跃子进程:`~/.metame/active_agent_pids.json`
|
|
89
89
|
- 热重载备份:`~/.metame/.last-good/`(daemon 稳定运行 60s 后自动备份)
|
|
90
90
|
- 崩溃计数:`~/.metame/.crash-count`(连续 2 次快速崩溃触发自动恢复)
|
|
91
|
+
- Dispatch 队列:`~/.metame/dispatch/pending.jsonl`(本地 socket 降级)
|
|
92
|
+
- 远端 Dispatch 队列:`~/.metame/dispatch/remote-pending.jsonl`(跨设备中继)
|
|
93
|
+
- Dispatch 签名密钥:`~/.metame/.dispatch_secret`(自动创建)
|
|
91
94
|
|
|
92
95
|
## 7. 热重载安全机制(三层防护)
|
|
93
96
|
|
|
@@ -126,12 +129,25 @@ feishu:
|
|
|
126
129
|
2. 若仍失败,手动 `/new` 新开会话
|
|
127
130
|
3. 检查 `~/.metame/active_agent_pids.json` 是否残留异常进程
|
|
128
131
|
|
|
132
|
+
### 远端 Dispatch 失败
|
|
133
|
+
|
|
134
|
+
症状:`/dispatch to peer:project` 返回 `feishu bot not connected`
|
|
135
|
+
|
|
136
|
+
处理:
|
|
137
|
+
|
|
138
|
+
1. 确认 `daemon.yaml` 中 `feishu.remote_dispatch.enabled: true` 且 `self`/`chat_id`/`secret` 非空
|
|
139
|
+
2. 确认飞书 bot 已连接:`/doctor` 查看飞书状态
|
|
140
|
+
3. 确认 relay 群已加入 `allowed_chat_ids`(relay 群消息需被 daemon 接收)
|
|
141
|
+
4. 两端的 `secret` 必须完全一致
|
|
142
|
+
|
|
129
143
|
## 9. 双平台/双引擎维护矩阵
|
|
130
144
|
|
|
131
145
|
### 统一维护(改一处即可)
|
|
132
146
|
- agent-layer.js / daemon-agent-tools.js / daemon-agent-commands.js / daemon-user-acl.js
|
|
133
147
|
- ENGINE_MODEL_CONFIG(daemon-engine-runtime.js 集中管理)
|
|
134
148
|
- daemon-runtime-lifecycle.js 的语法检查和备份机制
|
|
149
|
+
- daemon-remote-dispatch.js(纯逻辑,无平台差异)
|
|
150
|
+
- team-dispatch.js(共享解析/hint/enrichment)
|
|
135
151
|
|
|
136
152
|
### 需分别维护(有平台/引擎特殊分支)
|
|
137
153
|
|
|
@@ -145,7 +161,202 @@ feishu:
|
|
|
145
161
|
| daemon.js `spawnReplacementDaemon` | POSIX: `detached: true` / Windows: `detached: false` | 改 spawn 参数时注意平台分支 |
|
|
146
162
|
| NL Mac 控制(command-router) | macOS only,`process.platform === 'darwin'` 守卫 | Windows 天然跳过 |
|
|
147
163
|
|
|
148
|
-
## 10.
|
|
164
|
+
## 10. 团队路由(Team Routing)
|
|
165
|
+
|
|
166
|
+
### 概念
|
|
167
|
+
|
|
168
|
+
一个项目可以有多个 team 成员(数字分身),共享同一个 `cwd`,通过虚拟 chatId 并行工作。
|
|
169
|
+
|
|
170
|
+
### 创建团队成员(向导)
|
|
171
|
+
|
|
172
|
+
在手机端(飞书/Telegram)发送以下任一方式触发创建向导:
|
|
173
|
+
|
|
174
|
+
- 自然语言:`创建团队`、`新建工作组`、`建个团队` 等(`_detectTeamIntent` 识别,位于 `daemon-command-router.js`)
|
|
175
|
+
- 命令:`/agent new team`
|
|
176
|
+
|
|
177
|
+
向导分三步,全部在 `daemon-agent-commands.js` 中实现:
|
|
178
|
+
1. **name**:输入团队名称
|
|
179
|
+
2. **members**:输入成员列表,格式 `名称:icon:颜色`,一行或逗号分隔多个
|
|
180
|
+
3. **cwd**:通过文件浏览器(`daemon-file-browser.js` `team-new` 模式)选择父目录
|
|
181
|
+
|
|
182
|
+
目录确认(`/agent-team-dir` 回调)后:
|
|
183
|
+
- 在 `<父目录>/team/<成员key>/` 下创建工作目录及 CLAUDE.md
|
|
184
|
+
- 自动执行 `git init`(支持 checkpoint)
|
|
185
|
+
- 若父目录对应已有项目,自动写入 `daemon.yaml` 的 `team` 数组;否则提示手动注册
|
|
186
|
+
|
|
187
|
+
中间状态保存在 `pendingTeamFlows` Map(`daemon.js` 中定义)。
|
|
188
|
+
|
|
189
|
+
### 配置
|
|
190
|
+
|
|
191
|
+
在 `~/.metame/daemon.yaml` 的项目下添加 `team` 数组和 `broadcast: true`:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
metame:
|
|
195
|
+
name: 超级总管 Jarvis
|
|
196
|
+
icon: 🤖
|
|
197
|
+
broadcast: true
|
|
198
|
+
team:
|
|
199
|
+
- key: jia
|
|
200
|
+
name: Jarvis · 甲
|
|
201
|
+
icon: 🤖
|
|
202
|
+
color: green
|
|
203
|
+
cwd: ~/AGI/MetaMe
|
|
204
|
+
nicknames:
|
|
205
|
+
- 甲
|
|
206
|
+
auto_dispatch: true
|
|
207
|
+
- key: hunter
|
|
208
|
+
name: 猎手
|
|
209
|
+
icon: 🎯
|
|
210
|
+
peer: windows # ← 远端成员,运行在 Windows 设备上
|
|
211
|
+
nicknames:
|
|
212
|
+
- 猎手
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 路由规则(按优先级)
|
|
216
|
+
|
|
217
|
+
1. **引用回复** → 路由到原 agent + 设置 sticky
|
|
218
|
+
2. **显式昵称**(如"乙 帮我查下")→ 路由到对应成员 + 设置 sticky
|
|
219
|
+
3. **主项目昵称**(如"贾维斯")→ 清除 sticky,路由到主项目
|
|
220
|
+
4. **Sticky**:无昵称时 → 路由到上次显式指定的成员
|
|
221
|
+
5. **Auto-dispatch**:主忙时自动分配给空闲的 `auto_dispatch` 成员
|
|
222
|
+
|
|
223
|
+
**远端成员**:检测到 `member.peer` 时,bridges 自动走 `sendRemoteDispatch()` → relay 群 → 对端 daemon 接收执行,结果通过 relay 群回传。路由优先级不变,只是传输链路不同。
|
|
224
|
+
|
|
225
|
+
### /stop 精准路由
|
|
226
|
+
|
|
227
|
+
- `/stop 乙`:停止指定成员
|
|
228
|
+
- `/stop`:停止 sticky 成员
|
|
229
|
+
- 引用回复 `/stop`:停止对应成员
|
|
230
|
+
|
|
231
|
+
### /msg — 团队直接消息
|
|
232
|
+
|
|
233
|
+
格式:`/msg <agent昵称> <消息内容>`
|
|
234
|
+
|
|
235
|
+
- 例如:`/msg 乙 帮我看看这个文件`
|
|
236
|
+
- 按昵称解析目标(先查 team 成员,再查 projects)
|
|
237
|
+
- 以 `type='message', priority='normal'` 调度
|
|
238
|
+
- 实现文件:`daemon-admin-commands.js` resolveProjectKey 函数
|
|
239
|
+
|
|
240
|
+
### Team Broadcast(团队广播 = 可观察模式)
|
|
241
|
+
|
|
242
|
+
`broadcast: true` 时,team 成员之间通过 `dispatch_to` 互发消息会在群里用卡片广播。
|
|
243
|
+
|
|
244
|
+
**这就是"观察模式"**:开启 broadcast 后,你在群里可以实时看到成员之间互相传递任务的全过程(哪个成员发给了哪个成员、发了什么内容),以飞书卡片形式展示。
|
|
245
|
+
|
|
246
|
+
切换命令:`/broadcast on` / `/broadcast off`(实时生效,写入 daemon.yaml)
|
|
247
|
+
|
|
248
|
+
实现入口:`daemon.js` `_findTeamBroadcastContext()` + `handleDispatchItem()` 的广播分支。
|
|
249
|
+
|
|
250
|
+
### 虚拟 chatId
|
|
251
|
+
|
|
252
|
+
team 成员使用 `_agent_{key}` 格式的虚拟 chatId,与物理群 chatId 隔离。
|
|
253
|
+
|
|
254
|
+
### 卡片标题
|
|
255
|
+
|
|
256
|
+
由 `icon + name` 拼成,如 `🤖 Jarvis · 乙`。
|
|
257
|
+
|
|
258
|
+
## 11. 跨设备 Dispatch(Remote Peer Dispatch)
|
|
259
|
+
|
|
260
|
+
### 概念
|
|
261
|
+
|
|
262
|
+
team 成员可以通过 `peer` 字段标记为"远端成员"——运行在另一台机器上的 daemon。用户在飞书群里对远端成员的操作体验与本地成员完全一致(昵称路由、sticky follow 等),底层通过飞书 relay 群实现跨设备通信。
|
|
263
|
+
|
|
264
|
+
### 配置
|
|
265
|
+
|
|
266
|
+
两台设备的 `daemon.yaml` 需要配置相同的 relay 群和共享密钥,不同的 `self` 标识:
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
# Mac 端
|
|
270
|
+
feishu:
|
|
271
|
+
remote_dispatch:
|
|
272
|
+
enabled: true
|
|
273
|
+
self: mac # 本机标识(唯一)
|
|
274
|
+
chat_id: oc_relay_xxx # 专用中继群(两端相同)
|
|
275
|
+
secret: shared-secret-key # HMAC 签名密钥(两端相同)
|
|
276
|
+
|
|
277
|
+
# Windows 端
|
|
278
|
+
feishu:
|
|
279
|
+
remote_dispatch:
|
|
280
|
+
enabled: true
|
|
281
|
+
self: windows
|
|
282
|
+
chat_id: oc_relay_xxx # 同一个 relay 群
|
|
283
|
+
secret: shared-secret-key # 同一个密钥
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
team 成员添加 `peer` 字段指向远端设备:
|
|
287
|
+
|
|
288
|
+
```yaml
|
|
289
|
+
projects:
|
|
290
|
+
business:
|
|
291
|
+
team:
|
|
292
|
+
- key: writer
|
|
293
|
+
name: 编剧 # 无 peer → 本地执行
|
|
294
|
+
- key: hunter
|
|
295
|
+
name: 猎手
|
|
296
|
+
peer: windows # 远端设备
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 数据流
|
|
300
|
+
|
|
301
|
+
#### 用户消息 → 远端 team member
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
飞书群消息 "猎手 去调研竞品"
|
|
305
|
+
→ bridges.js findTeamMember → { member: { key:'hunter', peer:'windows' } }
|
|
306
|
+
→ _dispatchToTeamMember 检测 member.peer
|
|
307
|
+
→ sendRemoteDispatch → encodePacket + HMAC 签名 → relay 群
|
|
308
|
+
→ Windows daemon bridges 拦截 → decodePacket + verifyPacket + isDuplicate
|
|
309
|
+
→ handleDispatchItem(local) → Claude 执行
|
|
310
|
+
→ _replyFn → encode result → relay 群
|
|
311
|
+
→ Mac daemon 拦截 → decode → sendMarkdown 到用户飞书群
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### Claude session 内跨设备 dispatch
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
Claude 看到 hook 注入:
|
|
318
|
+
- hunter(猎手 [远端:windows]): `dispatch_to --from writer windows:hunter "消息"`
|
|
319
|
+
→ dispatch_to 解析 peer:project 格式
|
|
320
|
+
→ 写 remote-pending.jsonl → daemon heartbeat drain → bot 发 relay 群
|
|
321
|
+
→ 对端 daemon 接收执行
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Packet 协议
|
|
325
|
+
|
|
326
|
+
- 前缀:`[METAME_REMOTE_DISPATCH]`
|
|
327
|
+
- 编码:Base64(JSON)
|
|
328
|
+
- 签名:HMAC-SHA256,payload = packet body 去掉 sig 字段
|
|
329
|
+
- 去重:5 分钟 TTL Map,按 packet.id 去重
|
|
330
|
+
|
|
331
|
+
### 关键模块
|
|
332
|
+
|
|
333
|
+
| 模块 | 职责 |
|
|
334
|
+
|------|------|
|
|
335
|
+
| `daemon-remote-dispatch.js` | 编解码、签名验签、去重、配置解析、`parseRemoteTargetRef` |
|
|
336
|
+
| `daemon.js:sendRemoteDispatch()` | 构造签名 packet → 通过飞书 bot 发 relay 群 |
|
|
337
|
+
| `daemon.js:handleRemoteDispatchMessage()` | 接收端:decode → verify → dedup → 执行或投递结果 |
|
|
338
|
+
| `daemon-bridges.js` | Feishu bridge 拦截 relay 群消息 + `_dispatchToTeamMember` 远端分流 |
|
|
339
|
+
| `daemon-admin-commands.js` | `/dispatch peers` 查看配置 + `/dispatch to peer:project` 手动派发 |
|
|
340
|
+
| `scripts/bin/dispatch_to` | 支持 `peer:project` 格式 → 写 `remote-pending.jsonl` |
|
|
341
|
+
| `team-dispatch.js` | `buildTeamRosterHint()` 为远端成员生成 `peer:key` 格式命令 |
|
|
342
|
+
| `hooks/team-context.js` | intent hook 注入远端 `peer:key` dispatch 命令 |
|
|
343
|
+
|
|
344
|
+
### 管理命令
|
|
345
|
+
|
|
346
|
+
- `/dispatch peers`:查看远端配置(self peer、relay chat、所有远端成员列表)
|
|
347
|
+
- `/dispatch to windows:hunter <任务>`:手动跨设备派发
|
|
348
|
+
- `/dispatch to 猎手 <任务>`:按昵称解析,自动检测 `member.peer` 走远端
|
|
349
|
+
|
|
350
|
+
## 12. 私人配置保护
|
|
351
|
+
|
|
352
|
+
- `daemon.yaml` 是用户私人配置,包含 API keys、chat IDs、个人项目配置
|
|
353
|
+
- **绝不上传**到代码仓库,已加入 `.gitignore`
|
|
354
|
+
- 仓库只追踪 `scripts/daemon-default.yaml`(模板文件)
|
|
355
|
+
- 部署流程(`node index.js`)不会覆盖用户的 `~/.metame/daemon.yaml`
|
|
356
|
+
- 同样不应上传的文件:`MEMORY.md`、`SOUL.md`、`.env*`
|
|
357
|
+
- Agent 在执行任务时,**绝不能** `cp scripts/daemon.yaml ~/.metame/daemon.yaml`,这会覆盖用户私人配置
|
|
358
|
+
|
|
359
|
+
## 13. 变更后维护动作
|
|
149
360
|
|
|
150
361
|
1. `npm test`
|
|
151
362
|
2. `npm run sync:plugin`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MetaMe 脚本/文档指针地图
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 目的:回答"这段能力在哪个文件""当前升级做到哪一步""先看哪个脚本"。
|
|
4
4
|
|
|
5
5
|
## 快速入口
|
|
6
6
|
|
|
@@ -41,22 +41,71 @@
|
|
|
41
41
|
- Agent 命令处理(新):
|
|
42
42
|
- `scripts/daemon-agent-commands.js`
|
|
43
43
|
- 关键点:`createAgentCommandHandler()` 处理 `/agent`、`/activate`、`/resume`;
|
|
44
|
-
`/agent soul [repair|edit]`;`pendingActivations` 无 TTL
|
|
44
|
+
`/agent soul [repair|edit]`;`pendingActivations` 无 TTL(消费即删);防止创建群自激活;
|
|
45
|
+
`/agent new team` 三步向导(name → members → cwd);
|
|
46
|
+
`/agent-team-dir` 回调处理目录选择并最终写入 daemon.yaml `team` 段;
|
|
47
|
+
`pendingTeamFlows` Map 维护向导中间状态
|
|
45
48
|
|
|
46
49
|
- 路由与 Agent 创建:
|
|
47
50
|
- `scripts/daemon-command-router.js`
|
|
48
51
|
- `scripts/daemon-agent-tools.js`
|
|
49
52
|
- 关键点:自然语言提取 `codex` 关键词;默认 `claude` 不写 `engine` 字段,仅 `codex` 持久化 `engine: codex`;
|
|
50
|
-
`bindAgentToChat()` 自动调用 `ensureAgentMetadata()` 建立 soul
|
|
53
|
+
`bindAgentToChat()` 自动调用 `ensureAgentMetadata()` 建立 soul 层;
|
|
54
|
+
`_detectTeamIntent()` 自然语言意图识别(含负样本过滤),识别"建团队"意图后自动路由到 `/agent new team` 向导
|
|
51
55
|
|
|
52
56
|
- 会话命令与兼容边界:
|
|
53
57
|
- `scripts/daemon-exec-commands.js`
|
|
54
|
-
- 关键点:`/stop` 引擎中性;`/compact` 在 codex
|
|
58
|
+
- 关键点:`/stop` 引擎中性;`/compact` 在 codex 会话返回"暂不支持"
|
|
55
59
|
|
|
56
60
|
- 运行时引擎切换与诊断:
|
|
57
61
|
- `scripts/daemon-admin-commands.js`
|
|
58
62
|
- 关键点:`/engine` 切换默认引擎;`/doctor` 按默认引擎检查 CLI 可用性(Claude/Codex)并兼容自定义 provider 模型名
|
|
59
63
|
|
|
64
|
+
## 团队 Dispatch 与跨设备通信定位
|
|
65
|
+
|
|
66
|
+
- 共享 Dispatch 工具:
|
|
67
|
+
- `scripts/team-dispatch.js`
|
|
68
|
+
- 关键点:`resolveProjectKey()` 名称/昵称解析(含 team member `parent/member` 复合键);
|
|
69
|
+
`findTeamMember()` 文本前缀匹配团队成员昵称;
|
|
70
|
+
`buildTeamRosterHint()` 生成团队上下文块(远端成员自动带 `peer:key` 前缀);
|
|
71
|
+
`buildEnrichedPrompt()` 注入共享上下文(now.md + _latest.md + inbox)
|
|
72
|
+
|
|
73
|
+
- 远端 Dispatch 协议:
|
|
74
|
+
- `scripts/daemon-remote-dispatch.js`
|
|
75
|
+
- 关键点:`normalizeRemoteDispatchConfig()` 解析 `feishu.remote_dispatch` 配置;
|
|
76
|
+
`parseRemoteTargetRef()` 解析 `peer:project` 格式;
|
|
77
|
+
`encodePacket()`/`decodePacket()` Base64 + HMAC-SHA256 编解码;
|
|
78
|
+
`verifyPacket()` 签名验证;
|
|
79
|
+
`isDuplicate()` 5 分钟 TTL 去重;
|
|
80
|
+
`isRemoteMember()` 检测 `member.peer` 字段
|
|
81
|
+
|
|
82
|
+
- Daemon 远端 Dispatch 入口:
|
|
83
|
+
- `scripts/daemon.js`
|
|
84
|
+
- 关键点:`sendRemoteDispatch()` 构造签名 packet → 飞书 bot 发 relay 群;
|
|
85
|
+
`handleRemoteDispatchMessage()` 接收端逻辑(decode → verify → dedup → 按 type 路由);
|
|
86
|
+
`remote-pending.jsonl` drain(heartbeat 中处理 dispatch_to CLI 写入的远端队列)
|
|
87
|
+
|
|
88
|
+
- Bridge 集成:
|
|
89
|
+
- `scripts/daemon-bridges.js`
|
|
90
|
+
- 关键点:Feishu bridge `startReceiving` 回调最前面拦截 relay 群消息 → `handleRemoteDispatchMessage`;
|
|
91
|
+
`_dispatchToTeamMember` 检测 `isRemoteMember(member)` → 走 `sendRemoteDispatch` 而非本地 handleCommand
|
|
92
|
+
|
|
93
|
+
- Dispatch CLI:
|
|
94
|
+
- `scripts/bin/dispatch_to`
|
|
95
|
+
- 关键点:支持 `peer:project` 格式 → `sendRemoteViaRelay()`;
|
|
96
|
+
`--team` broadcast 自动分流远端成员写 `remote-pending.jsonl`;
|
|
97
|
+
本地走 Unix socket / `pending.jsonl` 降级
|
|
98
|
+
|
|
99
|
+
- 管理命令:
|
|
100
|
+
- `scripts/daemon-admin-commands.js`
|
|
101
|
+
- 关键点:`/dispatch peers` 查看远端配置;
|
|
102
|
+
`/dispatch to peer:project` 手动远端派发;
|
|
103
|
+
按昵称解析到远端 member 时自动走 `sendRemoteDispatch`
|
|
104
|
+
|
|
105
|
+
- Intent Hook:
|
|
106
|
+
- `scripts/hooks/team-context.js`
|
|
107
|
+
- 关键点:检测通信意图 → 注入 dispatch_to 命令提示;远端成员自动带 `peer:key` 前缀
|
|
108
|
+
|
|
60
109
|
## Mentor Mode(Step 1-4)定位
|
|
61
110
|
|
|
62
111
|
- Step 1 数据基建:
|
|
@@ -90,6 +139,11 @@
|
|
|
90
139
|
- 夜间反思文档:`~/.metame/memory/decisions/`、`~/.metame/memory/lessons/`
|
|
91
140
|
- 知识胶囊:`~/.metame/memory/capsules/`
|
|
92
141
|
- 复盘文档:`~/.metame/memory/postmortems/`
|
|
142
|
+
- Dispatch 队列:`~/.metame/dispatch/pending.jsonl`(本地 socket 降级)
|
|
143
|
+
- 远端 Dispatch 队列:`~/.metame/dispatch/remote-pending.jsonl`(跨设备中继)
|
|
144
|
+
- 共享进度白板:`~/.metame/memory/now/shared.md`
|
|
145
|
+
- Agent 最新产出:`~/.metame/memory/agents/{key}_latest.md`
|
|
146
|
+
- Agent 收件箱:`~/.metame/memory/inbox/{key}/`(未读),`read/`(已归档)
|
|
93
147
|
- **Agent Soul 层**:`~/.metame/agents/<agent_id>/`
|
|
94
148
|
- `agent.yaml` — id / name / engine / aliases
|
|
95
149
|
- `soul.md` — 身份定义(主文件,项目目录的 SOUL.md 是其链接)
|
|
@@ -102,7 +156,8 @@
|
|
|
102
156
|
1. 先看配置:`~/.metame/daemon.yaml` 与 `scripts/daemon-default.yaml`
|
|
103
157
|
2. 再看命令入口:`scripts/daemon-admin-commands.js`、`scripts/daemon-command-router.js`、`scripts/daemon-exec-commands.js`
|
|
104
158
|
3. 再看执行链路:`scripts/daemon-engine-runtime.js` → `scripts/daemon-claude-engine.js` → `scripts/mentor-engine.js`
|
|
105
|
-
4.
|
|
159
|
+
4. 团队/跨设备:`scripts/team-dispatch.js` → `scripts/daemon-remote-dispatch.js` → `scripts/daemon-bridges.js`
|
|
160
|
+
5. 最后看离线任务:`scripts/distill.js`、`scripts/memory-extract.js`、`scripts/memory-nightly-reflect.js`
|
|
106
161
|
|
|
107
162
|
## 同步提示
|
|
108
163
|
|
|
@@ -97,11 +97,14 @@ function createBot(config) {
|
|
|
97
97
|
const res = await withTimeout(client.im.message.create({
|
|
98
98
|
params: { receive_id_type: 'chat_id' },
|
|
99
99
|
data: { receive_id: chatId, msg_type: 'interactive', content: JSON.stringify(card) },
|
|
100
|
-
}));
|
|
100
|
+
}), 30000); // 30s: large card content can be slow; timeout must not fire after delivery
|
|
101
101
|
const msgId = res?.data?.message_id;
|
|
102
102
|
return msgId ? { message_id: msgId } : null;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
let _editBroken = false; // closure var — safe against destructured calls
|
|
106
|
+
let _editBrokenAt = 0; // timestamp when broken; auto-resets after 10min
|
|
107
|
+
|
|
105
108
|
return {
|
|
106
109
|
/**
|
|
107
110
|
* Send a plain text message
|
|
@@ -120,22 +123,26 @@ function createBot(config) {
|
|
|
120
123
|
return msgId ? { message_id: msgId } : null;
|
|
121
124
|
},
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
+
async editMessage(chatId, messageId, text, header = null) {
|
|
127
|
+
if (_editBroken && Date.now() - _editBrokenAt < 10 * 60 * 1000) return false;
|
|
128
|
+
if (_editBroken) _editBroken = false; // auto-reset after 10min
|
|
126
129
|
try {
|
|
127
130
|
// Feishu patch API only works on card (interactive) messages
|
|
128
|
-
// Update card content with markdown element
|
|
129
|
-
const card = { schema: '2.0', body: { elements: [{ tag: 'markdown', content: text }] } };
|
|
131
|
+
// Update card content with markdown element; preserve header if provided
|
|
132
|
+
const card = { schema: '2.0', body: { elements: [{ tag: 'markdown', content: text, text_size: 'x-large' }] } };
|
|
133
|
+
if (header && header.title) {
|
|
134
|
+
card.header = { title: { tag: 'plain_text', content: header.title }, template: header.color || 'blue' };
|
|
135
|
+
}
|
|
130
136
|
await withTimeout(client.im.message.patch({
|
|
131
137
|
path: { message_id: messageId },
|
|
132
138
|
data: { content: JSON.stringify(card) },
|
|
133
|
-
}));
|
|
139
|
+
}), 30000); // 30s: must not timeout after Feishu has applied the patch
|
|
134
140
|
return true;
|
|
135
141
|
} catch (e) {
|
|
136
142
|
const code = e?.code || e?.response?.data?.code;
|
|
137
143
|
if (code === 230001 || code === 230002 || /permission|forbidden/i.test(String(e))) {
|
|
138
|
-
|
|
144
|
+
_editBroken = true;
|
|
145
|
+
_editBrokenAt = Date.now();
|
|
139
146
|
}
|
|
140
147
|
return false;
|
|
141
148
|
}
|
|
@@ -316,7 +323,7 @@ function createBot(config) {
|
|
|
316
323
|
},
|
|
317
324
|
});
|
|
318
325
|
|
|
319
|
-
|
|
326
|
+
// Upload response logged at debug level if needed
|
|
320
327
|
|
|
321
328
|
// Response is { code, msg, data: { file_key } }
|
|
322
329
|
const fileKey = uploadRes?.data?.file_key || uploadRes?.file_key;
|
|
@@ -357,42 +364,50 @@ function createBot(config) {
|
|
|
357
364
|
},
|
|
358
365
|
|
|
359
366
|
/**
|
|
360
|
-
* Start WebSocket long connection to receive messages
|
|
367
|
+
* Start WebSocket long connection to receive messages (with auto-reconnect)
|
|
361
368
|
* @param {function} onMessage - callback(chatId, text, event)
|
|
362
|
-
* @
|
|
369
|
+
* @param {object} [opts]
|
|
370
|
+
* @param {function} [opts.log] - logger function(level, msg)
|
|
371
|
+
* @returns {Promise<{stop: function, reconnect: function, isAlive: function}>}
|
|
363
372
|
*/
|
|
364
|
-
startReceiving(onMessage) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
373
|
+
startReceiving(onMessage, opts = {}) {
|
|
374
|
+
const _log = opts.log || ((lvl, msg) => console.log(`[feishu] [${lvl}] ${msg}`));
|
|
375
|
+
let stopped = false;
|
|
376
|
+
let currentWs = null;
|
|
377
|
+
let healthTimer = null;
|
|
378
|
+
let reconnectTimer = null;
|
|
379
|
+
let reconnectDelay = 5000; // start 5s, doubles up to 60s
|
|
380
|
+
const MAX_RECONNECT_DELAY = 60000;
|
|
381
|
+
const HEALTH_CHECK_INTERVAL = 90000; // check every 90s
|
|
382
|
+
const SILENT_THRESHOLD = 300000; // 5 min no SDK activity → suspect dead
|
|
383
|
+
|
|
384
|
+
// Track last SDK activity (any event received = alive)
|
|
385
|
+
let _lastActivityAt = Date.now();
|
|
386
|
+
function touchActivity() { _lastActivityAt = Date.now(); }
|
|
387
|
+
|
|
388
|
+
// Dedup: track recent message_ids (Feishu may redeliver on slow ack)
|
|
389
|
+
const _seenMsgIds = new Map(); // message_id → timestamp
|
|
390
|
+
const DEDUP_TTL = 60000; // 60s window
|
|
391
|
+
function isDuplicate(msgId) {
|
|
392
|
+
if (!msgId) return false;
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
if (_seenMsgIds.size > 200) {
|
|
395
|
+
for (const [k, t] of _seenMsgIds) {
|
|
396
|
+
if (now - t > DEDUP_TTL) _seenMsgIds.delete(k);
|
|
383
397
|
}
|
|
384
|
-
if (_seenMsgIds.has(msgId)) return true;
|
|
385
|
-
_seenMsgIds.set(msgId, now);
|
|
386
|
-
return false;
|
|
387
398
|
}
|
|
399
|
+
if (_seenMsgIds.has(msgId)) return true;
|
|
400
|
+
_seenMsgIds.set(msgId, now);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
388
403
|
|
|
389
|
-
|
|
404
|
+
function buildDispatcher() {
|
|
405
|
+
return new Lark.EventDispatcher({}).register({
|
|
390
406
|
'im.message.receive_v1': async (data) => {
|
|
407
|
+
touchActivity();
|
|
391
408
|
try {
|
|
392
409
|
const msg = data.message;
|
|
393
410
|
if (!msg) return;
|
|
394
|
-
|
|
395
|
-
// Dedup by message_id
|
|
396
411
|
if (isDuplicate(msg.message_id)) return;
|
|
397
412
|
|
|
398
413
|
const chatId = msg.chat_id;
|
|
@@ -408,35 +423,30 @@ function createBot(config) {
|
|
|
408
423
|
text = msg.content || '';
|
|
409
424
|
}
|
|
410
425
|
} else if (msg.message_type === 'file' || msg.message_type === 'image' || msg.message_type === 'media') {
|
|
411
|
-
// File, image or media (video) message
|
|
412
426
|
try {
|
|
413
427
|
const content = JSON.parse(msg.content);
|
|
414
428
|
fileInfo = {
|
|
415
429
|
messageId: msg.message_id,
|
|
416
430
|
fileKey: content.file_key || content.image_key,
|
|
417
431
|
fileName: content.file_name || (content.image_key ? `image_${Date.now()}.png` : `file_${Date.now()}`),
|
|
418
|
-
msgType: msg.message_type,
|
|
432
|
+
msgType: msg.message_type,
|
|
419
433
|
};
|
|
420
434
|
} catch {}
|
|
421
435
|
}
|
|
422
436
|
|
|
423
|
-
// Strip @mention prefix if present
|
|
424
437
|
text = text.replace(/@_user_\d+\s*/g, '').trim();
|
|
425
438
|
|
|
426
439
|
if (text || fileInfo) {
|
|
427
|
-
// Fire-and-forget: don't block the event loop (SDK needs fast ack)
|
|
428
440
|
Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo, senderId)).catch((err) => {
|
|
429
441
|
try { console.error(`[feishu-adapter] onMessage error: ${err && err.message || err}`); } catch { }
|
|
430
442
|
});
|
|
431
443
|
}
|
|
432
|
-
} catch (e) {
|
|
433
|
-
// Non-fatal
|
|
434
|
-
}
|
|
444
|
+
} catch (e) { /* Non-fatal */ }
|
|
435
445
|
},
|
|
436
446
|
'card.action.trigger': async (data) => {
|
|
447
|
+
touchActivity();
|
|
437
448
|
try {
|
|
438
449
|
const action = data.action;
|
|
439
|
-
// Try multiple possible chatId fields
|
|
440
450
|
const chatId = data.open_chat_id || data.chat_id
|
|
441
451
|
|| (data.context && data.context.open_chat_id)
|
|
442
452
|
|| (data.event && data.event.open_chat_id);
|
|
@@ -449,27 +459,86 @@ function createBot(config) {
|
|
|
449
459
|
const cmd = action.value && action.value.cmd;
|
|
450
460
|
if (cmd) {
|
|
451
461
|
Promise.resolve().then(() => onMessage(chatId, cmd, data, null, senderId)).catch((err) => {
|
|
452
|
-
|
|
453
|
-
|
|
462
|
+
try { console.error(`[feishu-adapter] card action error: ${err && err.message || err}`); } catch { }
|
|
463
|
+
});
|
|
454
464
|
}
|
|
455
465
|
}
|
|
456
|
-
} catch (e) {
|
|
457
|
-
// Non-fatal
|
|
458
|
-
}
|
|
466
|
+
} catch (e) { /* Non-fatal */ }
|
|
459
467
|
return {};
|
|
460
468
|
},
|
|
461
469
|
});
|
|
470
|
+
}
|
|
462
471
|
|
|
463
|
-
|
|
472
|
+
function connect() {
|
|
473
|
+
if (stopped) return;
|
|
474
|
+
try {
|
|
475
|
+
currentWs = new Lark.WSClient({
|
|
476
|
+
appId: app_id,
|
|
477
|
+
appSecret: app_secret,
|
|
478
|
+
loggerLevel: Lark.LoggerLevel.info,
|
|
479
|
+
});
|
|
480
|
+
const eventDispatcher = buildDispatcher();
|
|
481
|
+
currentWs.start({ eventDispatcher });
|
|
482
|
+
touchActivity();
|
|
483
|
+
reconnectDelay = 5000; // reset backoff on successful start
|
|
484
|
+
_log('INFO', 'Feishu WebSocket connecting...');
|
|
485
|
+
} catch (err) {
|
|
486
|
+
_log('ERROR', `Feishu WSClient.start failed: ${err.message}`);
|
|
487
|
+
scheduleReconnect();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
464
490
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
});
|
|
491
|
+
function scheduleReconnect() {
|
|
492
|
+
if (stopped) return;
|
|
493
|
+
clearTimeout(reconnectTimer);
|
|
494
|
+
_log('INFO', `Feishu reconnecting in ${reconnectDelay / 1000}s...`);
|
|
495
|
+
reconnectTimer = setTimeout(() => {
|
|
496
|
+
_log('INFO', 'Feishu reconnecting now...');
|
|
497
|
+
connect();
|
|
498
|
+
}, reconnectDelay);
|
|
499
|
+
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Health check: detect silent WebSocket death via API probe
|
|
503
|
+
function startHealthCheck() {
|
|
504
|
+
clearInterval(healthTimer);
|
|
505
|
+
healthTimer = setInterval(async () => {
|
|
506
|
+
if (stopped) return;
|
|
507
|
+
const silentMs = Date.now() - _lastActivityAt;
|
|
508
|
+
if (silentMs < SILENT_THRESHOLD) return; // recently active, skip
|
|
509
|
+
// Probe: try a lightweight API call to verify token + connectivity
|
|
510
|
+
try {
|
|
511
|
+
await withTimeout(client.im.chat.list({ params: { page_size: 1 } }), 10000);
|
|
512
|
+
// API works — connection might still be alive, just quiet. Reset activity.
|
|
513
|
+
touchActivity();
|
|
514
|
+
} catch (err) {
|
|
515
|
+
_log('WARN', `Feishu health check failed after ${Math.round(silentMs / 1000)}s silence: ${err.message} — reconnecting`);
|
|
516
|
+
try { currentWs?.stop?.(); } catch { /* ignore */ }
|
|
517
|
+
currentWs = null;
|
|
518
|
+
connect();
|
|
519
|
+
}
|
|
520
|
+
}, HEALTH_CHECK_INTERVAL);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Initial connect
|
|
524
|
+
connect();
|
|
525
|
+
startHealthCheck();
|
|
526
|
+
|
|
527
|
+
return Promise.resolve({
|
|
528
|
+
stop() {
|
|
529
|
+
stopped = true;
|
|
530
|
+
clearTimeout(reconnectTimer);
|
|
531
|
+
clearInterval(healthTimer);
|
|
532
|
+
currentWs = null;
|
|
533
|
+
},
|
|
534
|
+
reconnect() {
|
|
535
|
+
_log('INFO', 'Feishu manual reconnect triggered');
|
|
536
|
+
reconnectDelay = 5000;
|
|
537
|
+
connect();
|
|
538
|
+
},
|
|
539
|
+
isAlive() {
|
|
540
|
+
return !stopped && (Date.now() - _lastActivityAt) < SILENT_THRESHOLD;
|
|
541
|
+
},
|
|
473
542
|
});
|
|
474
543
|
},
|
|
475
544
|
|