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.
- package/README.md +136 -94
- package/index.js +312 -57
- package/package.json +8 -4
- package/scripts/agent-layer.js +320 -0
- package/scripts/daemon-admin-commands.js +328 -28
- package/scripts/daemon-agent-commands.js +145 -6
- package/scripts/daemon-agent-tools.js +163 -7
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-checkpoints.js +36 -7
- package/scripts/daemon-claude-engine.js +849 -358
- package/scripts/daemon-command-router.js +31 -10
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +328 -0
- package/scripts/daemon-exec-commands.js +15 -7
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-ops-commands.js +8 -6
- package/scripts/daemon-runtime-lifecycle.js +129 -5
- package/scripts/daemon-session-commands.js +60 -25
- package/scripts/daemon-session-store.js +121 -13
- package/scripts/daemon-task-scheduler.js +129 -49
- package/scripts/daemon-user-acl.js +35 -9
- package/scripts/daemon.js +268 -33
- package/scripts/distill.js +327 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +155 -0
- package/scripts/docs/pointer-map.md +110 -0
- package/scripts/feishu-adapter.js +42 -13
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +105 -6
- package/scripts/memory-nightly-reflect.js +199 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +24 -0
- package/scripts/providers.js +182 -22
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/telegram-adapter.js +12 -8
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- 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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|
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
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
const
|
|
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
|
|
808
|
-
if (bot.sendButtons) {
|
|
809
|
-
const buttons =
|
|
810
|
-
text:
|
|
811
|
-
callback_data: `/model ${
|
|
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,
|
|
1020
|
+
await bot.sendButtons(chatId, statusLine, buttons);
|
|
814
1021
|
} else {
|
|
815
|
-
|
|
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
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
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 =
|
|
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.
|
|
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, `✅
|
|
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(
|
|
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
|
|
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 = {
|
|
657
|
+
module.exports = {
|
|
658
|
+
createAgentCommandHandler,
|
|
659
|
+
};
|