metame-cli 1.5.3 → 1.5.5

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.
Files changed (51) hide show
  1. package/README.md +60 -18
  2. package/index.js +352 -79
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +178 -90
  6. package/scripts/daemon-admin-commands.js +353 -105
  7. package/scripts/daemon-agent-commands.js +434 -66
  8. package/scripts/daemon-bridges.js +477 -68
  9. package/scripts/daemon-claude-engine.js +1267 -674
  10. package/scripts/daemon-command-router.js +205 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +7 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +108 -49
  15. package/scripts/daemon-file-browser.js +64 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +55 -1
  19. package/scripts/daemon-runtime-lifecycle.js +87 -0
  20. package/scripts/daemon-session-commands.js +102 -45
  21. package/scripts/daemon-session-store.js +497 -66
  22. package/scripts/daemon-siri-bridge.js +234 -0
  23. package/scripts/daemon-siri-imessage.js +209 -0
  24. package/scripts/daemon-task-scheduler.js +10 -2
  25. package/scripts/daemon.js +697 -179
  26. package/scripts/daemon.yaml +7 -0
  27. package/scripts/docs/agent-guide.md +36 -3
  28. package/scripts/docs/hook-config.md +134 -0
  29. package/scripts/docs/maintenance-manual.md +162 -5
  30. package/scripts/docs/pointer-map.md +60 -5
  31. package/scripts/feishu-adapter.js +7 -15
  32. package/scripts/hooks/doc-router.js +29 -0
  33. package/scripts/hooks/hook-utils.js +61 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +72 -0
  36. package/scripts/hooks/intent-file-transfer.js +51 -0
  37. package/scripts/hooks/intent-memory-recall.js +35 -0
  38. package/scripts/hooks/intent-ops-assist.js +54 -0
  39. package/scripts/hooks/intent-task-create.js +35 -0
  40. package/scripts/hooks/intent-team-dispatch.js +106 -0
  41. package/scripts/hooks/team-context.js +143 -0
  42. package/scripts/intent-registry.js +59 -0
  43. package/scripts/memory-extract.js +59 -0
  44. package/scripts/memory-nightly-reflect.js +109 -43
  45. package/scripts/memory.js +55 -17
  46. package/scripts/mentor-engine.js +6 -0
  47. package/scripts/schema.js +1 -0
  48. package/scripts/self-reflect.js +110 -12
  49. package/scripts/session-analytics.js +160 -0
  50. package/scripts/signal-capture.js +1 -1
  51. package/scripts/team-dispatch.js +315 -0
@@ -7,6 +7,14 @@ const {
7
7
  } = require('./usage-classifier');
8
8
  const { IS_WIN } = require('./platform');
9
9
  const { ENGINE_MODEL_CONFIG, resolveEngineModel } = require('./daemon-engine-runtime');
10
+ const { resolveProjectKey: _resolveProjectKey } = require('./team-dispatch');
11
+ const {
12
+ parseRemoteTargetRef,
13
+ getRemoteDispatchStatus,
14
+ generatePairCode,
15
+ isValidPairCode,
16
+ deriveSecretFromPairCode,
17
+ } = require('./daemon-remote-dispatch');
10
18
  let mentorEngine = null;
11
19
  try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
12
20
 
@@ -39,26 +47,26 @@ function createAdminCommandHandler(deps) {
39
47
  getDistillModel = () => 'haiku',
40
48
  } = deps;
41
49
 
