metame-cli 1.5.21 → 1.5.23

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.
@@ -0,0 +1,295 @@
1
+ 'use strict';
2
+
3
+ function buildBoundSessionChatId(projectKey) {
4
+ const key = String(projectKey || '').trim();
5
+ return key ? `_bound_${key}` : '';
6
+ }
7
+
8
+ function getBoundProject(chatId, cfg) {
9
+ const agentMap = {
10
+ ...(cfg && cfg.telegram ? cfg.telegram.chat_agent_map : {}),
11
+ ...(cfg && cfg.feishu ? cfg.feishu.chat_agent_map : {}),
12
+ };
13
+ const boundKey = agentMap[String(chatId)];
14
+ const boundProj = boundKey && cfg && cfg.projects && cfg.projects[boundKey];
15
+ return { boundKey: boundKey || null, boundProj: boundProj || null };
16
+ }
17
+
18
+ function getLatestActivationForChat(chatId, pendingActivations) {
19
+ if (!pendingActivations || pendingActivations.size === 0) return null;
20
+ const cid = String(chatId);
21
+ let latest = null;
22
+ for (const rec of pendingActivations.values()) {
23
+ if (rec.createdByChatId === cid) continue;
24
+ if (!latest || rec.createdAt > latest.createdAt) latest = rec;
25
+ }
26
+ return latest;
27
+ }
28
+
29
+ function listUnboundProjects(cfg) {
30
+ const allBoundKeys = new Set(Object.values({
31
+ ...(cfg && cfg.telegram ? cfg.telegram.chat_agent_map : {}),
32
+ ...(cfg && cfg.feishu ? cfg.feishu.chat_agent_map : {}),
33
+ }));
34
+
35
+ return Object.entries((cfg && cfg.projects) || {})
36
+ .filter(([key, p]) => p && p.cwd && !allBoundKeys.has(key))
37
+ .map(([key, p]) => ({ key, name: p.name || key, cwd: p.cwd, icon: p.icon || '🤖' }));
38
+ }
39
+
40
+ function attachBoundSession({
41
+ attachOrCreateSession,
42
+ projectKey,
43
+ chatId,
44
+ cwd,
45
+ name,
46
+ engine,
47
+ normalizeCwd,
48
+ getDefaultEngine,
49
+ }) {
50
+ if (!cwd || typeof attachOrCreateSession !== 'function') return;
51
+ const sessionChatId = projectKey ? buildBoundSessionChatId(projectKey) : String(chatId);
52
+ attachOrCreateSession(
53
+ sessionChatId,
54
+ normalizeCwd(cwd),
55
+ name || projectKey || '',
56
+ engine || getDefaultEngine()
57
+ );
58
+ }
59
+
60
+ async function bindAgentToChat({
61
+ agentTools,
62
+ doBindAgent,
63
+ bot,
64
+ chatId,
65
+ agentName,
66
+ agentCwd,
67
+ HOME,
68
+ attachOrCreateSession,
69
+ normalizeCwd,
70
+ getDefaultEngine,
71
+ announce = true,
72
+ }) {
73
+ if (agentTools && typeof agentTools.bindAgentToChat === 'function') {
74
+ const res = await agentTools.bindAgentToChat(chatId, agentName, agentCwd);
75
+ if (!res.ok) {
76
+ await bot.sendMessage(chatId, `❌ 绑定失败: ${res.error}`);
77
+ return { ok: false };
78
+ }
79
+ const p = res.data.project || {};
80
+ const icon = p.icon || '🤖';
81
+ const action = res.data.isNewProject ? '绑定成功' : '重新绑定';
82
+ const displayCwd = String(res.data.cwd || '').replace(HOME, '~');
83
+ attachBoundSession({
84
+ attachOrCreateSession,
85
+ projectKey: res.data.projectKey,
86
+ chatId,
87
+ cwd: res.data.cwd,
88
+ name: p.name || agentName || res.data.projectKey || '',
89
+ engine: p.engine,
90
+ normalizeCwd,
91
+ getDefaultEngine,
92
+ });
93
+ if (announce) {
94
+ await bot.sendMessage(chatId, `${icon} ${p.name || agentName} ${action}\n目录: ${displayCwd}`);
95
+ }
96
+ return { ok: true, data: res.data };
97
+ }
98
+
99
+ const fallback = await doBindAgent(bot, chatId, agentName, agentCwd);
100
+ if (!fallback || fallback.ok === false) {
101
+ return { ok: false, error: (fallback && fallback.error) || 'bind failed' };
102
+ }
103
+ const fallbackCwd = (fallback.data && fallback.data.cwd) || agentCwd;
104
+ attachBoundSession({
105
+ attachOrCreateSession,
106
+ projectKey: fallback && fallback.data ? fallback.data.projectKey : null,
107
+ chatId,
108
+ cwd: fallbackCwd,
109
+ name: agentName || '',
110
+ engine: fallback && fallback.data && fallback.data.project ? fallback.data.project.engine : null,
111
+ normalizeCwd,
112
+ getDefaultEngine,
113
+ });
114
+ return {
115
+ ok: true,
116
+ data: {
117
+ cwd: fallbackCwd,
118
+ projectKey: fallback && fallback.data ? fallback.data.projectKey : null,
119
+ project: fallback && fallback.data ? fallback.data.project : null,
120
+ },
121
+ };
122
+ }
123
+
124
+ async function editAgentRole({ agentTools, mergeAgentRole, workspaceDir, deltaText }) {
125
+ if (agentTools && typeof agentTools.editAgentRoleDefinition === 'function') {
126
+ return agentTools.editAgentRoleDefinition(workspaceDir, deltaText);
127
+ }
128
+ const legacy = await mergeAgentRole(workspaceDir, deltaText);
129
+ if (legacy.error) return { ok: false, error: legacy.error };
130
+ return { ok: true, data: legacy };
131
+ }
132
+
133
+ async function listAgents({ agentTools, chatId, loadConfig }) {
134
+ if (agentTools && typeof agentTools.listAllAgents === 'function') {
135
+ return agentTools.listAllAgents(chatId);
136
+ }
137
+
138
+ const cfg = loadConfig();
139
+ const projects = cfg.projects || {};
140
+ const entries = Object.entries(projects)
141
+ .filter(([, p]) => p && p.cwd)
142
+ .map(([key, p]) => ({
143
+ key,
144
+ name: p.name || key,
145
+ cwd: p.cwd,
146
+ icon: p.icon || '🤖',
147
+ }));
148
+ const { boundKey } = getBoundProject(chatId, cfg);
149
+ return { ok: true, data: { agents: entries, boundKey } };
150
+ }
151
+
152
+ async function unbindAgent({ agentTools, chatId, loadConfig, writeConfigSafe, backupConfig }) {
153
+ if (agentTools && typeof agentTools.unbindCurrentAgent === 'function') {
154
+ return agentTools.unbindCurrentAgent(chatId);
155
+ }
156
+
157
+ const cfg = loadConfig();
158
+ const isTg = typeof chatId === 'number';
159
+ const ak = isTg ? 'telegram' : 'feishu';
160
+ if (!cfg[ak]) cfg[ak] = {};
161
+ if (!cfg[ak].chat_agent_map) cfg[ak].chat_agent_map = {};
162
+ const old = cfg[ak].chat_agent_map[String(chatId)] || null;
163
+ if (old) {
164
+ delete cfg[ak].chat_agent_map[String(chatId)];
165
+ if (typeof writeConfigSafe === 'function') writeConfigSafe(cfg);
166
+ if (typeof backupConfig === 'function') backupConfig();
167
+ }
168
+ return { ok: true, data: { unbound: !!old, previousProjectKey: old } };
169
+ }
170
+
171
+ async function createWorkspaceAgent({
172
+ agentTools,
173
+ chatId,
174
+ agentName,
175
+ workspaceDir,
176
+ roleDescription,
177
+ pendingActivations,
178
+ skipChatBinding = false,
179
+ engine = null,
180
+ attachOrCreateSession,
181
+ normalizeCwd,
182
+ getDefaultEngine,
183
+ legacyCreate,
184
+ }) {
185
+ let res;
186
+ if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
187
+ res = await agentTools.createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, {
188
+ skipChatBinding,
189
+ engine,
190
+ });
191
+ } else if (typeof legacyCreate === 'function') {
192
+ res = await legacyCreate();
193
+ } else {
194
+ res = { ok: false, error: 'agentTools.createNewWorkspaceAgent unavailable' };
195
+ }
196
+
197
+ if (!res.ok) return res;
198
+
199
+ const data = res.data || {};
200
+ if (skipChatBinding) {
201
+ if (data.projectKey && pendingActivations) {
202
+ pendingActivations.set(data.projectKey, {
203
+ agentKey: data.projectKey,
204
+ agentName: (data.project && data.project.name) || agentName || data.projectKey,
205
+ cwd: data.cwd,
206
+ createdByChatId: String(chatId),
207
+ createdAt: Date.now(),
208
+ });
209
+ }
210
+ return res;
211
+ }
212
+
213
+ attachBoundSession({
214
+ attachOrCreateSession,
215
+ projectKey: data.projectKey,
216
+ chatId,
217
+ cwd: data.cwd,
218
+ name: (data.project && data.project.name) || agentName || data.projectKey || '',
219
+ engine: data.project && data.project.engine,
220
+ normalizeCwd,
221
+ getDefaultEngine,
222
+ });
223
+ return res;
224
+ }
225
+
226
+ async function handleActivateCommand({
227
+ bot,
228
+ chatId,
229
+ loadConfig,
230
+ pendingActivations,
231
+ bindAgent,
232
+ }) {
233
+ const cfg = loadConfig();
234
+ const { boundKey } = getBoundProject(chatId, cfg);
235
+ if (boundKey) {
236
+ await bot.sendMessage(chatId, `此群已绑定到「${boundKey}」,无需激活。如需更换请先 /agent unbind`);
237
+ return true;
238
+ }
239
+
240
+ const activation = getLatestActivationForChat(chatId, pendingActivations);
241
+ if (!activation) {
242
+ if (pendingActivations) {
243
+ for (const rec of pendingActivations.values()) {
244
+ if (rec.createdByChatId === String(chatId)) {
245
+ await bot.sendMessage(
246
+ chatId,
247
+ `❌ 不能在创建来源群激活。\n请在你新建的目标群里发送 \`/activate\`\n\n或在任意群用: \`/agent bind ${rec.agentName} ${rec.cwd}\``
248
+ );
249
+ return true;
250
+ }
251
+ }
252
+ }
253
+
254
+ const unboundProjects = listUnboundProjects(cfg);
255
+ if (unboundProjects.length === 1) {
256
+ const proj = unboundProjects[0];
257
+ const bindRes = await bindAgent(proj.key, proj.cwd);
258
+ if (bindRes.ok && pendingActivations) pendingActivations.delete(proj.key);
259
+ return true;
260
+ }
261
+
262
+ if (unboundProjects.length > 1) {
263
+ const lines = ['请选择要激活的 Agent:', ''];
264
+ for (const p of unboundProjects) {
265
+ lines.push(`${p.icon} ${p.name} → \`/agent bind ${p.key} ${p.cwd}\``);
266
+ }
267
+ lines.push('\n发送对应命令即可绑定此群。');
268
+ await bot.sendMessage(chatId, lines.join('\n'));
269
+ return true;
270
+ }
271
+
272
+ await bot.sendMessage(
273
+ chatId,
274
+ '没有待激活的 Agent。\n\n如果已创建过 Agent,直接用:\n`/agent bind <名称> <目录>`\n即可绑定,不需要重新创建。'
275
+ );
276
+ return true;
277
+ }
278
+
279
+ const bindRes = await bindAgent(activation.agentName, activation.cwd);
280
+ if (bindRes.ok && pendingActivations) pendingActivations.delete(activation.agentKey);
281
+ return true;
282
+ }
283
+
284
+ module.exports = {
285
+ getBoundProject,
286
+ getLatestActivationForChat,
287
+ listUnboundProjects,
288
+ buildBoundSessionChatId,
289
+ bindAgentToChat,
290
+ createWorkspaceAgent,
291
+ editAgentRole,
292
+ listAgents,
293
+ unbindAgent,
294
+ handleActivateCommand,
295
+ };
@@ -817,14 +817,14 @@ function createBridgeStarter(deps) {
817
817
  let _replyMappingFound = false; // true = mapping exists (agentKey may be null = main)
818
818
  // Load state once for the entire routing block
819
819
  const _st = loadState();
820
+ const _parentMapping = parentId && _st.msg_sessions ? _st.msg_sessions[parentId] : null;
820
821
  // Quoted reply = explicit parentId but NOT a topic thread (topics always carry parentId=root_id)
821
822
  const _isQuotedReply = !!(parentId && !threadRootId);
822
- if (_isQuotedReply) {
823
- log('INFO', `Feishu reply metadata detected chat=${chatId} parentId=${parentId}`);
823
+ if (parentId) {
824
+ log('INFO', `Feishu reply metadata detected chat=${chatId} parentId=${parentId}${threadRootId ? ' topic=true' : ''}`);
824
825
  }
825
- // In topic mode, session continuity is handled by pipelineChatId — skip msg_sessions lookup
826
826
  if (_isQuotedReply) {
827
- const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
827
+ const mapped = _parentMapping;
828
828
  if (mapped) {
829
829
  _replyMappingFound = true;
830
830
  if (typeof restoreSessionFromReply === 'function') {
@@ -843,6 +843,10 @@ function createBridgeStarter(deps) {
843
843
  } else {
844
844
  log('INFO', `Feishu reply parentId=${parentId} had no msg_sessions mapping`);
845
845
  }
846
+ } else if (threadRootId && _parentMapping) {
847
+ _replyMappingFound = true;
848
+ _replyAgentKey = _parentMapping.agentKey || null;
849
+ log('INFO', `Feishu topic inherited root mapping agentKey=${_replyAgentKey || 'main'} parentId=${parentId}`);
846
850
  }
847
851
 
848
852
  // Helper: set/clear sticky on shared state object and persist
@@ -857,13 +861,22 @@ function createBridgeStarter(deps) {
857
861
  if (_st.team_sticky) delete _st.team_sticky[_chatKey];
858
862
  saveState(_st);
859
863
  };
860
- const _stickyKey = (_st.team_sticky || {})[_chatKey] || null;
864
+ let _stickyKey = (_st.team_sticky || {})[_chatKey] || null;
861
865
 
862
866
  // Team group routing: if bound project has a team array, check message for member nickname
863
867
  // Non-/stop slash commands bypass team routing → handled by main project
864
868
  const { key: _boundKey, project: _boundProj } = _getBoundProject(chatId, liveCfg);
865
869
  const _isTeamSlashCmd = trimmedText.startsWith('/') && !/^\/stop(\s|$)/i.test(trimmedText);
866
870
  if (_boundProj && Array.isArray(_boundProj.team) && _boundProj.team.length > 0 && !_isTeamSlashCmd) {
871
+ if (threadRootId && !_stickyKey && _replyAgentKey) {
872
+ const _topicRootMember = _boundProj.team.find(m => m.key === _replyAgentKey);
873
+ if (_topicRootMember) {
874
+ _setSticky(_topicRootMember.key);
875
+ _stickyKey = _topicRootMember.key;
876
+ log('INFO', `Topic root mapping → sticky set: ${_chatKey.slice(-8)} → ${_topicRootMember.key}`);
877
+ }
878
+ }
879
+
867
880
  // ── /stop precise routing for team groups ──
868
881
  const _stopMatch = trimmedText && trimmedText.match(/^\/stop(?:\s+(.+))?$/i);
869
882
  if (_stopMatch) {
@@ -9,9 +9,17 @@ const {
9
9
  ENGINE_MODEL_CONFIG,
10
10
  _private: { resolveCodexPermissionProfile },
11
11
  } = require('./daemon-engine-runtime');
12
- const { buildIntentHintBlock } = require('./intent-registry');
13
12
  const { rawChatId } = require('./core/thread-chat-id');
14
13
  const { buildAgentContextForEngine, buildMemorySnapshotContent, refreshMemorySnapshot } = require('./agent-layer');
14
+ const {
15
+ adaptDaemonHintForEngine,
16
+ buildAgentHint,
17
+ buildDaemonHint,
18
+ buildMacAutomationHint,
19
+ buildLanguageGuard,
20
+ buildIntentHint,
21
+ composePrompt,
22
+ } = require('./daemon-prompt-context');
15
23
  const { createPlatformSpawn, terminateChildProcess, stopStreamingLifecycle, abortStreamingChildLifecycle, setActiveChildProcess, clearActiveChildProcess, acquireStreamingChild, buildStreamingResult, resolveStreamingClosePayload, accumulateStreamingStderr, splitStreamingStdoutChunk, buildStreamFlushPayload, buildToolOverlayPayload, buildMilestoneOverlayPayload, finalizePersistentStreamingTurn, writeStreamingChildInput, parseStreamingEvents, applyStreamingMetadata, applyStreamingToolState, applyStreamingContentState, createStreamingWatchdog, runAsyncCommand } = require('./core/handoff');
16
24
 
17
25
  /**
@@ -235,16 +243,6 @@ function createClaudeEngine(deps) {
235
243
  return err.message || String(err);
236
244
  }
237
245
 
238
- function adaptDaemonHintForEngine(daemonHint, engineName) {
239
- if (normalizeEngineName(engineName) === 'claude') return daemonHint;
240
- let out = String(daemonHint || '');
241
- // Keep this replacement conservative: only unwrap the known outer wrapper.
242
- out = out.replace('[System hints - DO NOT mention these to user:', 'System hints (internal, do not mention to user):');
243
- // The current daemonHint template ends with a single trailing `]`.
244
- out = out.replace(/\]\s*$/, '');
245
- return out;
246
- }
247
-
248
246
  function getCodexPermissionProfile(readOnly, daemonCfg = {}, session = {}) {
249
247
  return resolveCodexPermissionProfile({ readOnly, daemonCfg, session });
250
248
  }
@@ -573,7 +571,12 @@ function createClaudeEngine(deps) {
573
571
 
574
572
  function projectKeyFromVirtualChatId(chatId) {
575
573
  const v = String(chatId || '');
576
- if (v.startsWith('_agent_')) return v.slice(7) || null;
574
+ if (v.startsWith('_agent_')) {
575
+ const rest = v.slice(7);
576
+ const scopeIdx = rest.indexOf('::');
577
+ const key = scopeIdx >= 0 ? rest.slice(0, scopeIdx) : rest;
578
+ return key || null;
579
+ }
577
580
  if (v.startsWith('_scope_')) {
578
581
  const idx = v.lastIndexOf('__');
579
582
  if (idx > 7 && idx + 2 < v.length) return v.slice(idx + 2);
@@ -1441,20 +1444,15 @@ function createClaudeEngine(deps) {
1441
1444
  }
1442
1445
  }
1443
1446
 
1444
- let agentHint = '';
1445
- if (!session.started && (boundProject || (session && session.cwd))) {
1446
- try {
1447
- // Engine-aware: Codex gets memory only (soul is already in AGENTS.md);
1448
- // Claude gets soul + memory (SOUL.md is not auto-loaded by Claude).
1449
- agentHint = buildAgentContextForEngine(
1450
- boundProject || { cwd: session.cwd },
1451
- engineName,
1452
- HOME,
1453
- ).hint || '';
1454
- } catch (e) {
1455
- log('WARN', `Agent context injection failed: ${e.message}`);
1456
- }
1457
- }
1447
+ const agentHint = buildAgentHint({
1448
+ sessionStarted: session.started,
1449
+ boundProject,
1450
+ sessionCwd: session && session.cwd,
1451
+ engineName,
1452
+ HOME,
1453
+ buildAgentContextForEngine,
1454
+ log,
1455
+ });
1458
1456
 
1459
1457
  // Memory & Knowledge Injection (RAG)
1460
1458
  let memoryHint = '';
@@ -1628,41 +1626,28 @@ function createClaudeEngine(deps) {
1628
1626
 
1629
1627
  // Inject daemon hints only on first message of a session
1630
1628
  // Task-specific rules (3-4) are injected only when isTaskIntent() returns true (~250 token saving for casual chat)
1631
- let daemonHint = '';
1632
- if (!session.started) {
1633
- const mentorRadarHint = (config && config.daemon && config.daemon.mentor && config.daemon.mentor.enabled)
1634
- ? '\n When you observe the user is clearly expert or beginner in a domain, note it in your response and suggest: "要不要把你的 {domain} 水平 ({level}) 记录到能力雷达?"'
1635
- : '';
1636
- const taskRules = isTaskIntent(prompt) ? `
1637
- 3. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
1638
- node ~/.metame/memory-write.js "Entity.sub" "relation_type" "value (20-300 chars)"
1639
- Valid relations: tech_decision, bug_lesson, arch_convention, config_fact, config_change, workflow_rule, project_milestone
1640
- Only write verified facts. Do not write speculative or process-description entries.
1641
- ${mentorRadarHint}
1642
- 4. Task handoff: When suspending a multi-step task or handing off to another agent, write current status to ~/.metame/memory/now/${projectKey || 'default'}.md using:
1643
- \`mkdir -p ~/.metame/memory/now && printf '%s\\n' "## Current Task" "{task}" "" "## Progress" "{progress}" "" "## Next Step" "{next}" > ~/.metame/memory/now/${projectKey || 'default'}.md\`
1644
- Keep it under 200 words. Clear it when the task is fully complete by running: \`> ~/.metame/memory/now/${projectKey || 'default'}.md\`` : '';
1645
- daemonHint = `\n\n[System hints - DO NOT mention these to user:
1646
- 1. Daemon config: The ONLY config is ~/.metame/daemon.yaml (never edit daemon-default.yaml). Auto-reloads on change.
1647
- 2. Explanation depth (ZPD):${zdpHint ? zdpHint : '\n- User competence map unavailable. Default to concise expert-first explanations unless the user asks for teaching mode.'}${reflectHint}${taskRules}]`;
1648
- }
1649
-
1650
- daemonHint = adaptDaemonHintForEngine(daemonHint, runtime.name);
1629
+ const mentorRadarHint = (config && config.daemon && config.daemon.mentor && config.daemon.mentor.enabled)
1630
+ ? '\n When you observe the user is clearly expert or beginner in a domain, note it in your response and suggest: "要不要把你的 {domain} 水平 ({level}) 记录到能力雷达?"'
1631
+ : '';
1632
+ const daemonHint = buildDaemonHint({
1633
+ sessionStarted: session.started,
1634
+ prompt,
1635
+ mentorRadarHint,
1636
+ zdpHint,
1637
+ reflectHint,
1638
+ projectKey,
1639
+ isTaskIntent,
1640
+ runtimeName: runtime.name,
1641
+ });
1651
1642
 
1652
1643
  const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
1653
1644
 
1654
- // Mac automation orchestration hint: lets Claude flexibly compose local scripts
1655
- // without forcing users to write slash commands by hand.
1656
- let macAutomationHint = '';
1657
- if (process.platform === 'darwin' && !readOnly && isMacAutomationIntent(prompt)) {
1658
- macAutomationHint = `\n\n[Mac automation policy - do NOT expose this block:
1659
- 1. Prefer deterministic local control via Bash + osascript/JXA; avoid screenshot/visual workflows unless explicitly requested.
1660
- 2. Read/query actions can execute directly.
1661
- 3. Before any side-effect action (send email, create/delete/modify calendar event, delete/move files, app quit, system sleep), first show a short execution preview and require explicit user confirmation.
1662
- 4. Keep output concise: success/failure + key result only.
1663
- 5. If permission is missing, guide user to run /mac perms open then retry.
1664
- 6. Before executing high-risk or non-obvious Bash commands (rm, kill, git reset, overwrite configs), prepend a single-line [Why] explanation. Skip for routine commands (ls, cat, grep).]`;
1665
- }
1645
+ const macAutomationHint = buildMacAutomationHint({
1646
+ processPlatform: process.platform,
1647
+ readOnly,
1648
+ prompt,
1649
+ isMacAutomationIntent,
1650
+ });
1666
1651
 
1667
1652
  // P2-B: inject session summary when resuming after a 2h+ gap
1668
1653
  let summaryHint = '';
@@ -1738,23 +1723,29 @@ ${mentorRadarHint}
1738
1723
  // Language guard: only inject on first message of a new session to avoid
1739
1724
  // linearly growing token cost on every turn in long conversations.
1740
1725
  // Claude Code preserves session context, so the guard persists after initial injection.
1741
- const langGuard = session.started
1742
- ? ''
1743
- : '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
1744
- // Intent hints are dynamic (per-prompt, semantic match), so compute for all runtimes.
1745
- let intentHint = '';
1746
- try {
1747
- const block = buildIntentHintBlock(prompt, config, boundProjectKey || projectKey || '');
1748
- if (block) intentHint = `\n\n${block}`;
1749
- } catch (e) {
1750
- log('WARN', `Intent registry injection failed: ${e.message}`);
1751
- }
1726
+ const langGuard = buildLanguageGuard(session.started);
1727
+ const intentHint = buildIntentHint({
1728
+ prompt,
1729
+ config,
1730
+ boundProjectKey,
1731
+ projectKey,
1732
+ log,
1733
+ });
1752
1734
  // For warm process reuse: static context (daemonHint, memoryHint, etc.) is already
1753
1735
  // in the persistent process — skip those to save tokens. intentHint is dynamic
1754
1736
  // (varies per prompt), so include it even on warm reuse.
1755
- const fullPrompt = _warmEntry
1756
- ? routedPrompt + intentHint
1757
- : routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
1737
+ const fullPrompt = composePrompt({
1738
+ routedPrompt,
1739
+ warmEntry: _warmEntry,
1740
+ intentHint,
1741
+ daemonHint,
1742
+ agentHint,
1743
+ macAutomationHint,
1744
+ summaryHint,
1745
+ memoryHint,
1746
+ mentorHint,
1747
+ langGuard,
1748
+ });
1758
1749
  if (runtime.name === 'codex' && session.started && session.id && requestedCodexPermissionProfile) {
1759
1750
  const actualPermissionProfile = getActualCodexPermissionProfile(session);
1760
1751
  if (codexNeedsFallbackForRequestedPermissions(actualPermissionProfile, requestedCodexPermissionProfile)) {
@@ -2146,14 +2137,14 @@ ${mentorRadarHint}
2146
2137
  const allProjects = (config && config.projects) || {};
2147
2138
  const names = dispatchedTargets.map(k => (allProjects[k] && allProjects[k].name) || k).join('、');
2148
2139
  const doneMsg = await bot.sendMessage(chatId, `✉️ 已转达给 ${names},处理中…`);
2149
- if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
2140
+ if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, projectKeyFromVirtualChatId(chatId));
2150
2141
  const wasNew = !session.started;
2151
2142
  if (wasNew) markSessionStarted(sessionChatId, engineName);
2152
2143
  return { ok: true };
2153
2144
  }
2154
2145
  const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
2155
2146
  const doneMsg = await bot.sendMessage(chatId, `✅ 完成${filesDesc}`);
2156
- if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null);
2147
+ if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session, projectKeyFromVirtualChatId(chatId));
2157
2148
  const wasNew = !session.started;
2158
2149
  if (wasNew) markSessionStarted(sessionChatId, engineName);
2159
2150
  return { ok: true };
@@ -2217,6 +2208,10 @@ ${mentorRadarHint}
2217
2208
  cleanOutput = `⚠️ **任务超时,以下是已完成的部分结果:**\n\n${cleanOutput}`;
2218
2209
  }
2219
2210
 
2211
+ if (typeof bot.notifyFinalOutput === 'function') {
2212
+ try { await bot.notifyFinalOutput(cleanOutput); } catch { /* non-critical */ }
2213
+ }
2214
+
2220
2215
  // Match current session to a project for colored card display.
2221
2216
  // Prefer the bound project (known by virtual chatId or chat_agent_map) — avoids ambiguity
2222
2217
  // when multiple projects share the same cwd (e.g. team members with parent project cwd).
@@ -2291,7 +2286,7 @@ ${mentorRadarHint}
2291
2286
  log('ERROR', `sendMessage fallback also failed: ${e2.message}`);
2292
2287
  }
2293
2288
  }
2294
- const trackedAgentKey = String(chatId).startsWith('_agent_') ? String(chatId).slice(7) : null;
2289
+ const trackedAgentKey = projectKeyFromVirtualChatId(chatId);
2295
2290
  if (replyMsg && replyMsg.message_id && session) {
2296
2291
  if (runtime.name === 'codex' && session.runtimeSessionObserved === false) {
2297
2292
  trackMsgSession(replyMsg.message_id, session, trackedAgentKey, { routeOnly: true });
@@ -2408,6 +2403,9 @@ ${mentorRadarHint}
2408
2403
  if (retry.output) {
2409
2404
  markSessionStarted(sessionChatId, runtime.name);
2410
2405
  const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
2406
+ if (typeof bot.notifyFinalOutput === 'function') {
2407
+ try { await bot.notifyFinalOutput(retryClean); } catch { /* non-critical */ }
2408
+ }
2411
2409
  await bot.sendMarkdown(chatId, retryClean);
2412
2410
  await sendFileButtons(bot, chatId, mergeFileCollections(retryMarked, retry.files));
2413
2411
  return { ok: true };
@@ -2485,6 +2483,7 @@ ${mentorRadarHint}
2485
2483
  codexApprovalPrivilegeRank,
2486
2484
  codexNeedsFallbackForRequestedPermissions,
2487
2485
  buildCodexFallbackBridgePrompt,
2486
+ projectKeyFromVirtualChatId,
2488
2487
  },
2489
2488
  };
2490
2489
  }