metame-cli 1.4.34 → 1.5.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.
Files changed (48) hide show
  1. package/README.md +136 -94
  2. package/index.js +312 -57
  3. package/package.json +8 -4
  4. package/scripts/agent-layer.js +320 -0
  5. package/scripts/daemon-admin-commands.js +328 -28
  6. package/scripts/daemon-agent-commands.js +145 -6
  7. package/scripts/daemon-agent-tools.js +163 -7
  8. package/scripts/daemon-bridges.js +110 -20
  9. package/scripts/daemon-checkpoints.js +36 -7
  10. package/scripts/daemon-claude-engine.js +849 -358
  11. package/scripts/daemon-command-router.js +31 -10
  12. package/scripts/daemon-default.yaml +28 -4
  13. package/scripts/daemon-engine-runtime.js +328 -0
  14. package/scripts/daemon-exec-commands.js +15 -7
  15. package/scripts/daemon-notify.js +37 -1
  16. package/scripts/daemon-ops-commands.js +8 -6
  17. package/scripts/daemon-runtime-lifecycle.js +129 -5
  18. package/scripts/daemon-session-commands.js +60 -25
  19. package/scripts/daemon-session-store.js +121 -13
  20. package/scripts/daemon-task-scheduler.js +129 -49
  21. package/scripts/daemon-user-acl.js +35 -9
  22. package/scripts/daemon.js +268 -33
  23. package/scripts/distill.js +327 -18
  24. package/scripts/docs/agent-guide.md +12 -0
  25. package/scripts/docs/maintenance-manual.md +155 -0
  26. package/scripts/docs/pointer-map.md +110 -0
  27. package/scripts/feishu-adapter.js +42 -13
  28. package/scripts/hooks/stop-session-capture.js +243 -0
  29. package/scripts/memory-extract.js +105 -6
  30. package/scripts/memory-nightly-reflect.js +199 -11
  31. package/scripts/memory.js +134 -3
  32. package/scripts/mentor-engine.js +405 -0
  33. package/scripts/platform.js +24 -0
  34. package/scripts/providers.js +182 -22
  35. package/scripts/schema.js +12 -0
  36. package/scripts/session-analytics.js +245 -12
  37. package/scripts/skill-changelog.js +245 -0
  38. package/scripts/skill-evolution.js +288 -5
  39. package/scripts/telegram-adapter.js +12 -8
  40. package/scripts/usage-classifier.js +1 -1
  41. package/scripts/daemon-admin-commands.test.js +0 -333
  42. package/scripts/daemon-task-envelope.test.js +0 -59
  43. package/scripts/daemon-task-scheduler.test.js +0 -106
  44. package/scripts/reliability-core.test.js +0 -280
  45. package/scripts/skill-evolution.test.js +0 -113
  46. package/scripts/task-board.test.js +0 -83
  47. package/scripts/test_daemon.js +0 -1407
  48. package/scripts/utils.test.js +0 -192
@@ -5,6 +5,10 @@ const {
5
5
  CORE_USAGE_CATEGORIES,
6
6
  USAGE_CATEGORY_LABEL,
7
7
  } = require('./usage-classifier');
8
+ const { IS_WIN } = require('./platform');
9
+ const { ENGINE_MODEL_CONFIG } = require('./daemon-engine-runtime');
10
+ let mentorEngine = null;
11
+ try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
8
12
 