42
- function resolveProjectKey(targetName, projects) {
43
- if (!targetName || !projects) return null;
44
- for (const [key, proj] of Object.entries(projects || {})) {
45
- const nicknames = Array.isArray(proj.nicknames)
46
- ? proj.nicknames
47
- : (proj.nicknames ? [proj.nicknames] : []);
48
- if (key === targetName || nicknames.some(n => n === targetName)) return key;
49
-
50
- // Also search team members (nested projects)
51
- if (Array.isArray(proj.team)) {
52
- for (const member of proj.team) {
53
- const memberNicks = Array.isArray(member.nicknames) ? member.nicknames : [];
54
- if (member.key === targetName || memberNicks.some(n => n === targetName)) {
55
- // Return full path: parentKey/teamMemberKey
56
- return `${key}/${member.key}`;
57
- }
58
- }
59
- }
50
+ // resolveProjectKey: imported from team-dispatch.js (shared with dispatch_to and daemon.js)
51
+ const resolveProjectKey = _resolveProjectKey;
52
+
53
+ /**
54
+ * Resolve a target name to { dispatchKey, projInfo }.
55
+ * resolveProjectKey returns 'parent/member' for team members; this splits
56
+ * it into the bare dispatch key and looks up the project/member config.
57
+ */
58
+ function resolveDispatchTarget(targetName, projects) {
59
+ const resolved = resolveProjectKey(targetName, projects || {});
60
+ if (!resolved) return null;
61
+ if (!resolved.includes('/')) {
62
+ return { dispatchKey: resolved, projInfo: (projects || {})[resolved] || {} };
60
63
  }
61
- return null;
64
+ const [parentKey, memberKey] = resolved.split('/');
65
+ const parent = (projects || {})[parentKey] || {};
66
+ const member = Array.isArray(parent.team)
67
+ ? (parent.team.find(m => m.key === memberKey) || {})
68
+ : {};
69
+ return { dispatchKey: memberKey, projInfo: member };
62
70
  }
63
71
 
64
72
  function resolveSenderKey(chatId, config) {
@@ -69,6 +77,14 @@ function createAdminCommandHandler(deps) {
69
77
  return map[String(chatId)] || 'user';
70
78
  }
71
79
 
80
+ function resolveBoundProjectKey(chatId, config) {
81
+ const map = {
82
+ ...(config && config.feishu ? config.feishu.chat_agent_map : {}),
83
+ ...(config && config.telegram ? config.telegram.chat_agent_map : {}),
84
+ };
85
+ return map[String(chatId)] || '';
86
+ }
87
+
72
88
  function popFlag(input, flagName) {
73
89
  const src = String(input || '');
74
90
  const re = new RegExp(`(?:^|\\s)--${flagName}\\s+(\\S+)`, 'i');
@@ -97,6 +113,87 @@ function createAdminCommandHandler(deps) {
97
113
  };
98
114
  }
99
115
 
116
+ function isLikelyTeamTaskResumeIntent(text) {
117
+ const src = String(text || '').trim();
118
+ if (!src || src.startsWith('/')) return false;
119
+ if (src.length < 4 || src.length > 120) return false;
120
+ if (/(?:新建|创建|查看|列出|列表|详情|状态|有哪些|teamtask\s*$|\/teamtask)/i.test(src)) return false;
121
+ return /(?:继续(?:做|改|修)?|接着(?:做|改|修)?|续上|接续|返工|复工|再修(?:一下)?|再改(?:一下)?).*(?:上次|那个|这单|这个任务|任务|TeamTask|team task|工单)?|(?:上次|那个|这单|这个任务).*(?:继续|接着|返工|复工|再修|再改)/i.test(src);
122
+ }
123
+
124
+ function listAutoResumeCandidates(chatId, senderKey, config) {
125
+ if (!taskBoard || typeof taskBoard.listRecentTasks !== 'function') return [];
126
+ const now = Date.now();
127
+ const chatKey = String(chatId);
128
+ const recent = taskBoard.listRecentTasks(12, null, 'team');
129
+ return recent.filter((task) => {
130
+ if (!task || task.task_kind !== 'team') return false;
131
+ if (!config.projects || !config.projects[task.to_agent]) return false;
132
+ const sourceChatId = String(task.inputs && task.inputs.source_chat_id || '').trim();
133
+ if (!sourceChatId || sourceChatId !== chatKey) return false;
134
+ const updatedAt = Date.parse(task.updated_at || task.created_at || '');
135
+ if (!Number.isFinite(updatedAt) || (now - updatedAt) > 12 * 3600_000) return false;
136
+ const participants = Array.isArray(task.participants) ? task.participants : [];
137
+ return task.from_agent === senderKey || participants.includes(senderKey);
138
+ });
139
+ }
140
+
141
+ function buildTeamTaskResumeEnvelope(task, targetKey, chatId, config) {
142
+ return taskEnvelope && taskEnvelope.normalizeTaskEnvelope
143
+ ? taskEnvelope.normalizeTaskEnvelope({
144
+ ...task,
145
+ status: 'queued',
146
+ updated_at: new Date().toISOString(),
147
+ task_kind: 'team',
148
+ participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
149
+ }, {
150
+ from_agent: task.from_agent || resolveSenderKey(chatId, config),
151
+ to_agent: targetKey,
152
+ scope_id: task.scope_id || task.task_id,
153
+ })
154
+ : {
155
+ task_id: task.task_id,
156
+ scope_id: task.scope_id || task.task_id,
157
+ from_agent: task.from_agent || resolveSenderKey(chatId, config),
158
+ to_agent: targetKey,
159
+ participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
160
+ goal: task.goal,
161
+ definition_of_done: task.definition_of_done || [],
162
+ inputs: task.inputs || {},
163
+ artifacts: task.artifacts || [],
164
+ owned_paths: task.owned_paths || [],
165
+ priority: task.priority || 'normal',
166
+ status: 'queued',
167
+ task_kind: 'team',
168
+ created_at: task.created_at,
169
+ updated_at: new Date().toISOString(),
170
+ };
171
+ }
172
+
173
+ function dispatchTeamTaskResume(task, chatId, config, senderId = null) {
174
+ const targetKey = task.to_agent;
175
+ if (!config.projects || !config.projects[targetKey]) {
176
+ return { success: false, error: `target_missing:${targetKey}` };
177
+ }
178
+ const envelope = buildTeamTaskResumeEnvelope(task, targetKey, chatId, config);
179
+ const result = dispatchTask(targetKey, {
180
+ from: envelope.from_agent || 'user',
181
+ type: 'task',
182
+ priority: envelope.priority || 'normal',
183
+ payload: {
184
+ title: envelope.goal.slice(0, 60),
185
+ prompt: envelope.goal,
186
+ task_envelope: envelope,
187
+ },
188
+ callback: false,
189
+ new_session: false,
190
+ source_chat_id: String(chatId),
191
+ source_sender_key: envelope.from_agent || resolveSenderKey(chatId, config),
192
+ source_sender_id: String(senderId || '').trim() || '',
193
+ }, config);
194
+ return { success: !!(result && result.success), result, envelope, targetKey };
195
+ }
196
+
100
197
  function formatTaskSchedule(task) {
101
198
  const at = typeof task.at === 'string' ? task.at.trim() : '';
102
199
  if (at) {
@@ -165,8 +262,30 @@ function createAdminCommandHandler(deps) {
165
262
  }
166
263
  }
167
264
 
265
+ async function sendLocalDispatchReceipt(bot, chatId, targetKey, projInfo, result, preview) {
266
+ if (!result || !result.success) return;
267
+ const icon = projInfo && projInfo.icon ? projInfo.icon : '🤖';
268
+ const name = projInfo && projInfo.name ? projInfo.name : targetKey;
269
+ const lines = [
270
+ '📮 Dispatch 回执',
271
+ '',
272
+ `状态: ${icon} ${name} 已接收并入队`,
273
+ ];
274
+ if (result.id) lines.push(`编号: ${result.id}`);
275
+ if (preview) lines.push(`摘要: ${String(preview).slice(0, 120)}`);
276
+ if (result.task_id) {
277
+ lines.push('');
278
+ lines.push(`TeamTask: ${result.task_id}`);
279
+ if (result.scope_id && result.scope_id !== result.task_id) {
280
+ lines.push(`Scope: ${result.scope_id}`);
281
+ }
282
+ lines.push(`如需复工,请使用: /TeamTask resume ${result.task_id}`);
283
+ }
284
+ await bot.sendMessage(chatId, lines.join('\n'));
285
+ }
286
+
168
287
  async function handleAdminCommand(ctx) {
169
- const { bot, chatId, text } = ctx;
288
+ const { bot, chatId, text, senderId = null } = ctx;
170
289
  const state = ctx.state || {};
171
290
  let config = ctx.config || {};
172
291
 
@@ -366,6 +485,39 @@ function createAdminCommandHandler(deps) {
366
485
  return { handled: true, config };
367
486
  }
368
487
 
488
+ if (isLikelyTeamTaskResumeIntent(text)) {
489
+ const senderKey = resolveSenderKey(chatId, config);
490
+ const candidates = listAutoResumeCandidates(chatId, senderKey, config);
491
+ if (candidates.length === 1) {
492
+ const task = candidates[0];
493
+ const resumed = dispatchTeamTaskResume(task, chatId, config, senderId);
494
+ if (resumed.success) {
495
+ if (taskBoard && typeof taskBoard.appendTaskEvent === 'function') {
496
+ taskBoard.appendTaskEvent(task.task_id, 'task_resume_requested', String(chatId), { by: String(chatId), source: 'nl_auto_resume' });
497
+ }
498
+ await bot.sendMessage(chatId, [
499
+ `🔄 已自动续跑最近的 TeamTask: ${task.task_id}`,
500
+ `目标: ${task.to_agent}`,
501
+ `意图: ${text.trim().slice(0, 80)}`,
502
+ '回执会在目标端真正接收后返回。',
503
+ ].join('\n'));
504
+ await sendLocalDispatchReceipt(bot, chatId, resumed.targetKey, config.projects[resumed.targetKey], resumed.result, resumed.envelope.goal);
505
+ return { handled: true, config };
506
+ }
507
+ await bot.sendMessage(chatId, `❌ 自动续跑失败: ${resumed.result && resumed.result.error ? resumed.result.error : 'unknown_error'}`);
508
+ return { handled: true, config };
509
+ }
510
+ if (candidates.length > 1) {
511
+ const lines = ['⚠️ 检测到你可能想复工 TeamTask,但最近有多条候选任务:'];
512
+ for (const task of candidates.slice(0, 3)) {
513
+ lines.push(`- ${task.task_id} [${task.status}] ${task.goal.slice(0, 50)}`);
514
+ }
515
+ lines.push('请直接回复更明确一点,或使用 /TeamTask 查看后再选择。');
516
+ await bot.sendMessage(chatId, lines.join('\n'));
517
+ return { handled: true, config };
518
+ }
519
+ }
520
+
369
521
  // /TeamTask — create/list/detail/resume team collaboration tasks
370
522
  const teamTaskCmdMatch = text.match(/^\/teamtask(?:\s+([\s\S]+))?$/i);
371
523
  if (teamTaskCmdMatch) {
@@ -430,13 +582,18 @@ function createAdminCommandHandler(deps) {
430
582
  task_envelope: envelope,
431
583
  },
432
584
  callback: false,
585
+ source_chat_id: String(chatId),
586
+ source_sender_key: senderKey,
587
+ source_sender_id: String(senderId || '').trim() || '',
433
588
  }, config);
434
589
  if (result.success) {
435
590
  await bot.sendMessage(chatId, [
436
- `✅ 已创建 TeamTask 并派发: ${envelope.task_id}`,
591
+ `✅ 已创建 TeamTask 并提交派发: ${envelope.task_id}`,
437
592
  `Scope: ${envelope.scope_id || envelope.task_id}`,
593
+ '回执会在目标端真正接收后返回。',
438
594
  `查看: /TeamTask ${envelope.task_id}`,
439
595
  ].join('\n'));
596
+ await sendLocalDispatchReceipt(bot, chatId, targetKey, config.projects[targetKey], result, goal);
440
597
  } else {
441
598
  await bot.sendMessage(chatId, `❌ 创建 TeamTask 失败: ${result.error}`);
442
599
  }
@@ -476,52 +633,13 @@ function createAdminCommandHandler(deps) {
476
633
  await bot.sendMessage(chatId, `❌ 目标 agent 不存在: ${targetKey}`);
477
634
  return { handled: true, config };
478
635
  }
479
- const envelope = taskEnvelope && taskEnvelope.normalizeTaskEnvelope
480
- ? taskEnvelope.normalizeTaskEnvelope({
481
- ...task,
482
- status: 'queued',
483
- updated_at: new Date().toISOString(),
484
- task_kind: 'team',
485
- participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
486
- }, {
487
- from_agent: task.from_agent || resolveSenderKey(chatId, config),
488
- to_agent: targetKey,
489
- scope_id: task.scope_id || task.task_id,
490
- })
491
- : {
492
- task_id: task.task_id,
493
- scope_id: task.scope_id || task.task_id,
494
- from_agent: task.from_agent || resolveSenderKey(chatId, config),
495
- to_agent: targetKey,
496
- participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
497
- goal: task.goal,
498
- definition_of_done: task.definition_of_done || [],
499
- inputs: task.inputs || {},
500
- artifacts: task.artifacts || [],
501
- owned_paths: task.owned_paths || [],
502
- priority: task.priority || 'normal',
503
- status: 'queued',
504
- task_kind: 'team',
505
- created_at: task.created_at,
506
- updated_at: new Date().toISOString(),
507
- };
508
-
509
- const result = dispatchTask(targetKey, {
510
- from: envelope.from_agent || 'user',
511
- type: 'task',
512
- priority: envelope.priority || 'normal',
513
- payload: {
514
- title: envelope.goal.slice(0, 60),
515
- prompt: envelope.goal,
516
- task_envelope: envelope,
517
- },
518
- callback: false,
519
- new_session: false,
520
- }, config);
636
+ const resumed = dispatchTeamTaskResume(task, chatId, config, senderId);
637
+ const { result, envelope } = resumed;
521
638
 
522
639
  if (result.success) {
523
640
  taskBoard.appendTaskEvent(task.task_id, 'task_resume_requested', String(chatId), { by: String(chatId) });
524
- await bot.sendMessage(chatId, `✅ 已续跑 TeamTask: ${task.task_id}`);
641
+ await bot.sendMessage(chatId, `✅ 已续跑 TeamTask: ${task.task_id}\n回执会在目标端真正接收后返回。`);
642
+ await sendLocalDispatchReceipt(bot, chatId, targetKey, config.projects[targetKey], result, envelope.goal);
525
643
  } else {
526
644
  await bot.sendMessage(chatId, `❌ 续跑失败: ${result.error}`);
527
645
  }
@@ -622,23 +740,126 @@ function createAdminCommandHandler(deps) {
622
740
  return { handled: true, config };
623
741
  }
624
742
 
743
+ // /dispatch peers — show remote dispatch config
744
+ if (args === 'peers') {
745
+ const rd = getRemoteDispatchStatus(config);
746
+ if (!rd) {
747
+ await bot.sendMessage(chatId, '📡 远端 Dispatch 未配置\n\n在 daemon.yaml 中设置 feishu.remote_dispatch 启用。');
748
+ return { handled: true, config };
749
+ }
750
+ let msg = `📡 远端 Dispatch 配置\n─────────────\nself: ${rd.selfPeer}\nrelay chat: ${rd.chatId}\nmode: pair code\nsecret: ${rd.hasSecret ? 'configured' : 'missing'}\n\n远端成员:\n`;
751
+ let hasRemote = false;
752
+ for (const [key, proj] of Object.entries(config.projects || {})) {
753
+ if (!Array.isArray(proj.team)) continue;
754
+ for (const m of proj.team) {
755
+ if (m.peer) {
756
+ hasRemote = true;
757
+ msg += `- ${m.icon || '🤖'} ${m.name || m.key} → peer:${m.peer} (${key}/${m.key})\n`;
758
+ }
759
+ }
760
+ }
761
+ if (!hasRemote) msg += '(无远端成员)\n';
762
+ await bot.sendMessage(chatId, msg.trim());
763
+ return { handled: true, config };
764
+ }
765
+
766
+ if (args === 'code') {
767
+ const rd = getRemoteDispatchStatus(config);
768
+ if (!rd) {
769
+ await bot.sendMessage(chatId, '📡 远端 Dispatch 未配置\n\n在 daemon.yaml 中设置 feishu.remote_dispatch 启用。');
770
+ return { handled: true, config };
771
+ }
772
+ const code = generatePairCode();
773
+ const secret = deriveSecretFromPairCode(code, rd.chatId);
774
+ backupConfig();
775
+ const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
776
+ if (!cfg.feishu) cfg.feishu = {};
777
+ if (!cfg.feishu.remote_dispatch) cfg.feishu.remote_dispatch = {};
778
+ cfg.feishu.remote_dispatch.secret = secret;
779
+ writeConfigSafe(cfg);
780
+ config = loadConfig();
781
+ await bot.sendMessage(chatId, `🔐 配对码已生成\n\n配对码: ${code}\n\n把这 6 位码发到另一台设备执行:\n/dispatch pair ${code}`);
782
+ return { handled: true, config };
783
+ }
784
+
785
+ const pairMatch = args.match(/^pair\s+(\d{6})$/);
786
+ if (pairMatch) {
787
+ const rd = getRemoteDispatchStatus(config);
788
+ if (!rd) {
789
+ await bot.sendMessage(chatId, '📡 远端 Dispatch 未配置\n\n在 daemon.yaml 中设置 feishu.remote_dispatch 启用。');
790
+ return { handled: true, config };
791
+ }
792
+ const code = pairMatch[1];
793
+ if (!isValidPairCode(code)) {
794
+ await bot.sendMessage(chatId, '❌ 配对码必须是 6 位数字');
795
+ return { handled: true, config };
796
+ }
797
+ const secret = deriveSecretFromPairCode(code, rd.chatId);
798
+ backupConfig();
799
+ const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
800
+ if (!cfg.feishu) cfg.feishu = {};
801
+ if (!cfg.feishu.remote_dispatch) cfg.feishu.remote_dispatch = {};
802
+ cfg.feishu.remote_dispatch.secret = secret;
803
+ writeConfigSafe(cfg);
804
+ config = loadConfig();
805
+ await bot.sendMessage(chatId, `✅ 配对码已写入\n\n当前设备: ${rd.selfPeer}\nrelay chat: ${rd.chatId}\n现在可以测试 /dispatch to <peer:project> ...`);
806
+ return { handled: true, config };
807
+ }
808
+
625
809
  // /dispatch to <agent> <prompt>
626
810
  const toMatch = args.match(/^to\s+(\S+)\s+(.+)$/s);
627
811
  if (toMatch) {
628
812
  const targetName = toMatch[1];
629
813
  const prompt = toMatch[2].trim();
814
+ const senderKey = resolveSenderKey(chatId, config);
630
815
 
631
- // Resolve target by project key or nickname
632
- const targetKey = resolveProjectKey(targetName, config.projects || {});
633
- if (!targetKey) {
634
- await bot.sendMessage(chatId, `未找到 agent: ${targetName}\n可用: ${Object.keys(config.projects || {}).join(', ')}`);
816
+ // Check for remote target (peer:project format)
817
+ const remoteTarget = parseRemoteTargetRef(targetName);
818
+ if (remoteTarget && deps.sendRemoteDispatch) {
819
+ const res = await deps.sendRemoteDispatch({
820
+ type: 'task',
821
+ to_peer: remoteTarget.peer,
822
+ target_project: remoteTarget.project,
823
+ prompt,
824
+ source_chat_id: String(chatId),
825
+ source_sender_key: senderKey,
826
+ source_sender_id: String(senderId || '').trim() || '',
827
+ }, config);
828
+ if (res.success) {
829
+ await bot.sendMessage(chatId, `📡 已发送给 ${remoteTarget.peer}:${remoteTarget.project}`);
830
+ } else {
831
+ await bot.sendMessage(chatId, `❌ 远端派发失败: ${res.error}`);
832
+ }
635
833
  return { handled: true, config };
636
834
  }
637
835
 
638
- // Determine sender from current chat's project mapping
639
- const senderKey = resolveSenderKey(chatId, config);
836
+ // Resolve target by project key or nickname (handles team members via compound key)
837
+ const resolved = resolveDispatchTarget(targetName, config.projects || {});
838
+ if (!resolved) {
839
+ await bot.sendMessage(chatId, `未找到 agent: ${targetName}\n可用: ${Object.keys(config.projects || {}).join(', ')}`);
840
+ return { handled: true, config };
841
+ }
842
+ const { dispatchKey: targetKey, projInfo } = resolved;
843
+
844
+ // Check if resolved target is a remote team member
845
+ if (projInfo.peer && deps.sendRemoteDispatch) {
846
+ const res = await deps.sendRemoteDispatch({
847
+ type: 'task',
848
+ to_peer: projInfo.peer,
849
+ target_project: targetKey,
850
+ prompt,
851
+ source_chat_id: String(chatId),
852
+ source_sender_key: senderKey,
853
+ source_sender_id: String(senderId || '').trim() || '',
854
+ }, config);
855
+ if (res.success) {
856
+ await bot.sendMessage(chatId, `📡 已发送给 ${projInfo.icon || '🤖'} ${projInfo.name || targetKey} (${projInfo.peer})`);
857
+ } else {
858
+ await bot.sendMessage(chatId, `❌ 远端派发失败: ${res.error}`);
859
+ }
860
+ return { handled: true, config };
861
+ }
640
862
 
641
- const projInfo = config.projects[targetKey] || {};
642
863
  // Find the target project's own Feishu chat (reverse lookup of chat_agent_map)
643
864
  const feishuChatAgentMap = (config.feishu && config.feishu.chat_agent_map) || {};
644
865
  const targetChatId = Object.entries(feishuChatAgentMap).find(([, v]) => v === targetKey)?.[0] || null;
@@ -659,10 +880,14 @@ function createAdminCommandHandler(deps) {
659
880
  priority: 'normal',
660
881
  payload: { title: prompt.slice(0, 60), prompt },
661
882
  callback: false,
883
+ source_chat_id: String(chatId),
884
+ source_sender_key: senderKey,
885
+ source_sender_id: String(senderId || '').trim() || '',
662
886
  }, config, replyFn, dispatchStreamOptions);
663
887
 
664
888
  if (result.success) {
665
- await bot.sendMessage(chatId, `✅ 已派发给 ${projInfo.name || targetName},执行中…`);
889
+ await bot.sendMessage(chatId, `✅ 已提交派发给 ${projInfo.name || targetName},等待回执…`);
890
+ await sendLocalDispatchReceipt(bot, chatId, targetKey, projInfo, result, prompt);
666
891
  } else {
667
892
  await bot.sendMessage(chatId, `❌ 派发失败: ${result.error}`);
668
893
  }
@@ -673,7 +898,11 @@ function createAdminCommandHandler(deps) {
673
898
  '用法:',
674
899
  '/dispatch status — 查看状态',
675
900
  '/dispatch log — 查看记录',
901
+ '/dispatch peers — 查看远端配置',
902
+ '/dispatch code — 生成 6 位配对码并写入本机',
903
+ '/dispatch pair <123456> — 输入 6 位配对码写入本机',
676
904
  '/dispatch to <agent> <任务内容> — 直接跨 agent 派发',
905
+ '/dispatch to <peer:project> <任务内容> — 跨设备派发',
677
906
  '/TeamTask create <agent> <目标> [--scope <id>] [--parent <id>] — 创建/续接 TeamTask',
678
907
  '/TeamTask — 查看 TeamTask 列表',
679
908
  ].join('\n'));
@@ -691,42 +920,30 @@ function createAdminCommandHandler(deps) {
691
920
  const targetName = msgMatch[1];
692
921
  const message = msgMatch[2].trim();
693
922
 
694
- // Resolve target - check team members first, then projects
695
- let targetKey = null;
923
+ // Resolve target by nickname or key (handles team members via compound key)
696
924
  const senderKey = resolveSenderKey(chatId, config);
697
- const senderProj = config.projects ? config.projects[senderKey] : null;
698
-
699
- // Check if sender has a team
700
- if (senderProj && Array.isArray(senderProj.team)) {
701
- for (const member of senderProj.team) {
702
- const nicks = Array.isArray(member.nicknames) ? member.nicknames : [];
703
- if (member.key === targetName || nicks.some(n => n === targetName)) {
704
- targetKey = member.key;
705
- break;
706
- }
707
- }
708
- }
709
- // Fall back to project lookup
710
- if (!targetKey) {
711
- targetKey = resolveProjectKey(targetName, config.projects || {});
712
- }
925
+ const resolved = resolveDispatchTarget(targetName, config.projects || {});
713
926
 
714
- if (!targetKey) {
927
+ if (!resolved) {
715
928
  await bot.sendMessage(chatId, `未找到 agent: ${targetName}`);
716
929
  return { handled: true, config };
717
930
  }
931
+ const { dispatchKey: targetKey, projInfo: toProj } = resolved;
718
932
 
719
- const toProj = config.projects[targetKey] || {};
720
933
  const result = dispatchTask(targetKey, {
721
934
  from: senderKey,
722
935
  type: 'message',
723
936
  priority: 'normal',
724
937
  payload: { title: 'team message', prompt: `[来自团队的消息]\n\n${message}` },
725
938
  callback: false,
939
+ source_chat_id: String(chatId),
940
+ source_sender_key: senderKey,
941
+ source_sender_id: String(senderId || '').trim() || '',
726
942
  }, config, null, null);
727
943
 
728
944
  if (result.success) {
729
945
  await bot.sendMessage(chatId, `📬 已发送消息给 ${toProj.icon || '🤖'} ${toProj.name || targetKey}`);
946
+ await sendLocalDispatchReceipt(bot, chatId, targetKey, toProj, result, message);
730
947
  } else {
731
948
  await bot.sendMessage(chatId, `❌ 发送失败: ${result.error}`);
732
949
  }
@@ -917,7 +1134,7 @@ function createAdminCommandHandler(deps) {
917
1134
  const arg = text.slice('/mentor'.length).trim();
918
1135
 
919
1136
  if (!arg || arg === 'status') {
920
- const status = mentorEngine && typeof mentorEngine.getRuntimeStatus === 'function'
1137
+ const status = mentorCfg.enabled && mentorEngine && typeof mentorEngine.getRuntimeStatus === 'function'
921
1138
  ? mentorEngine.getRuntimeStatus()
922
1139
  : { debt_count: 0, cooldown_remaining_ms: 0 };
923
1140
  const mode = String(mentorCfg.mode || modeFromLevel(mentorCfg.friction_level));
@@ -937,6 +1154,9 @@ function createAdminCommandHandler(deps) {
937
1154
 
938
1155
  if (arg === 'on' || arg === 'off') {
939
1156
  mentorCfg.enabled = arg === 'on';
1157
+ if (!mentorCfg.enabled && mentorEngine && typeof mentorEngine.clearRuntime === 'function') {
1158
+ mentorEngine.clearRuntime();
1159
+ }
940
1160
  writeConfigSafe(cfg);
941
1161
  config = loadConfig();
942
1162
  await bot.sendMessage(chatId, mentorCfg.enabled
@@ -1239,21 +1459,30 @@ function createAdminCommandHandler(deps) {
1239
1459
  // Switching engine auto-syncs: distill model + preferred provider (if available)
1240
1460
  if (text === '/engine' || text.startsWith('/engine ')) {
1241
1461
  const arg = text.slice('/engine'.length).trim().toLowerCase();
1462
+ const boundProjectKey = resolveBoundProjectKey(chatId, config);
1463
+ const boundProject = boundProjectKey && config && config.projects ? config.projects[boundProjectKey] : null;
1242
1464
  if (!arg) {
1243
- const cur = getDefaultEngine();
1244
- const curEngineCfg = ENGINE_MODEL_CONFIG[cur] || ENGINE_MODEL_CONFIG.claude;
1245
- const activeProvider = (cur === 'claude' && providerMod)
1465
+ const cur = boundProject && boundProject.engine ? String(boundProject.engine).trim().toLowerCase() : getDefaultEngine();
1466
+ const safeCur = cur === 'codex' ? 'codex' : 'claude';
1467
+ const curEngineCfg = ENGINE_MODEL_CONFIG[safeCur] || ENGINE_MODEL_CONFIG.claude;
1468
+ const activeProvider = (safeCur === 'claude' && providerMod)
1246
1469
  ? providerMod.getActiveName()
1247
1470
  : curEngineCfg.provider;
1248
1471
  const distill = getDistillModel();
1249
1472
  const daemonCfg = config.daemon || {};
1250
- const currentModel = resolveEngineModel(cur, daemonCfg);
1473
+ const currentModel = resolveEngineModel(safeCur, daemonCfg, boundProject && boundProject.model);
1474
+ const scopeLine = boundProjectKey
1475
+ ? `📍 当前 chat 绑定 Agent: ${boundProjectKey}`
1476
+ : `📍 当前 chat 使用全局默认引擎`;
1251
1477
  await bot.sendMessage(chatId, [
1252
- `🔧 引擎: ${cur} | Provider: ${activeProvider}`,
1478
+ `🔧 引擎: ${safeCur} | Provider: ${activeProvider}`,
1253
1479
  `🤖 会话模型: ${currentModel} | 后台轻量: ${distill}`,
1480
+ scopeLine,
1254
1481
  '',
1255
1482
  '用法: /engine claude 或 /engine codex',
1256
- '切换引擎将自动同步 distill model 和首选 provider',
1483
+ boundProjectKey
1484
+ ? '当前 chat 已绑定 Agent;切换时会同步更新该 Agent 的 engine/model'
1485
+ : '切换引擎将自动同步 distill model 和首选 provider',
1257
1486
  ].join('\n'));
1258
1487
  return { handled: true, config };
1259
1488
  }
@@ -1266,13 +1495,29 @@ function createAdminCommandHandler(deps) {
1266
1495
 
1267
1496
  setDefaultEngine(arg); // syncs distill model + providerMod.setEngine (no longer resets session model)
1268
1497
  const distill = getDistillModel();
1269
- const freshCfg = loadConfig();
1498
+ let freshCfg = loadConfig();
1499
+ if (boundProjectKey && freshCfg && freshCfg.projects && freshCfg.projects[boundProjectKey]) {
1500
+ const nextCfg = JSON.parse(JSON.stringify(freshCfg));
1501
+ nextCfg.projects[boundProjectKey].engine = arg;
1502
+ nextCfg.projects[boundProjectKey].model = resolveEngineModel(arg, nextCfg.daemon || {});
1503
+ writeConfigSafe(nextCfg);
1504
+ freshCfg = loadConfig();
1505
+ }
1270
1506
  const freshDaemon = freshCfg.daemon || {};
1271
- const syncedModel = resolveEngineModel(arg, freshDaemon);
1507
+ const syncedModel = resolveEngineModel(
1508
+ arg,
1509
+ freshDaemon,
1510
+ boundProjectKey && freshCfg.projects && freshCfg.projects[boundProjectKey]
1511
+ ? freshCfg.projects[boundProjectKey].model
1512
+ : ''
1513
+ );
1272
1514
 
1273
- // Auto-switch provider if the preferred one exists in providers.yaml
1515
+ // Auto-switch provider only for Claude-compatible routing.
1516
+ // Codex auth is handled by `codex login` / `OPENAI_API_KEY`, not providers.yaml.
1274
1517
  let providerNote = '';
1275
- if (providerMod && preferredProvider) {
1518
+ if (arg === 'codex') {
1519
+ providerNote = '\n🔌 Codex 认证: 使用 `codex login` 或 OPENAI_API_KEY(/provider 不参与 Codex 路由)';
1520
+ } else if (providerMod && preferredProvider) {
1276
1521
  try {
1277
1522
  providerMod.setActive(preferredProvider);
1278
1523
  providerNote = `\n🔌 Provider 已同步: ${preferredProvider}`;
@@ -1283,8 +1528,11 @@ function createAdminCommandHandler(deps) {
1283
1528
  }
1284
1529
  }
1285
1530
 
1286
- await bot.sendMessage(chatId, `✅ 引擎已切换: ${arg}\n🤖 会话模型: ${syncedModel}\n🧪 后台轻量模型: ${distill}${providerNote}`);
1287
- return { handled: true, config };
1531
+ const scopeNote = boundProjectKey
1532
+ ? `\n📍 已同步当前 Agent: ${boundProjectKey}`
1533
+ : '';
1534
+ await bot.sendMessage(chatId, `✅ 引擎已切换: ${arg}\n🤖 会话模型: ${syncedModel}\n🧪 后台轻量模型: ${distill}${scopeNote}${providerNote}`);
1535
+ return { handled: true, config: freshCfg };
1288
1536
  }
1289
1537
 
1290
1538
  // /distill-model [name] — show or update distill model