metame-cli 1.4.34 → 1.5.0
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 +146 -32
- package/index.js +148 -9
- package/package.json +6 -3
- package/scripts/daemon-admin-commands.js +254 -9
- package/scripts/daemon-agent-commands.js +64 -6
- package/scripts/daemon-agent-tools.js +26 -5
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-claude-engine.js +698 -239
- package/scripts/daemon-command-router.js +24 -8
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +275 -0
- package/scripts/daemon-exec-commands.js +10 -4
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-runtime-lifecycle.js +2 -1
- package/scripts/daemon-session-commands.js +52 -4
- package/scripts/daemon-session-store.js +2 -1
- package/scripts/daemon-task-scheduler.js +68 -38
- package/scripts/daemon-user-acl.js +26 -9
- package/scripts/daemon.js +81 -17
- package/scripts/distill.js +323 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +119 -0
- package/scripts/docs/pointer-map.md +88 -0
- package/scripts/feishu-adapter.js +6 -1
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +100 -5
- package/scripts/memory-nightly-reflect.js +196 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +2 -0
- package/scripts/providers.js +169 -21
- 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/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,9 @@ const {
|
|
|
5
5
|
CORE_USAGE_CATEGORIES,
|
|
6
6
|
USAGE_CATEGORY_LABEL,
|
|
7
7
|
} = require('./usage-classifier');
|
|
8
|
+
const { IS_WIN } = require('./platform');
|
|
9
|
+
let mentorEngine = null;
|
|
10
|
+
try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
|
|
8
11
|
|
|
9
12
|
function createAdminCommandHandler(deps) {
|
|
10
13
|
const {
|
|
@@ -30,6 +33,8 @@ function createAdminCommandHandler(deps) {
|
|
|
30
33
|
getMessageQueue,
|
|
31
34
|
loadState,
|
|
32
35
|
saveState,
|
|
36
|
+
getDefaultEngine = () => 'claude',
|
|
37
|
+
setDefaultEngine = () => {},
|
|
33
38
|
} = deps;
|
|
34
39
|
|
|
35
40
|
function resolveProjectKey(targetName, projects) {
|
|
@@ -92,6 +97,61 @@ function createAdminCommandHandler(deps) {
|
|
|
92
97
|
return 'unspecified';
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
function modeFromLevel(level) {
|
|
101
|
+
const n = Number(level);
|
|
102
|
+
if (!Number.isFinite(n)) return 'gentle';
|
|
103
|
+
if (n >= 8) return 'intense';
|
|
104
|
+
if (n >= 4) return 'active';
|
|
105
|
+
return 'gentle';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseDistillModelIntent(input) {
|
|
109
|
+
const text = String(input || '').trim();
|
|
110
|
+
if (!text || text.startsWith('/')) return null;
|
|
111
|
+
if (!/(蒸馏|distill|提炼|提纯)/i.test(text)) return null;
|
|
112
|
+
const setVerb = '(?:改成|改为|设为|设置|切到|切换到|换成|改用|使用|用|set|switch|use)';
|
|
113
|
+
if (!(new RegExp(setVerb, 'i')).test(text)) return null;
|
|
114
|
+
|
|
115
|
+
const explicitModel = text.match(new RegExp(`(?:蒸馏模型|模型|distill\\s*model|model)\\s*(?:${setVerb}|to|is)?\\s*[::]?\\s*([a-zA-Z0-9._-]{2,80})`, 'i'));
|
|
116
|
+
if (explicitModel) return { model: explicitModel[1] };
|
|
117
|
+
|
|
118
|
+
if (/(蒸馏模型|模型|distill\s*model|model)/i.test(text)) {
|
|
119
|
+
const quotedModel = text.match(/[“"'「]([a-zA-Z0-9._-]{2,80})[”"'」]/);
|
|
120
|
+
if (quotedModel) return { model: quotedModel[1] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
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'));
|
|
124
|
+
if (knownToken) return { model: knownToken[1] };
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ensureMentorConfig(cfg) {
|
|
130
|
+
if (!cfg.daemon) cfg.daemon = {};
|
|
131
|
+
if (!cfg.daemon.mentor || typeof cfg.daemon.mentor !== 'object') {
|
|
132
|
+
cfg.daemon.mentor = {};
|
|
133
|
+
}
|
|
134
|
+
const mentor = cfg.daemon.mentor;
|
|
135
|
+
if (typeof mentor.enabled !== 'boolean') mentor.enabled = false;
|
|
136
|
+
if (!Number.isFinite(Number(mentor.friction_level))) mentor.friction_level = 3;
|
|
137
|
+
if (!mentor.mode || !['gentle', 'active', 'intense'].includes(String(mentor.mode))) {
|
|
138
|
+
mentor.mode = modeFromLevel(mentor.friction_level);
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(mentor.exclude_agents)) mentor.exclude_agents = ['personal', 'xianyu'];
|
|
141
|
+
if (!Array.isArray(mentor.emotion_keywords_extra)) mentor.emotion_keywords_extra = [];
|
|
142
|
+
return mentor;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function hasCli(execSyncFn, bin) {
|
|
146
|
+
try {
|
|
147
|
+
const cmd = process.platform === 'win32' ? `where ${bin}` : `which ${bin}`;
|
|
148
|
+
execSyncFn(cmd, { encoding: 'utf8' });
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
95
155
|
async function handleAdminCommand(ctx) {
|
|
96
156
|
const { bot, chatId, text } = ctx;
|
|
97
157
|
const state = ctx.state || {};
|
|
@@ -195,17 +255,69 @@ function createAdminCommandHandler(deps) {
|
|
|
195
255
|
return { handled: true, config };
|
|
196
256
|
}
|
|
197
257
|
|
|
258
|
+
// /skill-evo approve <id> — approve a workflow_proposal and dispatch skill creation
|
|
259
|
+
const approveMatch = arg.match(/^approve\s+(\S+)$/i);
|
|
260
|
+
if (approveMatch) {
|
|
261
|
+
const id = approveMatch[1];
|
|
262
|
+
// Find the queue item (search both pending and notified states)
|
|
263
|
+
const item = skillEvolution.listQueueItems({ status: ['pending', 'notified'], limit: 200 })
|
|
264
|
+
.find(i => i.id === id && i.type === 'workflow_proposal');
|
|
265
|
+
if (!item) {
|
|
266
|
+
await bot.sendMessage(chatId, `❌ 未找到 workflow_proposal: ${id}`);
|
|
267
|
+
return { handled: true, config };
|
|
268
|
+
}
|
|
269
|
+
// Build skill-creator prefilled prompt
|
|
270
|
+
const toolsSig = (item.tools_signature || []).join(', ');
|
|
271
|
+
const prefilledPrompt = [
|
|
272
|
+
'/skill-creator',
|
|
273
|
+
`创建一个新技能,自动化以下工作流:`,
|
|
274
|
+
`工作流模式: ${item.search_hint || item.reason}`,
|
|
275
|
+
toolsSig ? `常用工具: ${toolsSig}` : '',
|
|
276
|
+
item.example_prompt ? `用户示例: "${item.example_prompt}"` : '',
|
|
277
|
+
`该技能应封装这个多步工作流为单一可调用技能。`,
|
|
278
|
+
].filter(Boolean).join('\n');
|
|
279
|
+
// Dispatch to metame agent for skill creation (async — must not block event loop)
|
|
280
|
+
try {
|
|
281
|
+
const HOME = require('os').homedir();
|
|
282
|
+
const dispatchBin = require('path').join(HOME, '.metame', 'bin', 'dispatch_to');
|
|
283
|
+
const { execFile } = require('child_process');
|
|
284
|
+
const { promisify } = require('util');
|
|
285
|
+
const execFileAsync = promisify(execFile);
|
|
286
|
+
// dispatch_to is a Node.js script; on Windows shebang resolution is unavailable,
|
|
287
|
+
// so invoke via node explicitly for cross-platform safety
|
|
288
|
+
const cmd = IS_WIN ? process.execPath : dispatchBin;
|
|
289
|
+
const cmdArgs = IS_WIN ? [dispatchBin, 'metame', prefilledPrompt] : ['metame', prefilledPrompt];
|
|
290
|
+
await execFileAsync(cmd, cmdArgs, { encoding: 'utf8', timeout: 15000 });
|
|
291
|
+
// Mark installed only after successful dispatch
|
|
292
|
+
skillEvolution.resolveQueueItemById(id, 'installed');
|
|
293
|
+
await bot.sendMessage(chatId, `✅ 已派发给 Jarvis 创建技能,完成后会通知你\n工作流: ${item.search_hint || item.reason}`);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
// Dispatch failed — don't mark installed, keep in queue
|
|
296
|
+
await bot.sendMessage(chatId, `⚠️ 自动派发失败: ${e.message}\n提案仍在队列中,可重试: /skill-evo approve ${id}`);
|
|
297
|
+
}
|
|
298
|
+
return { handled: true, config };
|
|
299
|
+
}
|
|
300
|
+
|
|
198
301
|
const dismissMatch = arg.match(/^(?:dismiss|skip|ignored?)\s+(\S+)$/i);
|
|
199
302
|
if (dismissMatch) {
|
|
200
303
|
const id = dismissMatch[1];
|
|
304
|
+
// Check if this is a workflow_proposal — if so, reset the sketch
|
|
305
|
+
const item = skillEvolution.listQueueItems({ status: ['pending', 'notified'], limit: 200 })
|
|
306
|
+
.find(i => i.id === id);
|
|
201
307
|
const ok = skillEvolution.resolveQueueItemById
|
|
202
308
|
? skillEvolution.resolveQueueItemById(id, 'dismissed')
|
|
203
309
|
: false;
|
|
310
|
+
// Reset workflow sketch so it can re-accumulate
|
|
311
|
+
if (ok && item && item.type === 'workflow_proposal' && item.workflow_sketch_id) {
|
|
312
|
+
if (skillEvolution.resetWorkflowSketch) {
|
|
313
|
+
skillEvolution.resetWorkflowSketch(item.workflow_sketch_id);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
204
316
|
await bot.sendMessage(chatId, ok ? `✅ 已标记 dismissed: ${id}` : `❌ 未找到可处理项: ${id}`);
|
|
205
317
|
return { handled: true, config };
|
|
206
318
|
}
|
|
207
319
|
|
|
208
|
-
await bot.sendMessage(chatId, '用法: /skill-evo list | /skill-evo done <id> | /skill-evo dismiss <id>');
|
|
320
|
+
await bot.sendMessage(chatId, '用法: /skill-evo list | /skill-evo done <id> | /skill-evo dismiss <id> | /skill-evo approve <id>');
|
|
209
321
|
return { handled: true, config };
|
|
210
322
|
}
|
|
211
323
|
|
|
@@ -635,6 +747,68 @@ function createAdminCommandHandler(deps) {
|
|
|
635
747
|
return { handled: true, config };
|
|
636
748
|
}
|
|
637
749
|
|
|
750
|
+
if (text === '/mentor' || text.startsWith('/mentor ')) {
|
|
751
|
+
try {
|
|
752
|
+
backupConfig();
|
|
753
|
+
const cfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {};
|
|
754
|
+
const mentorCfg = ensureMentorConfig(cfg);
|
|
755
|
+
const arg = text.slice('/mentor'.length).trim();
|
|
756
|
+
|
|
757
|
+
if (!arg || arg === 'status') {
|
|
758
|
+
const status = mentorEngine && typeof mentorEngine.getRuntimeStatus === 'function'
|
|
759
|
+
? mentorEngine.getRuntimeStatus()
|
|
760
|
+
: { debt_count: 0, cooldown_remaining_ms: 0 };
|
|
761
|
+
const mode = String(mentorCfg.mode || modeFromLevel(mentorCfg.friction_level));
|
|
762
|
+
const level = Number(mentorCfg.friction_level || 0);
|
|
763
|
+
const cooldownSec = Math.ceil((Number(status.cooldown_remaining_ms) || 0) / 1000);
|
|
764
|
+
const lines = [
|
|
765
|
+
`Mentor: ${mentorCfg.enabled ? 'ON' : 'OFF'}`,
|
|
766
|
+
`Mode: ${mode}`,
|
|
767
|
+
`Friction level: ${level}`,
|
|
768
|
+
`Debts: ${status.debt_count || 0}`,
|
|
769
|
+
`Emotion cooldown: ${cooldownSec > 0 ? `${cooldownSec}s` : '0s'}`,
|
|
770
|
+
'Zone: n/a (runtime)',
|
|
771
|
+
];
|
|
772
|
+
await bot.sendMessage(chatId, lines.join('\n'));
|
|
773
|
+
return { handled: true, config };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (arg === 'on' || arg === 'off') {
|
|
777
|
+
mentorCfg.enabled = arg === 'on';
|
|
778
|
+
writeConfigSafe(cfg);
|
|
779
|
+
config = loadConfig();
|
|
780
|
+
await bot.sendMessage(chatId, mentorCfg.enabled
|
|
781
|
+
? '✅ Mentor mode enabled.'
|
|
782
|
+
: '✅ Mentor mode disabled.');
|
|
783
|
+
return { handled: true, config };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const mLevel = arg.match(/^level\s+(-?\d{1,2})$/i);
|
|
787
|
+
if (mLevel) {
|
|
788
|
+
let level = Number(mLevel[1]);
|
|
789
|
+
if (!Number.isFinite(level)) level = 3;
|
|
790
|
+
level = Math.max(0, Math.min(10, Math.floor(level)));
|
|
791
|
+
mentorCfg.friction_level = level;
|
|
792
|
+
mentorCfg.mode = modeFromLevel(level);
|
|
793
|
+
writeConfigSafe(cfg);
|
|
794
|
+
config = loadConfig();
|
|
795
|
+
await bot.sendMessage(chatId, `✅ Mentor level set to ${level} (${mentorCfg.mode}).`);
|
|
796
|
+
return { handled: true, config };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
await bot.sendMessage(chatId, [
|
|
800
|
+
'用法:',
|
|
801
|
+
'/mentor on',
|
|
802
|
+
'/mentor off',
|
|
803
|
+
'/mentor level <0-10>',
|
|
804
|
+
'/mentor status',
|
|
805
|
+
].join('\n'));
|
|
806
|
+
} catch (e) {
|
|
807
|
+
await bot.sendMessage(chatId, `❌ Mentor command failed: ${e.message}`);
|
|
808
|
+
}
|
|
809
|
+
return { handled: true, config };
|
|
810
|
+
}
|
|
811
|
+
|
|
638
812
|
if (text === '/reload') {
|
|
639
813
|
if (global._metameReload) {
|
|
640
814
|
const r = global._metameReload();
|
|
@@ -727,6 +901,10 @@ function createAdminCommandHandler(deps) {
|
|
|
727
901
|
const validModels = ['sonnet', 'opus', 'haiku'];
|
|
728
902
|
const checks = [];
|
|
729
903
|
let issues = 0;
|
|
904
|
+
const activeProvider = providerMod && typeof providerMod.getActiveName === 'function'
|
|
905
|
+
? providerMod.getActiveName()
|
|
906
|
+
: 'anthropic';
|
|
907
|
+
const isCustomProvider = activeProvider !== 'anthropic';
|
|
730
908
|
|
|
731
909
|
let cfg = null;
|
|
732
910
|
try {
|
|
@@ -738,21 +916,34 @@ function createAdminCommandHandler(deps) {
|
|
|
738
916
|
}
|
|
739
917
|
|
|
740
918
|
const m = (cfg && cfg.daemon && cfg.daemon.model) || 'opus';
|
|
741
|
-
|
|
919
|
+
const modelOk = isCustomProvider
|
|
920
|
+
? /^[a-zA-Z0-9._-]{2,80}$/.test(String(m || '').trim())
|
|
921
|
+
: validModels.includes(m);
|
|
922
|
+
if (modelOk) {
|
|
742
923
|
checks.push(`✅ 模型: ${m}`);
|
|
743
924
|
} else {
|
|
744
|
-
checks.push(`❌ 模型: ${m} (无效)`);
|
|
925
|
+
checks.push(`❌ 模型: ${m} (${isCustomProvider ? '格式无效' : '无效'})`);
|
|
745
926
|
issues++;
|
|
746
927
|
}
|
|
747
928
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
929
|
+
const hasClaude = hasCli(execSync, 'claude');
|
|
930
|
+
const hasCodex = hasCli(execSync, 'codex');
|
|
931
|
+
checks.push(hasClaude ? '✅ Claude CLI' : '⚠️ Claude CLI 未找到');
|
|
932
|
+
checks.push(hasCodex ? '✅ Codex CLI' : '⚠️ Codex CLI 未找到');
|
|
933
|
+
|
|
934
|
+
const currentEngine = getDefaultEngine() === 'codex' ? 'codex' : 'claude';
|
|
935
|
+
if (currentEngine === 'claude' && !hasClaude) {
|
|
936
|
+
checks.push('❌ 当前默认引擎是 claude,但 Claude CLI 不可用');
|
|
937
|
+
issues++;
|
|
938
|
+
}
|
|
939
|
+
if (currentEngine === 'codex' && !hasCodex) {
|
|
940
|
+
checks.push('❌ 当前默认引擎是 codex,但 Codex CLI 不可用');
|
|
753
941
|
issues++;
|
|
754
942
|
}
|
|
755
943
|
|
|
944
|
+
checks.push(`✅ 默认引擎: ${currentEngine}`);
|
|
945
|
+
checks.push(`✅ Provider: ${activeProvider}${isCustomProvider ? ' (custom)' : ''}`);
|
|
946
|
+
|
|
756
947
|
const bakFile = CONFIG_FILE + '.bak';
|
|
757
948
|
const hasBak = fs.existsSync(bakFile);
|
|
758
949
|
checks.push(hasBak ? '✅ 有备份' : '⚠️ 无备份');
|
|
@@ -867,10 +1058,64 @@ function createAdminCommandHandler(deps) {
|
|
|
867
1058
|
return { handled: true, config };
|
|
868
1059
|
}
|
|
869
1060
|
|
|
1061
|
+
// /engine [name] — show or switch default engine (claude/codex)
|
|
1062
|
+
if (text === '/engine' || text.startsWith('/engine ')) {
|
|
1063
|
+
const arg = text.slice('/engine'.length).trim().toLowerCase();
|
|
1064
|
+
if (!arg) {
|
|
1065
|
+
const cur = getDefaultEngine();
|
|
1066
|
+
const distill = providerMod ? providerMod.getDistillModel() : '(unknown)';
|
|
1067
|
+
await bot.sendMessage(chatId, `🔧 当前引擎: ${cur}\n🧪 蒸馏模型: ${distill}\n\n用法: /engine claude 或 /engine codex`);
|
|
1068
|
+
return { handled: true, config };
|
|
1069
|
+
}
|
|
1070
|
+
if (arg !== 'claude' && arg !== 'codex') {
|
|
1071
|
+
await bot.sendMessage(chatId, `❌ 不支持的引擎: ${arg}\n可选: claude, codex`);
|
|
1072
|
+
return { handled: true, config };
|
|
1073
|
+
}
|
|
1074
|
+
setDefaultEngine(arg);
|
|
1075
|
+
const distill = providerMod ? providerMod.getDistillModel() : '(unknown)';
|
|
1076
|
+
await bot.sendMessage(chatId, `✅ 默认引擎: ${arg}\n🧪 蒸馏模型已同步: ${distill}`);
|
|
1077
|
+
return { handled: true, config };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// /distill-model [name] — show or update distill model
|
|
1081
|
+
if (text === '/distill-model' || text.startsWith('/distill-model ')) {
|
|
1082
|
+
if (!providerMod || typeof providerMod.getDistillModel !== 'function' || typeof providerMod.setDistillModel !== 'function') {
|
|
1083
|
+
await bot.sendMessage(chatId, '❌ Distill model config is not available.');
|
|
1084
|
+
return { handled: true, config };
|
|
1085
|
+
}
|
|
1086
|
+
const arg = text.slice('/distill-model'.length).trim();
|
|
1087
|
+
if (!arg) {
|
|
1088
|
+
await bot.sendMessage(chatId, `🧪 当前蒸馏模型: ${providerMod.getDistillModel()}\n用法: /distill-model <model>\n示例: /distill-model gpt-5.1-codex-mini`);
|
|
1089
|
+
return { handled: true, config };
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
providerMod.setDistillModel(arg);
|
|
1093
|
+
await bot.sendMessage(chatId, `✅ 蒸馏模型已更新为: ${providerMod.getDistillModel()}`);
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
await bot.sendMessage(chatId, `❌ 设置失败: ${e.message}`);
|
|
1096
|
+
}
|
|
1097
|
+
return { handled: true, config };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const nlDistillIntent = parseDistillModelIntent(text);
|
|
1101
|
+
if (nlDistillIntent) {
|
|
1102
|
+
if (!providerMod || typeof providerMod.setDistillModel !== 'function' || typeof providerMod.getDistillModel !== 'function') {
|
|
1103
|
+
await bot.sendMessage(chatId, '❌ Distill model config is not available.');
|
|
1104
|
+
return { handled: true, config };
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
providerMod.setDistillModel(nlDistillIntent.model);
|
|
1108
|
+
await bot.sendMessage(chatId, `✅ 已按自然语言请求更新蒸馏模型: ${providerMod.getDistillModel()}`);
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
await bot.sendMessage(chatId, `❌ 设置失败: ${e.message}`);
|
|
1111
|
+
}
|
|
1112
|
+
return { handled: true, config };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
870
1115
|
return { handled: false, config };
|
|
871
1116
|
}
|
|
872
1117
|
|
|
873
|
-
return { handleAdminCommand };
|
|
1118
|
+
return { handleAdminCommand, _private: { parseDistillModelIntent } };
|
|
874
1119
|
}
|
|
875
1120
|
|
|
876
1121
|
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;
|
|
@@ -485,7 +512,36 @@ function createAgentCommandHandler(deps) {
|
|
|
485
512
|
}
|
|
486
513
|
}
|
|
487
514
|
}
|
|
488
|
-
// No pending activation
|
|
515
|
+
// No pending activation — fall back to scanning daemon.yaml for unbound projects
|
|
516
|
+
const allBoundKeys = new Set(Object.values({
|
|
517
|
+
...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
|
|
518
|
+
...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
|
|
519
|
+
}));
|
|
520
|
+
const unboundProjects = Object.entries(cfg.projects || {})
|
|
521
|
+
.filter(([key, p]) => p && p.cwd && !allBoundKeys.has(key))
|
|
522
|
+
.map(([key, p]) => ({ key, name: p.name || key, cwd: p.cwd, icon: p.icon || '🤖' }));
|
|
523
|
+
|
|
524
|
+
if (unboundProjects.length === 1) {
|
|
525
|
+
// Exactly one unbound project — auto-bind using project KEY (not display name)
|
|
526
|
+
// to ensure toProjectKey() resolves to the correct existing key in daemon.yaml
|
|
527
|
+
const proj = unboundProjects[0];
|
|
528
|
+
const bindRes2 = await bindViaUnifiedApi(bot, chatId, proj.key, proj.cwd);
|
|
529
|
+
if (bindRes2.ok) pendingActivations && pendingActivations.delete(proj.key);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (unboundProjects.length > 1) {
|
|
534
|
+
// Multiple unbound projects — show pick list using project keys
|
|
535
|
+
const lines = ['请选择要激活的 Agent:', ''];
|
|
536
|
+
for (const p of unboundProjects) {
|
|
537
|
+
lines.push(`${p.icon} ${p.name} → \`/agent bind ${p.key} ${p.cwd}\``);
|
|
538
|
+
}
|
|
539
|
+
lines.push('\n发送对应命令即可绑定此群。');
|
|
540
|
+
await bot.sendMessage(chatId, lines.join('\n'));
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Truly nothing to activate
|
|
489
545
|
await bot.sendMessage(chatId,
|
|
490
546
|
'没有待激活的 Agent。\n\n如果已创建过 Agent,直接用:\n`/agent bind <名称> <目录>`\n即可绑定,不需要重新创建。'
|
|
491
547
|
);
|
|
@@ -517,4 +573,6 @@ function createAgentCommandHandler(deps) {
|
|
|
517
573
|
return { handleAgentCommand };
|
|
518
574
|
}
|
|
519
575
|
|
|
520
|
-
module.exports = {
|
|
576
|
+
module.exports = {
|
|
577
|
+
createAgentCommandHandler,
|
|
578
|
+
};
|
|
@@ -31,13 +31,17 @@ function createAgentTools(deps) {
|
|
|
31
31
|
return (String(agentName || '').replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase() || String(chatId));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function normalizeEngine(engine) {
|
|
35
|
+
return String(engine || '').trim().toLowerCase() === 'codex' ? 'codex' : 'claude';
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
function ensureAdapterConfig(cfg, adapterKey) {
|
|
35
39
|
if (!cfg[adapterKey]) cfg[adapterKey] = {};
|
|
36
40
|
if (!cfg[adapterKey].allowed_chat_ids) cfg[adapterKey].allowed_chat_ids = [];
|
|
37
41
|
if (!cfg[adapterKey].chat_agent_map) cfg[adapterKey].chat_agent_map = {};
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
async function bindAgentToChat(chatId, agentName, workspaceDir, { force = false } = {}) {
|
|
44
|
+
async function bindAgentToChat(chatId, agentName, workspaceDir, { force = false, engine = null } = {}) {
|
|
41
45
|
try {
|
|
42
46
|
const safeName = sanitizeText(agentName, 120);
|
|
43
47
|
if (!safeName) return { ok: false, error: 'agentName is required' };
|
|
@@ -49,6 +53,7 @@ function createAgentTools(deps) {
|
|
|
49
53
|
|
|
50
54
|
const projectKey = toProjectKey(safeName, chatId);
|
|
51
55
|
let resolvedDir = resolveWorkspaceDir(workspaceDir);
|
|
56
|
+
const normalizedEngine = engine ? normalizeEngine(engine) : null;
|
|
52
57
|
|
|
53
58
|
if (!resolvedDir) {
|
|
54
59
|
const existing = cfg.projects[projectKey];
|
|
@@ -80,7 +85,12 @@ function createAgentTools(deps) {
|
|
|
80
85
|
cfg[adapterKey].chat_agent_map[String(chatId)] = projectKey;
|
|
81
86
|
const existed = !!cfg.projects[projectKey];
|
|
82
87
|
if (!existed) {
|
|
83
|
-
cfg.projects[projectKey] = {
|
|
88
|
+
cfg.projects[projectKey] = {
|
|
89
|
+
name: safeName,
|
|
90
|
+
cwd: resolvedDir,
|
|
91
|
+
nicknames: [safeName],
|
|
92
|
+
...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
|
|
93
|
+
};
|
|
84
94
|
} else {
|
|
85
95
|
const nicknames = Array.isArray(cfg.projects[projectKey].nicknames)
|
|
86
96
|
? cfg.projects[projectKey].nicknames
|
|
@@ -91,6 +101,7 @@ function createAgentTools(deps) {
|
|
|
91
101
|
name: safeName,
|
|
92
102
|
cwd: resolvedDir,
|
|
93
103
|
nicknames,
|
|
104
|
+
...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
|
|
94
105
|
};
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -175,8 +186,9 @@ ${safeDelta}
|
|
|
175
186
|
}
|
|
176
187
|
}
|
|
177
188
|
|
|
178
|
-
async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false } = {}) {
|
|
189
|
+
async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false, engine = null } = {}) {
|
|
179
190
|
let bindData;
|
|
191
|
+
const normalizedEngine = engine ? normalizeEngine(engine) : null;
|
|
180
192
|
|
|
181
193
|
if (skipChatBinding) {
|
|
182
194
|
// Create the project entry without touching chat_agent_map
|
|
@@ -192,7 +204,16 @@ ${safeDelta}
|
|
|
192
204
|
const projectKey = toProjectKey(safeName, chatId);
|
|
193
205
|
const existed = !!cfg.projects[projectKey];
|
|
194
206
|
if (!existed) {
|
|
195
|
-
cfg.projects[projectKey] = {
|
|
207
|
+
cfg.projects[projectKey] = {
|
|
208
|
+
name: safeName,
|
|
209
|
+
cwd: resolvedDir,
|
|
210
|
+
nicknames: [safeName],
|
|
211
|
+
...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
|
|
212
|
+
};
|
|
213
|
+
writeConfigSafe(cfg);
|
|
214
|
+
backupConfig();
|
|
215
|
+
} else if (normalizedEngine === 'codex') {
|
|
216
|
+
cfg.projects[projectKey] = { ...cfg.projects[projectKey], engine: 'codex' };
|
|
196
217
|
writeConfigSafe(cfg);
|
|
197
218
|
backupConfig();
|
|
198
219
|
}
|
|
@@ -204,7 +225,7 @@ ${safeDelta}
|
|
|
204
225
|
project: cfg.projects[projectKey],
|
|
205
226
|
};
|
|
206
227
|
} else {
|
|
207
|
-
const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir);
|
|
228
|
+
const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir, { engine: normalizedEngine });
|
|
208
229
|
if (!bindResult.ok) return bindResult;
|
|
209
230
|
bindData = bindResult.data;
|
|
210
231
|
}
|