9
13
  function createAdminCommandHandler(deps) {
10
14
  const {
@@ -30,6 +34,9 @@ function createAdminCommandHandler(deps) {
30
34
  getMessageQueue,
31
35
  loadState,
32
36
  saveState,
37
+ getDefaultEngine = () => 'claude',
38
+ setDefaultEngine = () => {},
39
+ getDistillModel = () => 'haiku',
33
40
  } = deps;
34
41
 
35
42
  function resolveProjectKey(targetName, projects) {
@@ -92,6 +99,61 @@ function createAdminCommandHandler(deps) {
92
99
  return 'unspecified';
93
100
  }
94
101
 
102
+ function modeFromLevel(level) {
103
+ const n = Number(level);
104
+ if (!Number.isFinite(n)) return 'gentle';
105
+ if (n >= 8) return 'intense';
106
+ if (n >= 4) return 'active';
107
+ return 'gentle';
108
+ }
109
+
110
+ function parseDistillModelIntent(input) {
111
+ const text = String(input || '').trim();
112
+ if (!text || text.startsWith('/')) return null;
113
+ if (!/(蒸馏|distill|提炼|提纯)/i.test(text)) return null;
114
+ const setVerb = '(?:改成|改为|设为|设置|切到|切换到|换成|改用|使用|用|set|switch|use)';
115
+ if (!(new RegExp(setVerb, 'i')).test(text)) return null;
116
+
117
+ const explicitModel = text.match(new RegExp(`(?:蒸馏模型|模型|distill\\s*model|model)\\s*(?:${setVerb}|to|is)?\\s*[::]?\\s*([a-zA-Z0-9._-]{2,80})`, 'i'));
118
+ if (explicitModel) return { model: explicitModel[1] };
119
+
120
+ if (/(蒸馏模型|模型|distill\s*model|model)/i.test(text)) {
121
+ const quotedModel = text.match(/[“"'「]([a-zA-Z0-9._-]{2,80})[”"'」]/);
122
+ if (quotedModel) return { model: quotedModel[1] };
123
+ }
124
+
125
+ const knownToken = text.match(new RegExp(`${setVerb}\\s*(?:为|成|到|to)?\\s*[::]?\\s*(gpt-5\\.1-codex-mini|gpt-5-mini|haiku|sonnet|opus|5\\.1mini|5mini|codex-mini)\\b`, 'i'));
126
+ if (knownToken) return { model: knownToken[1] };
127
+
128
+ return null;
129
+ }
130
+
131
+ function ensureMentorConfig(cfg) {
132
+ if (!cfg.daemon) cfg.daemon = {};
133
+ if (!cfg.daemon.mentor || typeof cfg.daemon.mentor !== 'object') {
134
+ cfg.daemon.mentor = {};
135
+ }
136
+ const mentor = cfg.daemon.mentor;
137
+ if (typeof mentor.enabled !== 'boolean') mentor.enabled = false;
138
+ if (!Number.isFinite(Number(mentor.friction_level))) mentor.friction_level = 3;
139
+ if (!mentor.mode || !['gentle', 'active', 'intense'].includes(String(mentor.mode))) {
140
+ mentor.mode = modeFromLevel(mentor.friction_level);
141
+ }
142
+ if (!Array.isArray(mentor.exclude_agents)) mentor.exclude_agents = ['personal', 'xianyu'];
143
+ if (!Array.isArray(mentor.emotion_keywords_extra)) mentor.emotion_keywords_extra = [];
144
+ return mentor;
145
+ }
146
+
147
+ function hasCli(execSyncFn, bin) {
148
+ try {
149
+ const cmd = process.platform === 'win32' ? `where ${bin}` : `which ${bin}`;
150
+ execSyncFn(cmd, { encoding: 'utf8', ...(process.platform === 'win32' ? { windowsHide: true } : {}) });
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
95
157
  async function handleAdminCommand(ctx) {
96
158
  const { bot, chatId, text } = ctx;
97
159
  const state = ctx.state || {};
@@ -195,17 +257,69 @@ function createAdminCommandHandler(deps) {
195
257
  return { handled: true, config };
196
258
  }
197
259
 
260
+ // /skill-evo approve <id> — approve a workflow_proposal and dispatch skill creation
261
+ const approveMatch = arg.match(/^approve\s+(\S+)$/i);
262
+ if (approveMatch) {
263
+ const id = approveMatch[1];
264
+ // Find the queue item (search both pending and notified states)
265
+ const item = skillEvolution.listQueueItems({ status: ['pending', 'notified'], limit: 200 })
266
+ .find(i => i.id === id && i.type === 'workflow_proposal');
267
+ if (!item) {
268
+ await bot.sendMessage(chatId, `❌ 未找到 workflow_proposal: ${id}`);
269
+ return { handled: true, config };
270
+ }
271
+ // Build skill-creator prefilled prompt
272
+ const toolsSig = (item.tools_signature || []).join(', ');
273
+ const prefilledPrompt = [
274
+ '/skill-creator',
275
+ `创建一个新技能,自动化以下工作流:`,
276
+ `工作流模式: ${item.search_hint || item.reason}`,
277
+ toolsSig ? `常用工具: ${toolsSig}` : '',
278
+ item.example_prompt ? `用户示例: "${item.example_prompt}"` : '',
279
+ `该技能应封装这个多步工作流为单一可调用技能。`,
280
+ ].filter(Boolean).join('\n');
281
+ // Dispatch to metame agent for skill creation (async — must not block event loop)
282
+ try {
283
+ const HOME = require('os').homedir();
284
+ const dispatchBin = require('path').join(HOME, '.metame', 'bin', 'dispatch_to');
285
+ const { execFile } = require('child_process');
286
+ const { promisify } = require('util');
287
+ const execFileAsync = promisify(execFile);
288
+ // dispatch_to is a Node.js script; on Windows shebang resolution is unavailable,
289
+ // so invoke via node explicitly for cross-platform safety
290
+ const cmd = IS_WIN ? process.execPath : dispatchBin;
291
+ const cmdArgs = IS_WIN ? [dispatchBin, 'metame', prefilledPrompt] : ['metame', prefilledPrompt];
292
+ await execFileAsync(cmd, cmdArgs, { encoding: 'utf8', timeout: 15000 });
293
+ // Mark installed only after successful dispatch
294
+ skillEvolution.resolveQueueItemById(id, 'installed');
295
+ await bot.sendMessage(chatId, `✅ 已派发给 Jarvis 创建技能,完成后会通知你\n工作流: ${item.search_hint || item.reason}`);
296
+ } catch (e) {
297
+ // Dispatch failed — don't mark installed, keep in queue
298
+ await bot.sendMessage(chatId, `⚠️ 自动派发失败: ${e.message}\n提案仍在队列中,可重试: /skill-evo approve ${id}`);
299
+ }
300
+ return { handled: true, config };
301
+ }
302
+
198
303
  const dismissMatch = arg.match(/^(?:dismiss|skip|ignored?)\s+(\S+)$/i);
199
304
  if (dismissMatch) {
200
305
  const id = dismissMatch[1];
306
+ // Check if this is a workflow_proposal — if so, reset the sketch
307
+ const item = skillEvolution.listQueueItems({ status: ['pending', 'notified'], limit: 200 })
308
+ .find(i => i.id === id);
201
309
  const ok = skillEvolution.resolveQueueItemById
202
310
  ? skillEvolution.resolveQueueItemById(id, 'dismissed')
203
311
  : false;
312
+ // Reset workflow sketch so it can re-accumulate
313
+ if (ok && item && item.type === 'workflow_proposal' && item.workflow_sketch_id) {
314
+ if (skillEvolution.resetWorkflowSketch) {
315
+ skillEvolution.resetWorkflowSketch(item.workflow_sketch_id);
316
+ }
317
+ }
204
318
  await bot.sendMessage(chatId, ok ? `✅ 已标记 dismissed: ${id}` : `❌ 未找到可处理项: ${id}`);
205
319
  return { handled: true, config };
206
320
  }
207
321
 
208
- await bot.sendMessage(chatId, '用法: /skill-evo list | /skill-evo done <id> | /skill-evo dismiss <id>');
322
+ await bot.sendMessage(chatId, '用法: /skill-evo list | /skill-evo done <id> | /skill-evo dismiss <id> | /skill-evo approve <id>');
209
323
  return { handled: true, config };
210
324
  }
211
325
 
@@ -635,6 +749,68 @@ function createAdminCommandHandler(deps) {
635
749
  return { handled: true, config };
636
750
  }
637
751
 
752
+ if (text === '/mentor' || text.startsWith('/mentor ')) {
753
+ try {
754
+ backupConfig();
755
+ const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
756
+ const mentorCfg = ensureMentorConfig(cfg);
757
+ const arg = text.slice('/mentor'.length).trim();
758
+
759
+ if (!arg || arg === 'status') {
760
+ const status = mentorEngine && typeof mentorEngine.getRuntimeStatus === 'function'
761
+ ? mentorEngine.getRuntimeStatus()
762
+ : { debt_count: 0, cooldown_remaining_ms: 0 };
763
+ const mode = String(mentorCfg.mode || modeFromLevel(mentorCfg.friction_level));
764
+ const level = Number(mentorCfg.friction_level || 0);
765
+ const cooldownSec = Math.ceil((Number(status.cooldown_remaining_ms) || 0) / 1000);
766
+ const lines = [
767
+ `Mentor: ${mentorCfg.enabled ? 'ON' : 'OFF'}`,
768
+ `Mode: ${mode}`,
769
+ `Friction level: ${level}`,
770
+ `Debts: ${status.debt_count || 0}`,
771
+ `Emotion cooldown: ${cooldownSec > 0 ? `${cooldownSec}s` : '0s'}`,
772
+ 'Zone: n/a (runtime)',
773
+ ];
774
+ await bot.sendMessage(chatId, lines.join('\n'));
775
+ return { handled: true, config };
776
+ }
777
+
778
+ if (arg === 'on' || arg === 'off') {
779
+ mentorCfg.enabled = arg === 'on';
780
+ writeConfigSafe(cfg);
781
+ config = loadConfig();
782
+ await bot.sendMessage(chatId, mentorCfg.enabled
783
+ ? '✅ Mentor mode enabled.'
784
+ : '✅ Mentor mode disabled.');
785
+ return { handled: true, config };
786
+ }
787
+
788
+ const mLevel = arg.match(/^level\s+(-?\d{1,2})$/i);
789
+ if (mLevel) {
790
+ let level = Number(mLevel[1]);
791
+ if (!Number.isFinite(level)) level = 3;
792
+ level = Math.max(0, Math.min(10, Math.floor(level)));
793
+ mentorCfg.friction_level = level;
794
+ mentorCfg.mode = modeFromLevel(level);
795
+ writeConfigSafe(cfg);
796
+ config = loadConfig();
797
+ await bot.sendMessage(chatId, `✅ Mentor level set to ${level} (${mentorCfg.mode}).`);
798
+ return { handled: true, config };
799
+ }
800
+
801
+ await bot.sendMessage(chatId, [
802
+ '用法:',
803
+ '/mentor on',
804
+ '/mentor off',
805
+ '/mentor level <0-10>',
806
+ '/mentor status',
807
+ ].join('\n'));
808
+ } catch (e) {
809
+ await bot.sendMessage(chatId, `❌ Mentor command failed: ${e.message}`);
810
+ }
811
+ return { handled: true, config };
812
+ }
813
+
638
814
  if (text === '/reload') {
639
815
  if (global._metameReload) {
640
816
  const r = global._metameReload();
@@ -727,6 +903,10 @@ function createAdminCommandHandler(deps) {
727
903
  const validModels = ['sonnet', 'opus', 'haiku'];
728
904
  const checks = [];
729
905
  let issues = 0;
906
+ const activeProvider = providerMod && typeof providerMod.getActiveName === 'function'
907
+ ? providerMod.getActiveName()
908
+ : 'anthropic';
909
+ const isCustomProvider = activeProvider !== 'anthropic';
730
910
 
731
911
  let cfg = null;
732
912
  try {
@@ -738,20 +918,33 @@ function createAdminCommandHandler(deps) {
738
918
  }
739
919
 
740
920
  const m = (cfg && cfg.daemon && cfg.daemon.model) || 'opus';
741
- if (validModels.includes(m)) {
921
+ const modelOk = isCustomProvider
922
+ ? /^[a-zA-Z0-9._-]{2,80}$/.test(String(m || '').trim())
923
+ : validModels.includes(m);
924
+ if (modelOk) {
742
925
  checks.push(`✅ 模型: ${m}`);
743
926
  } else {
744
- checks.push(`❌ 模型: ${m} (无效)`);
927
+ checks.push(`❌ 模型: ${m} (${isCustomProvider ? '格式无效' : '无效'})`);
745
928
  issues++;
746
929
  }
747
930
 
748
- try {
749
- execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { encoding: 'utf8' });
750
- checks.push('✅ Claude CLI');
751
- } catch {
752
- checks.push('❌ Claude CLI 未找到');
931
+ const hasClaude = hasCli(execSync, 'claude');
932
+ const hasCodex = hasCli(execSync, 'codex');
933
+ checks.push(hasClaude ? '✅ Claude CLI' : '⚠️ Claude CLI 未找到');
934
+ checks.push(hasCodex ? '✅ Codex CLI' : '⚠️ Codex CLI 未找到');
935
+
936
+ const currentEngine = getDefaultEngine() === 'codex' ? 'codex' : 'claude';
937
+ if (currentEngine === 'claude' && !hasClaude) {
938
+ checks.push('❌ 当前默认引擎是 claude,但 Claude CLI 不可用');
753
939
  issues++;
754
940
  }
941
+ if (currentEngine === 'codex' && !hasCodex) {
942
+ checks.push('❌ 当前默认引擎是 codex,但 Codex CLI 不可用');
943
+ issues++;
944
+ }
945
+
946
+ checks.push(`✅ 默认引擎: ${currentEngine}`);
947
+ checks.push(`✅ Provider: ${activeProvider}${isCustomProvider ? ' (custom)' : ''}`);
755
948
 
756
949
  const bakFile = CONFIG_FILE + '.bak';
757
950
  const hasBak = fs.existsSync(bakFile);
@@ -795,38 +988,54 @@ function createAdminCommandHandler(deps) {
795
988
  return { handled: true, config };
796
989
  }
797
990
 
798
- // /model [name] — switch model (interactive, accepts any name for custom providers)
991
+ // /model [name] — switch session model, engine-aware
799
992
  if (text === '/model' || text.startsWith('/model ')) {
800
993
  const arg = text.slice(6).trim();
801
- const builtinModels = ['sonnet', 'opus', 'haiku'];
802
- const currentModel = (config.daemon && config.daemon.model) || 'opus';
803
- const activeProvider = providerMod ? providerMod.getActiveName() : 'anthropic';
804
- const isCustomProvider = activeProvider !== 'anthropic';
994
+ const currentEngine = getDefaultEngine();
995
+ const engineCfg = ENGINE_MODEL_CONFIG[currentEngine] || ENGINE_MODEL_CONFIG.claude;
996
+ // options is [{value, label}, ...] normalize to a flat list for logic
997
+ const optionEntries = (engineCfg.options || []).map(o =>
998
+ typeof o === 'string' ? { value: o, label: o } : o
999
+ );
1000
+ const optionValues = optionEntries.map(o => o.value);
1001
+ const daemonCfg = config.daemon || {};
1002
+ const currentModel = (daemonCfg.models && daemonCfg.models[currentEngine])
1003
+ || daemonCfg.model // legacy fallback
1004
+ || engineCfg.main;
1005
+ // providerMod manages Claude providers only — for codex use engineCfg.provider
1006
+ const activeProvider = (currentEngine === 'claude' && providerMod)
1007
+ ? providerMod.getActiveName()
1008
+ : engineCfg.provider;
1009
+ const isBuiltinProvider = activeProvider === engineCfg.provider;
1010
+ const distillModel = getDistillModel();
1011
+ const hintLine = engineCfg.hint ? `\n💡 ${engineCfg.hint}` : (!isBuiltinProvider ? `\n💡 ${activeProvider} 可输入任意模型名` : '');
805
1012
 
806
1013
  if (!arg) {
807
- const hint = isCustomProvider ? `\n💡 ${activeProvider} 可输入任意模型名` : '';
808
- if (bot.sendButtons) {
809
- const buttons = builtinModels.map(m => [{
810
- text: m === currentModel ? `${m} ✓` : m,
811
- callback_data: `/model ${m}`,
1014
+ const statusLine = `🤖 [${currentEngine}] 会话模型: ${currentModel} Provider: ${activeProvider}\n🧪 后台轻量: ${distillModel} (/distill-model 修改)${hintLine}`;
1015
+ if (bot.sendButtons && optionEntries.length > 0) {
1016
+ const buttons = optionEntries.map(({ value, label }) => [{
1017
+ text: value === currentModel ? `${label} ✓` : label,
1018
+ callback_data: `/model ${value}`,
812
1019
  }]);
813
- await bot.sendButtons(chatId, `🤖 当前模型: ${currentModel}${hint}`, buttons);
1020
+ await bot.sendButtons(chatId, statusLine, buttons);
814
1021
  } else {
815
- await bot.sendMessage(chatId, `🤖 当前模型: ${currentModel}\n可选: ${builtinModels.join(', ')}${hint}`);
1022
+ const optionHint = optionValues.length > 0 ? `\n\n可选: ${optionValues.join(', ')}` : '';
1023
+ await bot.sendMessage(chatId, `${statusLine}${optionHint}`);
816
1024
  }
817
1025
  return { handled: true, config };
818
1026
  }
819
1027
 
820
1028
  const normalizedArg = arg.toLowerCase();
821
- // Builtin providers only accept builtin model names
822
- if (!isCustomProvider && !builtinModels.includes(normalizedArg)) {
823
- await bot.sendMessage(chatId, `❌ 无效模型: ${arg}\n可选: ${builtinModels.join(', ')}\n💡 切换到自定义 provider 后可用任意模型名`);
1029
+ // Only restrict model names for claude with anthropic provider (known fixed set)
1030
+ // codex always allows free-form input (OpenAI models change frequently)
1031
+ if (currentEngine === 'claude' && isBuiltinProvider && !optionValues.includes(normalizedArg)) {
1032
+ await bot.sendMessage(chatId, `❌ 无效模型: ${arg}\n可选: ${optionValues.join(', ')}\n💡 切换到自定义 provider 后可用任意模型名`);
824
1033
  return { handled: true, config };
825
1034
  }
826
1035
 
827
- const modelName = builtinModels.includes(normalizedArg) ? normalizedArg : arg;
1036
+ const modelName = optionValues.includes(normalizedArg) ? normalizedArg : arg;
828
1037
  if (modelName === currentModel) {
829
- await bot.sendMessage(chatId, `🤖 已经是 ${modelName}`);
1038
+ await bot.sendMessage(chatId, `🤖 已经是 ${modelName}(后台轻量模型: ${distillModel})`);
830
1039
  return { handled: true, config };
831
1040
  }
832
1041
 
@@ -834,10 +1043,11 @@ function createAdminCommandHandler(deps) {
834
1043
  backupConfig();
835
1044
  const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
836
1045
  if (!cfg.daemon) cfg.daemon = {};
837
- cfg.daemon.model = modelName;
1046
+ if (!cfg.daemon.models) cfg.daemon.models = {};
1047
+ cfg.daemon.models[currentEngine] = modelName;
838
1048
  writeConfigSafe(cfg);
839
1049
  config = loadConfig();
840
- await bot.sendMessage(chatId, `✅ 模型已切换: ${currentModel} → ${modelName}`);
1050
+ await bot.sendMessage(chatId, `✅ [${currentEngine}] 会话模型: ${currentModel} → ${modelName}\n🧪 后台轻量模型: ${distillModel}(如需修改用 /distill-model)`);
841
1051
  } catch (e) {
842
1052
  await bot.sendMessage(chatId, `❌ 切换失败: ${e.message}`);
843
1053
  }
@@ -867,10 +1077,100 @@ function createAdminCommandHandler(deps) {
867
1077
  return { handled: true, config };
868
1078
  }
869
1079
 
1080
+ // /engine [name] — show or switch default engine (claude/codex)
1081
+ // Switching engine auto-syncs: distill model + preferred provider (if available)
1082
+ if (text === '/engine' || text.startsWith('/engine ')) {
1083
+ const arg = text.slice('/engine'.length).trim().toLowerCase();
1084
+ if (!arg) {
1085
+ const cur = getDefaultEngine();
1086
+ const curEngineCfg = ENGINE_MODEL_CONFIG[cur] || ENGINE_MODEL_CONFIG.claude;
1087
+ const activeProvider = (cur === 'claude' && providerMod)
1088
+ ? providerMod.getActiveName()
1089
+ : curEngineCfg.provider;
1090
+ const distill = getDistillModel();
1091
+ const daemonCfg = config.daemon || {};
1092
+ const currentModel = (daemonCfg.models && daemonCfg.models[cur])
1093
+ || daemonCfg.model || curEngineCfg.main;
1094
+ await bot.sendMessage(chatId, [
1095
+ `🔧 引擎: ${cur} | Provider: ${activeProvider}`,
1096
+ `🤖 会话模型: ${currentModel} | 后台轻量: ${distill}`,
1097
+ '',
1098
+ '用法: /engine claude 或 /engine codex',
1099
+ '切换引擎将自动同步 distill model 和首选 provider',
1100
+ ].join('\n'));
1101
+ return { handled: true, config };
1102
+ }
1103
+ if (arg !== 'claude' && arg !== 'codex') {
1104
+ await bot.sendMessage(chatId, `❌ 不支持的引擎: ${arg}\n可选: claude, codex`);
1105
+ return { handled: true, config };
1106
+ }
1107
+
1108
+ const preferredProvider = (ENGINE_MODEL_CONFIG[arg] || {}).provider;
1109
+
1110
+ setDefaultEngine(arg); // syncs distill model + providerMod.setEngine (no longer resets session model)
1111
+ const distill = getDistillModel();
1112
+ const freshCfg = loadConfig();
1113
+ const freshDaemon = freshCfg.daemon || {};
1114
+ const targetEngineCfg = ENGINE_MODEL_CONFIG[arg] || ENGINE_MODEL_CONFIG.claude;
1115
+ const syncedModel = (freshDaemon.models && freshDaemon.models[arg])
1116
+ || freshDaemon.model || targetEngineCfg.main;
1117
+
1118
+ // Auto-switch provider if the preferred one exists in providers.yaml
1119
+ let providerNote = '';
1120
+ if (providerMod && preferredProvider) {
1121
+ try {
1122
+ providerMod.setActive(preferredProvider);
1123
+ providerNote = `\n🔌 Provider 已同步: ${preferredProvider}`;
1124
+ } catch {
1125
+ // Provider not configured — just inform
1126
+ const cur = providerMod ? providerMod.getActiveName() : '';
1127
+ providerNote = `\n🔌 Provider: ${cur}(如需切换请 /provider ${preferredProvider})`;
1128
+ }
1129
+ }
1130
+
1131
+ await bot.sendMessage(chatId, `✅ 引擎已切换: ${arg}\n🤖 会话模型: ${syncedModel}\n🧪 后台轻量模型: ${distill}${providerNote}`);
1132
+ return { handled: true, config };
1133
+ }
1134
+
1135
+ // /distill-model [name] — show or update distill model
1136
+ if (text === '/distill-model' || text.startsWith('/distill-model ')) {
1137
+ if (!providerMod || typeof providerMod.getDistillModel !== 'function' || typeof providerMod.setDistillModel !== 'function') {
1138
+ await bot.sendMessage(chatId, '❌ Distill model config is not available.');
1139
+ return { handled: true, config };
1140
+ }
1141
+ const arg = text.slice('/distill-model'.length).trim();
1142
+ if (!arg) {
1143
+ await bot.sendMessage(chatId, `🧪 当前蒸馏模型: ${providerMod.getDistillModel()}\n用法: /distill-model <model>\n示例: /distill-model gpt-5.1-codex-mini`);
1144
+ return { handled: true, config };
1145
+ }
1146
+ try {
1147
+ providerMod.setDistillModel(arg);
1148
+ await bot.sendMessage(chatId, `✅ 蒸馏模型已更新为: ${providerMod.getDistillModel()}`);
1149
+ } catch (e) {
1150
+ await bot.sendMessage(chatId, `❌ 设置失败: ${e.message}`);
1151
+ }
1152
+ return { handled: true, config };
1153
+ }
1154
+
1155
+ const nlDistillIntent = parseDistillModelIntent(text);
1156
+ if (nlDistillIntent) {
1157
+ if (!providerMod || typeof providerMod.setDistillModel !== 'function' || typeof providerMod.getDistillModel !== 'function') {
1158
+ await bot.sendMessage(chatId, '❌ Distill model config is not available.');
1159
+ return { handled: true, config };
1160
+ }
1161
+ try {
1162
+ providerMod.setDistillModel(nlDistillIntent.model);
1163
+ await bot.sendMessage(chatId, `✅ 已按自然语言请求更新蒸馏模型: ${providerMod.getDistillModel()}`);
1164
+ } catch (e) {
1165
+ await bot.sendMessage(chatId, `❌ 设置失败: ${e.message}`);
1166
+ }
1167
+ return { handled: true, config };
1168
+ }
1169
+
870
1170
  return { handled: false, config };
871
1171
  }
872
1172
 
873
- return { handleAdminCommand };
1173
+ return { handleAdminCommand, _private: { parseDistillModelIntent } };
874
1174
  }
875
1175
 
876
1176
  module.exports = { createAdminCommandHandler };
@@ -28,8 +28,26 @@ function createAgentCommandHandler(deps) {
28
28
  attachOrCreateSession,
29
29
  agentFlowTtlMs,
30
30
  agentBindTtlMs,
31
+ getDefaultEngine = () => 'claude',
31
32
  } = deps;
32
33
 
34
+ function normalizeEngineName(name) {
35
+ const n = String(name || '').trim().toLowerCase();
36
+ return n === 'codex' ? 'codex' : getDefaultEngine();
37
+ }
38
+
39
+ function inferEngineByCwd(cfg, cwd) {
40
+ if (!cfg || !cfg.projects || !cwd) return null;
41
+ const targetCwd = normalizeCwd(cwd);
42
+ for (const proj of Object.values(cfg.projects || {})) {
43
+ if (!proj || !proj.cwd) continue;
44
+ if (normalizeCwd(proj.cwd) === targetCwd) {
45
+ return normalizeEngineName(proj.engine);
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
33
51
  // Pending activations have no TTL — they persist until consumed.
34
52
  // The creating chatId is stored to prevent self-activation.
35
53
 
@@ -127,7 +145,12 @@ function createAgentCommandHandler(deps) {
127
145
  const action = res.data.isNewProject ? '绑定成功' : '重新绑定';
128
146
  const displayCwd = String(res.data.cwd || '').replace(HOME, '~');
129
147
  if (res.data.cwd && typeof attachOrCreateSession === 'function') {
130
- attachOrCreateSession(chatId, normalizeCwd(res.data.cwd), p.name || agentName || res.data.projectKey || '');
148
+ attachOrCreateSession(
149
+ chatId,
150
+ normalizeCwd(res.data.cwd),
151
+ p.name || agentName || res.data.projectKey || '',
152
+ p.engine || getDefaultEngine()
153
+ );
131
154
  }
132
155
  await bot.sendMessage(chatId, `${icon} ${p.name || agentName} ${action}\n目录: ${displayCwd}`);
133
156
  return { ok: true, data: res.data };
@@ -140,7 +163,7 @@ function createAgentCommandHandler(deps) {
140
163
  }
141
164
  const fallbackCwd = (fallback.data && fallback.data.cwd) || agentCwd;
142
165
  if (fallbackCwd && typeof attachOrCreateSession === 'function') {
143
- attachOrCreateSession(chatId, normalizeCwd(fallbackCwd), agentName || '');
166
+ attachOrCreateSession(chatId, normalizeCwd(fallbackCwd), agentName || '', getDefaultEngine());
144
167
  }
145
168
  return {
146
169
  ok: true,
@@ -163,9 +186,9 @@ function createAgentCommandHandler(deps) {
163
186
 
164
187
  async function createAgentViaUnifiedApi(chatId, name, dir, roleDesc, opts = {}) {
165
188
  // Default: skip binding the creating chat — let the target group activate via /activate
166
- const { skipChatBinding = true } = opts;
189
+ const { skipChatBinding = true, engine = null } = opts;
167
190
  if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
168
- const res = await agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId, { skipChatBinding });
191
+ const res = await agentTools.createNewWorkspaceAgent(name, dir, roleDesc, chatId, { skipChatBinding, engine });
169
192
  if (res.ok && skipChatBinding && res.data && res.data.projectKey) {
170
193
  storePendingActivation(res.data.projectKey, name, res.data.cwd, chatId);
171
194
  }
@@ -273,10 +296,14 @@ function createAgentCommandHandler(deps) {
273
296
  const cwd = fullMatch.projectPath || (getSession(chatId) && getSession(chatId).cwd) || HOME;
274
297
 
275
298
  const state2 = loadState();
299
+ const cfgForEngine = loadConfig();
300
+ const engineByTargetCwd = inferEngineByCwd(cfgForEngine, cwd);
301
+ const currentEngine = normalizeEngineName(state2.sessions[chatId] && state2.sessions[chatId].engine);
276
302
  state2.sessions[chatId] = {
277
303
  id: sessionId,
278
304
  cwd,
279
305
  started: true,
306
+ engine: engineByTargetCwd || currentEngine,
280
307
  };
281
308
  saveState(state2);
282
309
  const name = fullMatch.customTitle;
@@ -380,6 +407,10 @@ function createAgentCommandHandler(deps) {
380
407
  return true;
381
408
  }
382
409
  const cwd = normalizeCwd(boundProj.cwd);
410
+ // Lazy migration: ensure soul layer exists for agents created before this feature
411
+ if (agentTools && typeof agentTools.repairAgentSoul === 'function') {
412
+ await agentTools.repairAgentSoul(cwd).catch(() => {});
413
+ }
383
414
  const inlineDelta = agentParts.slice(1).join(' ').trim();
384
415
  if (inlineDelta) {
385
416
  await bot.sendMessage(chatId, '⏳ 正在更新 CLAUDE.md...');
@@ -443,6 +474,83 @@ function createAgentCommandHandler(deps) {
443
474
  return true;
444
475
  }
445
476
 
477
+
478
+ // /agent soul [repair | edit <text>]
479
+ // Manage the agent's soul.md identity file.
480
+ // "repair" → lazy-migration: create ~/.metame/agents/<id>/ and project symlinks.
481
+ // "edit" → overwrite soul.md with provided text.
482
+ // (default) → display current SOUL.md content.
483
+ if (agentSub === 'soul') {
484
+ const soulAction = agentParts[1] || '';
485
+ const cfg = loadConfig();
486
+ const { boundProj } = getBoundProject(chatId, cfg);
487
+ if (!boundProj || !boundProj.cwd) {
488
+ await bot.sendMessage(chatId, '❌ 当前群未绑定 Agent,请先 /agent bind <名称> <目录>');
489
+ return true;
490
+ }
491
+ const cwd = normalizeCwd(boundProj.cwd);
492
+ const soulPath = path.join(cwd, 'SOUL.md');
493
+
494
+ if (soulAction === 'repair') {
495
+ if (agentTools && typeof agentTools.repairAgentSoul === 'function') {
496
+ const res = await agentTools.repairAgentSoul(cwd);
497
+ if (!res.ok) {
498
+ await bot.sendMessage(chatId, '❌ Soul 修复失败: ' + res.error);
499
+ } else {
500
+ const viewModes = res.data.views
501
+ ? Object.entries(res.data.views).map(([k, v]) => k + ':' + v).join(', ')
502
+ : '—';
503
+ await bot.sendMessage(chatId, [
504
+ '✅ Agent Soul 层已就绪',
505
+ 'agent_id: ' + res.data.agentId,
506
+ '链接方式: ' + viewModes,
507
+ '',
508
+ '文件位置:',
509
+ ' SOUL.md → ~/.metame/agents/' + res.data.agentId + '/soul.md',
510
+ ' MEMORY.md → ~/.metame/agents/' + res.data.agentId + '/memory-snapshot.md',
511
+ ].join('\n'));
512
+ }
513
+ } else {
514
+ await bot.sendMessage(chatId, '❌ agentTools 不可用');
515
+ }
516
+ return true;
517
+ }
518
+
519
+ if (soulAction === 'edit') {
520
+ const soulText = agentParts.slice(2).join(' ').trim();
521
+ if (!soulText) {
522
+ await bot.sendMessage(chatId, '用法: /agent soul edit <新内容>\n当前内容: /agent soul');
523
+ return true;
524
+ }
525
+ try {
526
+ fs.writeFileSync(soulPath, soulText, 'utf8');
527
+ await bot.sendMessage(chatId, '✅ SOUL.md 已更新');
528
+ } catch (e) {
529
+ await bot.sendMessage(chatId, '❌ 写入失败: ' + e.message);
530
+ }
531
+ return true;
532
+ }
533
+
534
+ // Default: show current SOUL.md content
535
+ if (!fs.existsSync(soulPath)) {
536
+ await bot.sendMessage(chatId, [
537
+ '⚠️ SOUL.md 不存在',
538
+ '',
539
+ '老项目或刚绑定的 Agent 可能尚未建立 Soul 层。',
540
+ '运行 /agent soul repair 自动生成。',
541
+ ].join('\n'));
542
+ return true;
543
+ }
544
+ try {
545
+ const soulContent = fs.readFileSync(soulPath, 'utf8').trim().slice(0, 2000);
546
+ await bot.sendMessage(chatId, '📋 当前 Soul:\n\n' + soulContent);
547
+ } catch (e) {
548
+ await bot.sendMessage(chatId, '❌ 读取 SOUL.md 失败: ' + e.message);
549
+ }
550
+ return true;
551
+ }
552
+
553
+
446
554
  // /agent (no sub command): show agent switch picker
447
555
  {
448
556
  const projects = config.projects || {};
@@ -485,7 +593,36 @@ function createAgentCommandHandler(deps) {
485
593
  }
486
594
  }
487
595
  }
488
- // No pending activation at all guide to manual bind
596
+ // No pending activation fall back to scanning daemon.yaml for unbound projects
597
+ const allBoundKeys = new Set(Object.values({
598
+ ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
599
+ ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
600
+ }));
601
+ const unboundProjects = Object.entries(cfg.projects || {})
602
+ .filter(([key, p]) => p && p.cwd && !allBoundKeys.has(key))
603
+ .map(([key, p]) => ({ key, name: p.name || key, cwd: p.cwd, icon: p.icon || '🤖' }));
604
+
605
+ if (unboundProjects.length === 1) {
606
+ // Exactly one unbound project — auto-bind using project KEY (not display name)
607
+ // to ensure toProjectKey() resolves to the correct existing key in daemon.yaml
608
+ const proj = unboundProjects[0];
609
+ const bindRes2 = await bindViaUnifiedApi(bot, chatId, proj.key, proj.cwd);
610
+ if (bindRes2.ok) pendingActivations && pendingActivations.delete(proj.key);
611
+ return true;
612
+ }
613
+
614
+ if (unboundProjects.length > 1) {
615
+ // Multiple unbound projects — show pick list using project keys
616
+ const lines = ['请选择要激活的 Agent:', ''];
617
+ for (const p of unboundProjects) {
618
+ lines.push(`${p.icon} ${p.name} → \`/agent bind ${p.key} ${p.cwd}\``);
619
+ }
620
+ lines.push('\n发送对应命令即可绑定此群。');
621
+ await bot.sendMessage(chatId, lines.join('\n'));
622
+ return true;
623
+ }
624
+
625
+ // Truly nothing to activate
489
626
  await bot.sendMessage(chatId,
490
627
  '没有待激活的 Agent。\n\n如果已创建过 Agent,直接用:\n`/agent bind <名称> <目录>`\n即可绑定,不需要重新创建。'
491
628
  );
@@ -517,4 +654,6 @@ function createAgentCommandHandler(deps) {
517
654
  return { handleAgentCommand };
518
655
  }
519
656
 
520
- module.exports = { createAgentCommandHandler };
657
+ module.exports = {
658
+ createAgentCommandHandler,
659
+ };