metame-cli 1.4.33 → 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 +187 -48
- 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 +704 -268
- 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 +87 -28
- 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
|
@@ -26,8 +26,24 @@ function createSessionCommandHandler(deps) {
|
|
|
26
26
|
sessionRichLabel,
|
|
27
27
|
buildSessionCardElements,
|
|
28
28
|
sessionLabel,
|
|
29
|
+
getDefaultEngine = () => 'claude',
|
|
29
30
|
} = deps;
|
|
30
31
|
|
|
32
|
+
function normalizeEngineName(name) {
|
|
33
|
+
const n = String(name || '').trim().toLowerCase();
|
|
34
|
+
return n === 'codex' ? 'codex' : getDefaultEngine();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function inferEngineByCwd(cfg, cwd) {
|
|
38
|
+
if (!cfg || !cfg.projects || !cwd) return null;
|
|
39
|
+
const target = normalizeCwd(cwd);
|
|
40
|
+
for (const proj of Object.values(cfg.projects || {})) {
|
|
41
|
+
if (!proj || !proj.cwd) continue;
|
|
42
|
+
if (normalizeCwd(proj.cwd) === target) return normalizeEngineName(proj.engine);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
async function handleSessionCommand(ctx) {
|
|
32
48
|
const { bot, chatId, text } = ctx;
|
|
33
49
|
|
|
@@ -61,7 +77,7 @@ function createSessionCommandHandler(deps) {
|
|
|
61
77
|
const boundProj = boundKey && newCfg.projects && newCfg.projects[boundKey];
|
|
62
78
|
if (boundProj && boundProj.cwd) {
|
|
63
79
|
const boundCwd = normalizeCwd(boundProj.cwd);
|
|
64
|
-
const session = createSession(chatId, boundCwd, '');
|
|
80
|
+
const session = createSession(chatId, boundCwd, '', normalizeEngineName(boundProj.engine));
|
|
65
81
|
await bot.sendMessage(chatId, `✅ 新会话已创建\nWorkdir: ${session.cwd}`);
|
|
66
82
|
return true;
|
|
67
83
|
}
|
|
@@ -87,7 +103,13 @@ function createSessionCommandHandler(deps) {
|
|
|
87
103
|
return true;
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
|
-
const
|
|
106
|
+
const cfgForEngine = loadConfig();
|
|
107
|
+
const mapForEngine = { ...(cfgForEngine.telegram ? cfgForEngine.telegram.chat_agent_map : {}), ...(cfgForEngine.feishu ? cfgForEngine.feishu.chat_agent_map : {}) };
|
|
108
|
+
const mappedKeyForEngine = mapForEngine[String(chatId)];
|
|
109
|
+
const mappedProjForEngine = mappedKeyForEngine && cfgForEngine.projects ? cfgForEngine.projects[mappedKeyForEngine] : null;
|
|
110
|
+
const currentEngine = normalizeEngineName(getSession(chatId) && getSession(chatId).engine);
|
|
111
|
+
const sessionEngine = normalizeEngineName((mappedProjForEngine && mappedProjForEngine.engine) || currentEngine);
|
|
112
|
+
const session = createSession(chatId, dirPath, sessionName || '', sessionEngine);
|
|
91
113
|
const label = sessionName ? `[${sessionName}]` : '';
|
|
92
114
|
await bot.sendMessage(chatId, `New session ${label}\nWorkdir: ${session.cwd}`);
|
|
93
115
|
return true;
|
|
@@ -142,11 +164,15 @@ function createSessionCommandHandler(deps) {
|
|
|
142
164
|
if (!s) {
|
|
143
165
|
// Last resort: use __continue__ to resume whatever Claude thinks is last
|
|
144
166
|
const state2 = loadState();
|
|
167
|
+
const cfgForEngine = loadConfig();
|
|
168
|
+
const currentEngine = normalizeEngineName(state2.sessions[chatId] && state2.sessions[chatId].engine);
|
|
169
|
+
const engineByCwd = inferEngineByCwd(cfgForEngine, curCwd || HOME);
|
|
145
170
|
state2.sessions[chatId] = {
|
|
146
171
|
id: '__continue__',
|
|
147
172
|
cwd: curCwd || HOME,
|
|
148
173
|
created: new Date().toISOString(),
|
|
149
174
|
started: true,
|
|
175
|
+
engine: engineByCwd || currentEngine,
|
|
150
176
|
};
|
|
151
177
|
saveState(state2);
|
|
152
178
|
await bot.sendMessage(chatId, `⚡ Resuming last session in ${path.basename(curCwd || HOME)}`);
|
|
@@ -154,10 +180,14 @@ function createSessionCommandHandler(deps) {
|
|
|
154
180
|
}
|
|
155
181
|
|
|
156
182
|
const state2 = loadState();
|
|
183
|
+
const cfgForEngine = loadConfig();
|
|
184
|
+
const currentEngine = normalizeEngineName(state2.sessions[chatId] && state2.sessions[chatId].engine);
|
|
185
|
+
const engineByCwd = inferEngineByCwd(cfgForEngine, s.projectPath || HOME);
|
|
157
186
|
state2.sessions[chatId] = {
|
|
158
187
|
id: s.sessionId,
|
|
159
188
|
cwd: s.projectPath || HOME,
|
|
160
189
|
started: true,
|
|
190
|
+
engine: engineByCwd || currentEngine,
|
|
161
191
|
};
|
|
162
192
|
saveState(state2);
|
|
163
193
|
// Display: name/summary + id on separate lines
|
|
@@ -223,9 +253,12 @@ function createSessionCommandHandler(deps) {
|
|
|
223
253
|
|
|
224
254
|
// /sessions — compact list, tap to see details, then tap to switch
|
|
225
255
|
if (text === '/sessions') {
|
|
256
|
+
const currentEngine = normalizeEngineName(getSession(chatId) && getSession(chatId).engine);
|
|
257
|
+
const codexLimitTip = '⚠️ 当前为 Codex 会话:`/sessions` 列表暂仅展示 Claude 本地会话,Codex 会话暂不可见。';
|
|
226
258
|
const allSessions = listRecentSessions(15);
|
|
227
259
|
if (allSessions.length === 0) {
|
|
228
|
-
|
|
260
|
+
const base = 'No sessions found. Try /new first.';
|
|
261
|
+
await bot.sendMessage(chatId, currentEngine === 'codex' ? `${base}\n\n${codexLimitTip}` : base);
|
|
229
262
|
return true;
|
|
230
263
|
}
|
|
231
264
|
if (bot.sendButtons) {
|
|
@@ -236,8 +269,12 @@ function createSessionCommandHandler(deps) {
|
|
|
236
269
|
allSessions.forEach((s, i) => {
|
|
237
270
|
msg += sessionRichLabel(s, i + 1, _tags1) + '\n';
|
|
238
271
|
});
|
|
272
|
+
if (currentEngine === 'codex') msg += `\n${codexLimitTip}\n`;
|
|
239
273
|
await bot.sendMessage(chatId, msg);
|
|
240
274
|
}
|
|
275
|
+
if (bot.sendButtons && currentEngine === 'codex') {
|
|
276
|
+
await bot.sendMessage(chatId, codexLimitTip);
|
|
277
|
+
}
|
|
241
278
|
return true;
|
|
242
279
|
}
|
|
243
280
|
|
|
@@ -348,10 +385,14 @@ function createSessionCommandHandler(deps) {
|
|
|
348
385
|
const target = candidates[0];
|
|
349
386
|
// Switch to that session (like /resume) AND its directory
|
|
350
387
|
const state2 = loadState();
|
|
388
|
+
const cfgForEngine = loadConfig();
|
|
389
|
+
const currentEngine = normalizeEngineName(state2.sessions[chatId] && state2.sessions[chatId].engine);
|
|
390
|
+
const engineByCwd = inferEngineByCwd(cfgForEngine, target.projectPath);
|
|
351
391
|
state2.sessions[chatId] = {
|
|
352
392
|
id: target.sessionId,
|
|
353
393
|
cwd: target.projectPath,
|
|
354
394
|
started: true,
|
|
395
|
+
engine: engineByCwd || currentEngine,
|
|
355
396
|
};
|
|
356
397
|
saveState(state2);
|
|
357
398
|
const name = target.customTitle || target.summary || '';
|
|
@@ -378,19 +419,26 @@ function createSessionCommandHandler(deps) {
|
|
|
378
419
|
if (recentInDir.length > 0 && recentInDir[0].sessionId) {
|
|
379
420
|
// Attach to existing session in this directory
|
|
380
421
|
const target = recentInDir[0];
|
|
422
|
+
const cfgForEngine = loadConfig();
|
|
423
|
+
const engineByCwd = inferEngineByCwd(cfgForEngine, newCwd);
|
|
381
424
|
state2.sessions[chatId] = {
|
|
382
425
|
id: target.sessionId,
|
|
383
426
|
cwd: newCwd,
|
|
384
427
|
started: true,
|
|
428
|
+
engine: engineByCwd || normalizeEngineName(state2.sessions[chatId] && state2.sessions[chatId].engine),
|
|
385
429
|
};
|
|
386
430
|
saveState(state2);
|
|
387
431
|
const label = target.customTitle || target.summary?.slice(0, 30) || target.sessionId.slice(0, 8);
|
|
388
432
|
await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)}\n🔄 Attached: ${label}`);
|
|
389
433
|
} else if (!state2.sessions[chatId]) {
|
|
390
|
-
|
|
434
|
+
const cfgForEngine = loadConfig();
|
|
435
|
+
const engineByCwd = inferEngineByCwd(cfgForEngine, newCwd);
|
|
436
|
+
const currentEngine = normalizeEngineName(getSession(chatId) && getSession(chatId).engine);
|
|
437
|
+
createSession(chatId, newCwd, '', engineByCwd || currentEngine);
|
|
391
438
|
await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)} (new session)`);
|
|
392
439
|
} else {
|
|
393
440
|
state2.sessions[chatId].cwd = newCwd;
|
|
441
|
+
if (!state2.sessions[chatId].engine) state2.sessions[chatId].engine = getDefaultEngine();
|
|
394
442
|
saveState(state2);
|
|
395
443
|
await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)}`);
|
|
396
444
|
}
|
|
@@ -432,13 +432,14 @@ function createSessionStore(deps) {
|
|
|
432
432
|
return state.sessions[chatId] || null;
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
-
function createSession(chatId, cwd, name) {
|
|
435
|
+
function createSession(chatId, cwd, name, engine = 'claude') {
|
|
436
436
|
const state = loadState();
|
|
437
437
|
const sessionId = crypto.randomUUID();
|
|
438
438
|
state.sessions[chatId] = {
|
|
439
439
|
id: sessionId,
|
|
440
440
|
cwd: cwd || HOME,
|
|
441
441
|
started: false,
|
|
442
|
+
engine: String(engine || 'claude').trim().toLowerCase() === 'codex' ? 'codex' : 'claude',
|
|
442
443
|
};
|
|
443
444
|
saveState(state);
|
|
444
445
|
invalidateSessionCache();
|
|
@@ -95,6 +95,23 @@ function nextClockRunAfter(schedule, fromMs) {
|
|
|
95
95
|
return baseMs + 24 * 60 * 60 * 1000;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// Map short aliases and full model IDs to what Claude CLI accepts.
|
|
99
|
+
// Claude CLI 2.x accepts both 'sonnet' and 'claude-sonnet-4-6'.
|
|
100
|
+
// This normalization keeps daemon.yaml configs forward-compatible.
|
|
101
|
+
const MODEL_ALIASES = {
|
|
102
|
+
haiku: 'claude-haiku-4-5-20251001',
|
|
103
|
+
sonnet: 'claude-sonnet-4-6',
|
|
104
|
+
opus: 'claude-opus-4-6',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function normalizeModel(raw) {
|
|
108
|
+
if (!raw || typeof raw !== 'string') return MODEL_ALIASES.haiku;
|
|
109
|
+
const lower = raw.trim().toLowerCase();
|
|
110
|
+
if (Object.prototype.hasOwnProperty.call(MODEL_ALIASES, lower)) return MODEL_ALIASES[lower];
|
|
111
|
+
// Already a full model ID (e.g. 'claude-sonnet-4-6') — pass through
|
|
112
|
+
return raw.trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
function buildTaskSchedule(task, parseInterval) {
|
|
99
116
|
const atRaw = typeof task.at === 'string' ? task.at.trim() : '';
|
|
100
117
|
if (atRaw) {
|
|
@@ -195,25 +212,24 @@ function createTaskScheduler(deps) {
|
|
|
195
212
|
try {
|
|
196
213
|
let cmd = task.precondition;
|
|
197
214
|
|
|
198
|
-
// Cross-platform: expand ~ to HOME and handle `test -s`
|
|
215
|
+
// Cross-platform: expand ~ to HOME and handle `test -s` via Node.js
|
|
199
216
|
cmd = cmd.replace(/^~|(?<=\s)~/g, HOME);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
+
// Handle `test -s <file>` natively in JS on ALL platforms.
|
|
218
|
+
// Shell `test -s` produces no stdout on success, which previously
|
|
219
|
+
// caused the empty-output check below to incorrectly skip the task.
|
|
220
|
+
const testMatch = cmd.match(/^test\s+-s\s+(.+)$/);
|
|
221
|
+
if (testMatch) {
|
|
222
|
+
const filePath = testMatch[1].trim().replace(/["']/g, '');
|
|
223
|
+
const fs = require('fs');
|
|
224
|
+
try {
|
|
225
|
+
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
226
|
+
if (content.length > 0) {
|
|
227
|
+
log('INFO', `Precondition passed for ${task.name} (${content.split('\n').length} lines)`);
|
|
228
|
+
return { pass: true, context: content };
|
|
229
|
+
}
|
|
230
|
+
} catch { /* file doesn't exist or unreadable */ }
|
|
231
|
+
log('INFO', `Precondition failed for ${task.name}: file empty or missing`);
|
|
232
|
+
return { pass: false, context: '' };
|
|
217
233
|
}
|
|
218
234
|
|
|
219
235
|
const output = execSync(cmd, {
|
|
@@ -358,7 +374,7 @@ function createTaskScheduler(deps) {
|
|
|
358
374
|
}
|
|
359
375
|
|
|
360
376
|
const preamble = buildProfilePreamble();
|
|
361
|
-
const model = task.model || 'haiku';
|
|
377
|
+
const model = normalizeModel(task.model || 'haiku');
|
|
362
378
|
// If precondition returned context data, append it to the prompt
|
|
363
379
|
let taskPrompt = task.prompt;
|
|
364
380
|
if (precheck.context) {
|
|
@@ -533,7 +549,7 @@ function createTaskScheduler(deps) {
|
|
|
533
549
|
const steps = task.steps || [];
|
|
534
550
|
if (steps.length === 0) return { success: false, error: 'No steps defined', output: '' };
|
|
535
551
|
|
|
536
|
-
const model = task.model || 'sonnet';
|
|
552
|
+
const model = normalizeModel(task.model || 'sonnet');
|
|
537
553
|
const cwd = task.cwd ? task.cwd.replace(/^~/, HOME) : HOME;
|
|
538
554
|
const sessionId = crypto.randomUUID();
|
|
539
555
|
const outputs = [];
|
|
@@ -617,11 +633,22 @@ function createTaskScheduler(deps) {
|
|
|
617
633
|
return found || null;
|
|
618
634
|
}
|
|
619
635
|
|
|
620
|
-
function startHeartbeat(config, notifyFn) {
|
|
636
|
+
function startHeartbeat(config, notifyFn, notifyPersonalFn) {
|
|
621
637
|
const { all: tasks } = getAllTasks(config);
|
|
622
638
|
|
|
623
639
|
const enabledTasks = tasks.filter(t => t.enabled !== false);
|
|
624
640
|
const checkIntervalSec = (config.daemon && config.daemon.heartbeat_check_interval) || 60;
|
|
641
|
+
|
|
642
|
+
// Helper: compute next run time, falling back to 2-tick backoff on error.
|
|
643
|
+
function safeNextRun(taskName, schedule, now) {
|
|
644
|
+
try {
|
|
645
|
+
return { next: nextRunAfter(schedule, now), failed: false };
|
|
646
|
+
} catch (e) {
|
|
647
|
+
log('ERROR', `nextRunAfter failed for "${taskName}": ${e.message}`);
|
|
648
|
+
return { next: now + checkIntervalSec * 2 * 1000, failed: true };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
625
652
|
const taskSchedules = new Map();
|
|
626
653
|
const runnableTasks = [];
|
|
627
654
|
for (const task of enabledTasks) {
|
|
@@ -654,7 +681,25 @@ function createTaskScheduler(deps) {
|
|
|
654
681
|
// Tracks tasks currently running (prevents concurrent runs of the same task)
|
|
655
682
|
const runningTasks = new Set();
|
|
656
683
|
|
|
684
|
+
// Wake detection: if tick interval far exceeds expected, system likely slept (macOS lid close).
|
|
685
|
+
// Use 5min floor to avoid false triggers from CPU load spikes or GC pauses.
|
|
686
|
+
let lastTickTime = Date.now();
|
|
687
|
+
const WAKE_THRESHOLD = Math.max(checkIntervalSec * 3 * 1000, 5 * 60 * 1000);
|
|
688
|
+
|
|
657
689
|
const timer = setInterval(() => {
|
|
690
|
+
const tickNow = Date.now();
|
|
691
|
+
const tickElapsed = tickNow - lastTickTime;
|
|
692
|
+
lastTickTime = tickNow;
|
|
693
|
+
|
|
694
|
+
if (tickElapsed > WAKE_THRESHOLD) {
|
|
695
|
+
log('FATAL', `[WAKE-DETECT] System resumed after ${Math.round(tickElapsed / 1000)}s sleep — restarting daemon to rebuild connections`);
|
|
696
|
+
const st = loadState();
|
|
697
|
+
st.wake_restart = new Date().toISOString();
|
|
698
|
+
st.wake_sleep_seconds = Math.round(tickElapsed / 1000);
|
|
699
|
+
saveState(st);
|
|
700
|
+
process.exit(1); // caffeinate will restart us with fresh connections
|
|
701
|
+
}
|
|
702
|
+
|
|
658
703
|
// ① Physiological heartbeat (zero token, pure awareness)
|
|
659
704
|
physiologicalHeartbeat(config);
|
|
660
705
|
|
|
@@ -686,12 +731,14 @@ function createTaskScheduler(deps) {
|
|
|
686
731
|
|
|
687
732
|
if (runningTasks.has(task.name)) {
|
|
688
733
|
// Task is still running; skip this cycle and keep full interval cadence.
|
|
689
|
-
nextRun[task.name] =
|
|
734
|
+
nextRun[task.name] = safeNextRun(task.name, schedule, currentTime).next;
|
|
690
735
|
log('WARN', `Task ${task.name} still running — skipping this interval`);
|
|
691
736
|
continue;
|
|
692
737
|
}
|
|
693
738
|
|
|
694
|
-
|
|
739
|
+
const { next: nextRunTime, failed: schedFailed } = safeNextRun(task.name, schedule, currentTime);
|
|
740
|
+
nextRun[task.name] = nextRunTime;
|
|
741
|
+
if (schedFailed) continue; // back off, skip execution this cycle
|
|
695
742
|
runningTasks.add(task.name);
|
|
696
743
|
// executeTask now returns a Promise (async, non-blocking, process-group kill)
|
|
697
744
|
Promise.resolve(executeTask(task, config))
|
|
@@ -714,13 +761,20 @@ function createTaskScheduler(deps) {
|
|
|
714
761
|
}
|
|
715
762
|
|
|
716
763
|
// Skill evolution: check queue and notify user of actionable items
|
|
717
|
-
|
|
764
|
+
// Can be disabled via daemon.yaml: skill_evolution_notify: false
|
|
765
|
+
const skillEvolutionNotifyEnabled = !config.daemon || config.daemon.skill_evolution_notify !== false;
|
|
766
|
+
if (skillEvolution && skillEvolutionNotifyEnabled) {
|
|
718
767
|
try {
|
|
719
768
|
const notifications = skillEvolution.checkEvolutionQueue();
|
|
720
769
|
for (const item of notifications) {
|
|
721
770
|
let msg = '';
|
|
722
771
|
const idHint = item.id ? `\nID: \`${item.id}\`` : '';
|
|
723
|
-
if (item.type === '
|
|
772
|
+
if (item.type === 'workflow_proposal') {
|
|
773
|
+
msg = `🔄 *工作流技能建议*\n检测到重复工作流: ${item.search_hint || item.reason}`;
|
|
774
|
+
if (item.tools_signature && item.tools_signature.length) msg += `\n工具: ${item.tools_signature.join(', ')}`;
|
|
775
|
+
if (item.example_prompt) msg += `\n示例: "${item.example_prompt}"`;
|
|
776
|
+
if (item.evidence_count) msg += `\n出现次数: ${item.evidence_count}`;
|
|
777
|
+
} else if (item.type === 'skill_gap') {
|
|
724
778
|
msg = `🧬 *技能缺口检测*\n${item.reason}`;
|
|
725
779
|
if (item.search_hint) msg += `\n搜索建议: \`${item.search_hint}\``;
|
|
726
780
|
} else if (item.type === 'skill_fix') {
|
|
@@ -728,9 +782,14 @@ function createTaskScheduler(deps) {
|
|
|
728
782
|
} else if (item.type === 'user_complaint') {
|
|
729
783
|
msg = `⚠️ *技能反馈*\n技能 \`${item.skill_name}\` 收到用户反馈\n${item.reason}`;
|
|
730
784
|
}
|
|
731
|
-
if (msg && item.id
|
|
732
|
-
|
|
733
|
-
if (msg &&
|
|
785
|
+
if (msg && item.id && item.type === 'workflow_proposal') {
|
|
786
|
+
msg += `${idHint}\n处理: \`/skill-evo approve ${item.id}\` 或 \`/skill-evo dismiss ${item.id}\``;
|
|
787
|
+
} else if (msg && item.id) {
|
|
788
|
+
msg += `${idHint}\n处理: \`/skill-evo done ${item.id}\` 或 \`/skill-evo dismiss ${item.id}\``;
|
|
789
|
+
} else if (msg) msg += idHint;
|
|
790
|
+
// Skill notifications go only to personal chats, not agent group chats
|
|
791
|
+
const skillNotifyFn = notifyPersonalFn || notifyFn;
|
|
792
|
+
if (msg && skillNotifyFn) skillNotifyFn(msg);
|
|
734
793
|
}
|
|
735
794
|
} catch (e) { log('WARN', `Skill evolution queue check failed: ${e.message}`); }
|
|
736
795
|
}
|
|
@@ -166,7 +166,7 @@ const ACTION_GROUPS = {
|
|
|
166
166
|
const ROLE_DEFAULT_ACTIONS = {
|
|
167
167
|
admin: Object.keys(ACTION_GROUPS), // 全部权限
|
|
168
168
|
member: ['query'], // 默认只能问答
|
|
169
|
-
stranger: [],
|
|
169
|
+
stranger: ['query'], // 仅基础问答(只读)
|
|
170
170
|
};
|
|
171
171
|
|
|
172
172
|
// 不可赋予 member 的 admin 专属 action
|
|
@@ -182,6 +182,7 @@ const ADMIN_ONLY_ACTIONS = new Set(['system', 'agent', 'config', 'admin_acl']);
|
|
|
182
182
|
*/
|
|
183
183
|
function resolveUserCtx(senderId, config) {
|
|
184
184
|
const userData = loadUsers();
|
|
185
|
+
const hasConfiguredUsers = !!(userData && userData.users && Object.keys(userData.users).length > 0);
|
|
185
186
|
|
|
186
187
|
let role, name, allowedActions;
|
|
187
188
|
|
|
@@ -213,9 +214,25 @@ function resolveUserCtx(senderId, config) {
|
|
|
213
214
|
name = senderId.slice(-6);
|
|
214
215
|
allowedActions = ROLE_DEFAULT_ACTIONS.admin;
|
|
215
216
|
} else {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
if (!hasConfiguredUsers) {
|
|
218
|
+
// Bootstrap only once: persist the first seen sender as admin.
|
|
219
|
+
const next = {
|
|
220
|
+
default_role: userData.default_role || 'stranger',
|
|
221
|
+
users: { ...(userData.users || {}) },
|
|
222
|
+
};
|
|
223
|
+
next.users[senderId] = {
|
|
224
|
+
role: 'admin',
|
|
225
|
+
name: senderId.slice(-6),
|
|
226
|
+
};
|
|
227
|
+
try { saveUsers(next); } catch { /* non-fatal: fallback to in-memory role */ }
|
|
228
|
+
role = 'admin';
|
|
229
|
+
name = senderId.slice(-6);
|
|
230
|
+
allowedActions = ROLE_DEFAULT_ACTIONS.admin;
|
|
231
|
+
} else {
|
|
232
|
+
role = userData.default_role || 'stranger';
|
|
233
|
+
name = senderId.slice(-6);
|
|
234
|
+
allowedActions = ROLE_DEFAULT_ACTIONS[role] || [];
|
|
235
|
+
}
|
|
219
236
|
}
|
|
220
237
|
}
|
|
221
238
|
}
|
|
@@ -313,7 +330,7 @@ function handleUserCommand(text, userCtx) {
|
|
|
313
330
|
|
|
314
331
|
if (sub === 'add') {
|
|
315
332
|
// /user add <open_id> <role> [name...]
|
|
316
|
-
const [, ,
|
|
333
|
+
const [, , uid, role, ...nameParts] = args;
|
|
317
334
|
if (!uid || !role) return { handled: true, reply: '用法: /user add <open_id> <role> [name]' };
|
|
318
335
|
// [S2] open_id 格式校验
|
|
319
336
|
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法(应为 10-64 位字母数字下划线)' };
|
|
@@ -328,7 +345,7 @@ function handleUserCommand(text, userCtx) {
|
|
|
328
345
|
}
|
|
329
346
|
|
|
330
347
|
if (sub === 'role') {
|
|
331
|
-
const [, ,
|
|
348
|
+
const [, , uid, role] = args;
|
|
332
349
|
if (!uid || !role) return { handled: true, reply: '用法: /user role <open_id> <role>' };
|
|
333
350
|
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法' };
|
|
334
351
|
if (!['admin', 'member', 'stranger'].includes(role)) {
|
|
@@ -343,7 +360,7 @@ function handleUserCommand(text, userCtx) {
|
|
|
343
360
|
}
|
|
344
361
|
|
|
345
362
|
if (sub === 'grant') {
|
|
346
|
-
const [, ,
|
|
363
|
+
const [, , uid, action] = args;
|
|
347
364
|
if (!uid || !action) return { handled: true, reply: '用法: /user grant <open_id> <action>' };
|
|
348
365
|
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法' };
|
|
349
366
|
if (ADMIN_ONLY_ACTIONS.has(action)) {
|
|
@@ -364,7 +381,7 @@ function handleUserCommand(text, userCtx) {
|
|
|
364
381
|
}
|
|
365
382
|
|
|
366
383
|
if (sub === 'revoke') {
|
|
367
|
-
const [, ,
|
|
384
|
+
const [, , uid, action] = args;
|
|
368
385
|
if (!uid || !action) return { handled: true, reply: '用法: /user revoke <open_id> <action>' };
|
|
369
386
|
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法' };
|
|
370
387
|
const data = loadUsers();
|
|
@@ -376,7 +393,7 @@ function handleUserCommand(text, userCtx) {
|
|
|
376
393
|
}
|
|
377
394
|
|
|
378
395
|
if (sub === 'remove') {
|
|
379
|
-
const [, ,
|
|
396
|
+
const [, , uid] = args;
|
|
380
397
|
if (!uid) return { handled: true, reply: '用法: /user remove <open_id>' };
|
|
381
398
|
if (!isValidOpenId(uid)) return { handled: true, reply: '❌ open_id 格式不合法' };
|
|
382
399
|
const data = loadUsers();
|
package/scripts/daemon.js
CHANGED
|
@@ -144,6 +144,7 @@ const { createFileBrowser } = require('./daemon-file-browser');
|
|
|
144
144
|
const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
|
|
145
145
|
const { createNotifier } = require('./daemon-notify');
|
|
146
146
|
const { createClaudeEngine } = require('./daemon-claude-engine');
|
|
147
|
+
const { createEngineRuntimeFactory, detectDefaultEngine, ENGINE_DISTILL_MAP } = require('./daemon-engine-runtime');
|
|
147
148
|
const { createCommandRouter } = require('./daemon-command-router');
|
|
148
149
|
const { createTaskScheduler } = require('./daemon-task-scheduler');
|
|
149
150
|
const { createAgentTools } = require('./daemon-agent-tools');
|
|
@@ -932,6 +933,15 @@ function handleDispatchItem(item, config) {
|
|
|
932
933
|
log('WARN', `dispatch: unknown target "${item.target}"`);
|
|
933
934
|
return;
|
|
934
935
|
}
|
|
936
|
+
// 安全护栏:禁止 agent 主动 dispatch 到 personal(防止 LLM 幻觉乱发消息给小美)
|
|
937
|
+
// personal 只允许用户本人触发,或来源为 user/unknown 的系统任务
|
|
938
|
+
const _agentSources = new Set(Object.keys((config.projects) || {}));
|
|
939
|
+
const isFromAgent = _agentSources.has(item.from) || item.from === '_claude_session';
|
|
940
|
+
const targetProject = config.projects?.[item.target] || {};
|
|
941
|
+
if (isFromAgent && targetProject.guard === 'user-only') {
|
|
942
|
+
log('WARN', `dispatch: blocked agent "${item.from}" → "${item.target}" (user-only guard)`);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
935
945
|
log('INFO', `Dispatch: ${item.from || '?'} → ${item.target}: ${item.prompt.slice(0, 60)}`);
|
|
936
946
|
let pendingReplyFn = null;
|
|
937
947
|
let streamOptions = null;
|
|
@@ -1109,13 +1119,11 @@ const {
|
|
|
1109
1119
|
/**
|
|
1110
1120
|
* Attach chatId to the most recent session in projCwd, or create a new one.
|
|
1111
1121
|
*/
|
|
1112
|
-
function attachOrCreateSession(chatId, projCwd, name) {
|
|
1113
|
-
|
|
1122
|
+
function attachOrCreateSession(chatId, projCwd, name, engine) {
|
|
1123
|
+
engine = engine || getDefaultEngine();
|
|
1114
1124
|
// Virtual chatIds (_agent_* / _scope_*) are isolated from real user chats.
|
|
1115
1125
|
// This avoids cross-context session collisions between user chat and dispatch flows.
|
|
1116
|
-
|
|
1117
|
-
state.sessions[chatId] = { id: newSess.id, cwd: projCwd, started: false };
|
|
1118
|
-
saveState(state);
|
|
1126
|
+
createSession(chatId, projCwd, name || '', engine);
|
|
1119
1127
|
}
|
|
1120
1128
|
|
|
1121
1129
|
/**
|
|
@@ -1264,7 +1272,7 @@ const {
|
|
|
1264
1272
|
watchSessionFiles(); // 热加载:手机端新建 session 后桌面无需重启
|
|
1265
1273
|
|
|
1266
1274
|
// Active Claude processes per chat (for /stop)
|
|
1267
|
-
const activeProcesses = new Map(); // chatId -> { child, aborted }
|
|
1275
|
+
const activeProcesses = new Map(); // chatId -> { child, aborted, engine, killSignal }
|
|
1268
1276
|
|
|
1269
1277
|
// Activity tracking for idle/sleep detection
|
|
1270
1278
|
let lastInteractionTime = Date.now(); // updated on every incoming message
|
|
@@ -1301,13 +1309,19 @@ function isUserIdle() {
|
|
|
1301
1309
|
return activeProcesses.size === 0;
|
|
1302
1310
|
}
|
|
1303
1311
|
|
|
1304
|
-
//
|
|
1305
|
-
const ACTIVE_PIDS_FILE = path.join(HOME, '.metame', '
|
|
1312
|
+
// Persist child PIDs so next daemon startup can kill orphans
|
|
1313
|
+
const ACTIVE_PIDS_FILE = path.join(HOME, '.metame', 'active_agent_pids.json');
|
|
1306
1314
|
function saveActivePids() {
|
|
1307
1315
|
try {
|
|
1308
1316
|
const pids = {};
|
|
1309
1317
|
for (const [chatId, proc] of activeProcesses) {
|
|
1310
|
-
if (proc.child && proc.child.pid)
|
|
1318
|
+
if (proc.child && proc.child.pid) {
|
|
1319
|
+
pids[chatId] = {
|
|
1320
|
+
pid: proc.child.pid,
|
|
1321
|
+
engine: proc.engine || getDefaultEngine(),
|
|
1322
|
+
killSignal: proc.killSignal || 'SIGTERM',
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1311
1325
|
}
|
|
1312
1326
|
fs.writeFileSync(ACTIVE_PIDS_FILE, JSON.stringify(pids), 'utf8');
|
|
1313
1327
|
} catch { }
|
|
@@ -1321,22 +1335,63 @@ function killOrphanPids() {
|
|
|
1321
1335
|
try {
|
|
1322
1336
|
if (!fs.existsSync(ACTIVE_PIDS_FILE)) return;
|
|
1323
1337
|
const pids = JSON.parse(fs.readFileSync(ACTIVE_PIDS_FILE, 'utf8'));
|
|
1324
|
-
for (const [chatId,
|
|
1338
|
+
for (const [chatId, rec] of Object.entries(pids)) {
|
|
1325
1339
|
try {
|
|
1326
|
-
|
|
1340
|
+
const pid = typeof rec === 'number' ? rec : Number(rec && rec.pid);
|
|
1341
|
+
if (!Number.isFinite(pid) || pid <= 0) continue;
|
|
1342
|
+
// Safety: only kill if PID still belongs to a known agent process (prevent PID reuse accidents)
|
|
1327
1343
|
const comm = getProcessName(pid);
|
|
1328
|
-
|
|
1329
|
-
|
|
1344
|
+
const isKnownAgent = !!comm && (comm.includes('claude') || comm.includes('codex'));
|
|
1345
|
+
if (!isKnownAgent) {
|
|
1346
|
+
log('WARN', `Skipping PID ${pid} (chatId: ${chatId}): process is "${comm}", not claude/codex`);
|
|
1330
1347
|
continue;
|
|
1331
1348
|
}
|
|
1332
1349
|
process.kill(pid, 'SIGKILL');
|
|
1333
|
-
log('INFO', `Killed orphan
|
|
1350
|
+
log('INFO', `Killed orphan agent PID ${pid} (chatId: ${chatId})`);
|
|
1334
1351
|
} catch { }
|
|
1335
1352
|
}
|
|
1336
1353
|
fs.unlinkSync(ACTIVE_PIDS_FILE);
|
|
1337
1354
|
} catch { }
|
|
1338
1355
|
}
|
|
1339
1356
|
|
|
1357
|
+
const detectedEngine = detectDefaultEngine({ fs, execSync });
|
|
1358
|
+
let _defaultEngine = loadState().default_engine || detectedEngine;
|
|
1359
|
+
if (providerMod && typeof providerMod.setEngine === 'function') {
|
|
1360
|
+
providerMod.setEngine(_defaultEngine);
|
|
1361
|
+
}
|
|
1362
|
+
log('INFO', `Default engine: ${_defaultEngine} (detected: ${detectedEngine})`);
|
|
1363
|
+
|
|
1364
|
+
function getDefaultEngine() {
|
|
1365
|
+
return _defaultEngine;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function setDefaultEngine(engine) {
|
|
1369
|
+
_defaultEngine = engine;
|
|
1370
|
+
const st = loadState();
|
|
1371
|
+
st.default_engine = engine;
|
|
1372
|
+
saveState(st);
|
|
1373
|
+
if (providerMod) {
|
|
1374
|
+
// Couple distill model
|
|
1375
|
+
if (typeof providerMod.setDistillModel === 'function') {
|
|
1376
|
+
const paired = ENGINE_DISTILL_MAP[engine] || ENGINE_DISTILL_MAP.claude;
|
|
1377
|
+
try { providerMod.setDistillModel(paired); } catch { /* ignore */ }
|
|
1378
|
+
}
|
|
1379
|
+
// Couple distill binary
|
|
1380
|
+
if (typeof providerMod.setEngine === 'function') {
|
|
1381
|
+
try { providerMod.setEngine(engine); } catch { /* ignore */ }
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const getEngineRuntime = createEngineRuntimeFactory({
|
|
1387
|
+
fs,
|
|
1388
|
+
path,
|
|
1389
|
+
HOME,
|
|
1390
|
+
execSync,
|
|
1391
|
+
CLAUDE_BIN,
|
|
1392
|
+
getActiveProviderEnv,
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1340
1395
|
const {
|
|
1341
1396
|
checkPrecondition,
|
|
1342
1397
|
executeTask,
|
|
@@ -1403,6 +1458,8 @@ const { handleAdminCommand } = createAdminCommandHandler({
|
|
|
1403
1458
|
getMessageQueue: () => messageQueue,
|
|
1404
1459
|
loadState,
|
|
1405
1460
|
saveState,
|
|
1461
|
+
getDefaultEngine,
|
|
1462
|
+
setDefaultEngine,
|
|
1406
1463
|
});
|
|
1407
1464
|
|
|
1408
1465
|
const { handleSessionCommand } = createSessionCommandHandler({
|
|
@@ -1430,6 +1487,7 @@ const { handleSessionCommand } = createSessionCommandHandler({
|
|
|
1430
1487
|
sessionRichLabel,
|
|
1431
1488
|
buildSessionCardElements,
|
|
1432
1489
|
sessionLabel,
|
|
1490
|
+
getDefaultEngine,
|
|
1433
1491
|
});
|
|
1434
1492
|
|
|
1435
1493
|
// Message queue for messages received while a task is running
|
|
@@ -1472,6 +1530,8 @@ const { spawnClaudeAsync, askClaude } = createClaudeEngine({
|
|
|
1472
1530
|
touchInteraction,
|
|
1473
1531
|
statusThrottleMs: STATUS_THROTTLE_MS,
|
|
1474
1532
|
fallbackThrottleMs: FALLBACK_THROTTLE_MS,
|
|
1533
|
+
getEngineRuntime,
|
|
1534
|
+
getDefaultEngine,
|
|
1475
1535
|
});
|
|
1476
1536
|
|
|
1477
1537
|
const agentTools = createAgentTools({
|
|
@@ -1531,6 +1591,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
|
|
|
1531
1591
|
attachOrCreateSession,
|
|
1532
1592
|
agentFlowTtlMs: getAgentFlowTtlMs,
|
|
1533
1593
|
agentBindTtlMs: getAgentBindTtlMs,
|
|
1594
|
+
getDefaultEngine,
|
|
1534
1595
|
});
|
|
1535
1596
|
|
|
1536
1597
|
// Caffeinate process for /nosleep toggle (macOS only)
|
|
@@ -1604,6 +1665,7 @@ const { handleCommand } = createCommandRouter({
|
|
|
1604
1665
|
pendingAgentFlows,
|
|
1605
1666
|
pendingActivations,
|
|
1606
1667
|
agentFlowTtlMs: getAgentFlowTtlMs,
|
|
1668
|
+
getDefaultEngine,
|
|
1607
1669
|
});
|
|
1608
1670
|
|
|
1609
1671
|
// Bind handleCommand for agent dispatch (must come after handleCommand definition)
|
|
@@ -1749,12 +1811,13 @@ async function main() {
|
|
|
1749
1811
|
});
|
|
1750
1812
|
const notifyFn = notifier.notify;
|
|
1751
1813
|
const adminNotifyFn = notifier.notifyAdmin;
|
|
1814
|
+
const notifyPersonalFn = notifier.notifyPersonal;
|
|
1752
1815
|
|
|
1753
1816
|
// Start dispatch socket server (low-latency IPC, fallback: file polling still works)
|
|
1754
1817
|
const dispatchSocket = startDispatchSocket(() => config);
|
|
1755
1818
|
|
|
1756
1819
|
// Start heartbeat scheduler
|
|
1757
|
-
let heartbeatTimer = startHeartbeat(config, notifyFn);
|
|
1820
|
+
let heartbeatTimer = startHeartbeat(config, notifyFn, notifyPersonalFn);
|
|
1758
1821
|
|
|
1759
1822
|
const runtimeWatchers = setupRuntimeWatchers({
|
|
1760
1823
|
fs,
|
|
@@ -1769,6 +1832,7 @@ async function main() {
|
|
|
1769
1832
|
log,
|
|
1770
1833
|
notifyFn,
|
|
1771
1834
|
adminNotifyFn,
|
|
1835
|
+
notifyPersonalFn,
|
|
1772
1836
|
activeProcesses,
|
|
1773
1837
|
getConfig: () => config,
|
|
1774
1838
|
setConfig: (next) => { config = next; },
|
|
@@ -1819,10 +1883,10 @@ async function main() {
|
|
|
1819
1883
|
if (feishuBridge) feishuBridge.stop();
|
|
1820
1884
|
// Stop QMD semantic search daemon if it was started
|
|
1821
1885
|
try { require('./qmd-client').stopDaemon(); } catch { /* ignore */ }
|
|
1822
|
-
// Kill all tracked
|
|
1886
|
+
// Kill all tracked engine process groups before exiting (covers sub-agents too)
|
|
1823
1887
|
for (const [cid, proc] of activeProcesses) {
|
|
1824
1888
|
try { process.kill(-proc.child.pid, 'SIGKILL'); } catch { try { proc.child.kill('SIGKILL'); } catch { } }
|
|
1825
|
-
log('INFO', `Shutdown: killed
|
|
1889
|
+
log('INFO', `Shutdown: killed engine process group for chatId ${cid}`);
|
|
1826
1890
|
}
|
|
1827
1891
|
activeProcesses.clear();
|
|
1828
1892
|
try { if (fs.existsSync(ACTIVE_PIDS_FILE)) fs.unlinkSync(ACTIVE_PIDS_FILE); } catch { }
|