metame-cli 1.5.4 → 1.5.5
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 +6 -1
- package/index.js +277 -55
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +17 -5
- package/scripts/daemon-admin-commands.js +264 -62
- package/scripts/daemon-agent-commands.js +188 -66
- package/scripts/daemon-bridges.js +447 -48
- package/scripts/daemon-claude-engine.js +650 -103
- package/scripts/daemon-command-router.js +134 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +2 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +106 -50
- package/scripts/daemon-file-browser.js +63 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +34 -2
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +610 -181
- package/scripts/docs/hook-config.md +7 -4
- package/scripts/docs/maintenance-manual.md +8 -1
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +9 -40
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +150 -11
- package/scripts/hooks/intent-agent-manage.js +0 -50
- package/scripts/hooks/intent-hook-config.js +0 -28
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { classifyTaskUsage } = require('./usage-classifier');
|
|
4
4
|
const { normalizeModel } = require('./daemon-task-scheduler');
|
|
5
|
+
const { createCommandSessionResolver } = require('./daemon-command-session-route');
|
|
5
6
|
|
|
6
7
|
function createExecCommandHandler(deps) {
|
|
7
8
|
const {
|
|
@@ -24,9 +25,19 @@ function createExecCommandHandler(deps) {
|
|
|
24
25
|
getSessionName,
|
|
25
26
|
createSession,
|
|
26
27
|
findSessionFile,
|
|
28
|
+
findCodexSessionFile = null,
|
|
27
29
|
loadConfig,
|
|
28
30
|
getDistillModel,
|
|
31
|
+
getDefaultEngine = () => 'claude',
|
|
29
32
|
} = deps;
|
|
33
|
+
const { getActiveSession } = createCommandSessionResolver({
|
|
34
|
+
path,
|
|
35
|
+
loadConfig,
|
|
36
|
+
loadState,
|
|
37
|
+
getSession,
|
|
38
|
+
getSessionForEngine,
|
|
39
|
+
getDefaultEngine,
|
|
40
|
+
});
|
|
30
41
|
|
|
31
42
|
function truncateOutput(output, maxLen = 4000) {
|
|
32
43
|
const text = (output || '').trim() || '(no output)';
|
|
@@ -235,7 +246,7 @@ function createExecCommandHandler(deps) {
|
|
|
235
246
|
const signal = proc.killSignal || 'SIGTERM';
|
|
236
247
|
try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
|
|
237
248
|
}
|
|
238
|
-
const session =
|
|
249
|
+
const { session } = getActiveSession(chatId);
|
|
239
250
|
const name = session ? getSessionName(session.id) : null;
|
|
240
251
|
const label = name || (session ? session.id.slice(0, 8) : 'none');
|
|
241
252
|
await bot.sendMessage(chatId, `🔄 Session restarted. MCP/config reloaded.\n📁 ${session ? path.basename(session.cwd) : '~'} [${label}]`);
|
|
@@ -244,52 +255,84 @@ function createExecCommandHandler(deps) {
|
|
|
244
255
|
|
|
245
256
|
// /compact — compress current session context to save tokens
|
|
246
257
|
if (text === '/compact') {
|
|
247
|
-
const
|
|
248
|
-
const engine = rawSession?.engine || 'claude';
|
|
249
|
-
const session = getSessionForEngine(chatId, engine);
|
|
258
|
+
const { sessionKey, engine, session } = getActiveSession(chatId);
|
|
250
259
|
if (!session || !session.id || !session.started) {
|
|
251
260
|
await bot.sendMessage(chatId, '❌ No active session to compact.');
|
|
252
261
|
return true;
|
|
253
262
|
}
|
|
254
|
-
if (String(engine).toLowerCase() === 'codex') {
|
|
255
|
-
await bot.sendMessage(chatId, '⚠️ Codex 会话暂不支持 /compact,请继续在同一会话里对话。');
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
263
|
await bot.sendMessage(chatId, '🗜 Compacting session...');
|
|
259
264
|
|
|
260
|
-
// Step 1: Read conversation
|
|
261
|
-
const
|
|
262
|
-
if (!jsonlPath) {
|
|
263
|
-
await bot.sendMessage(chatId, '❌ Session file not found.');
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
265
|
+
// Step 1: Read conversation (JSONL for Claude, rollout JSONL for Codex)
|
|
266
|
+
const isCodex = String(engine).toLowerCase() === 'codex';
|
|
266
267
|
const messages = [];
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
268
|
+
if (isCodex) {
|
|
269
|
+
const codexFile = findCodexSessionFile && findCodexSessionFile(session.id);
|
|
270
|
+
if (!codexFile) {
|
|
271
|
+
await bot.sendMessage(chatId, '❌ Codex session file not found.');
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const lines = fs.readFileSync(codexFile, 'utf8').split('\n').filter(Boolean);
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
try {
|
|
278
|
+
const obj = JSON.parse(line);
|
|
279
|
+
if (obj.type !== 'response_item') continue;
|
|
280
|
+
const p = obj.payload;
|
|
281
|
+
if (!p || p.type !== 'message') continue;
|
|
282
|
+
const role = String(p.role || '').toLowerCase();
|
|
283
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
284
|
+
const content = p.content || p;
|
|
275
285
|
let textContent = '';
|
|
276
286
|
if (typeof content === 'string') {
|
|
277
287
|
textContent = content;
|
|
278
288
|
} else if (Array.isArray(content)) {
|
|
279
|
-
textContent = content
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
textContent = content.map(c => {
|
|
290
|
+
if (typeof c === 'string') return c;
|
|
291
|
+
if (c && typeof c.text === 'string') return c.text;
|
|
292
|
+
return '';
|
|
293
|
+
}).filter(Boolean).join(' ');
|
|
283
294
|
}
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
textContent = textContent.trim();
|
|
296
|
+
if (textContent) messages.push({ role, text: textContent });
|
|
297
|
+
} catch { /* skip malformed lines */ }
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
await bot.sendMessage(chatId, `❌ Cannot read Codex session: ${e.message}`);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
const jsonlPath = findSessionFile(session.id);
|
|
305
|
+
if (!jsonlPath) {
|
|
306
|
+
await bot.sendMessage(chatId, '❌ Session file not found.');
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const lines = fs.readFileSync(jsonlPath, 'utf8').split('\n').filter(Boolean);
|
|
311
|
+
for (const line of lines) {
|
|
312
|
+
try {
|
|
313
|
+
const obj = JSON.parse(line);
|
|
314
|
+
if (obj.type === 'user' || obj.type === 'assistant') {
|
|
315
|
+
const msg = obj.message || {};
|
|
316
|
+
const content = msg.content;
|
|
317
|
+
let textContent = '';
|
|
318
|
+
if (typeof content === 'string') {
|
|
319
|
+
textContent = content;
|
|
320
|
+
} else if (Array.isArray(content)) {
|
|
321
|
+
textContent = content
|
|
322
|
+
.filter(c => c.type === 'text')
|
|
323
|
+
.map(c => c.text || '')
|
|
324
|
+
.join(' ');
|
|
325
|
+
}
|
|
326
|
+
if (textContent.trim()) {
|
|
327
|
+
messages.push({ role: obj.type, text: textContent.trim() });
|
|
328
|
+
}
|
|
286
329
|
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
330
|
+
} catch { /* skip malformed lines */ }
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
await bot.sendMessage(chatId, `❌ Cannot read session: ${e.message}`);
|
|
334
|
+
return true;
|
|
289
335
|
}
|
|
290
|
-
} catch (e) {
|
|
291
|
-
await bot.sendMessage(chatId, `❌ Cannot read session: ${e.message}`);
|
|
292
|
-
return true;
|
|
293
336
|
}
|
|
294
337
|
|
|
295
338
|
if (messages.length === 0) {
|
|
@@ -325,23 +368,36 @@ function createExecCommandHandler(deps) {
|
|
|
325
368
|
}
|
|
326
369
|
|
|
327
370
|
// Step 4: Create new session with the summary
|
|
328
|
-
const model = daemonCfg.model || 'opus';
|
|
329
371
|
const oldName = getSessionName(session.id);
|
|
330
|
-
const newSession = createSession(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return true;
|
|
339
|
-
}
|
|
340
|
-
// Mark as started
|
|
341
|
-
const state2 = loadState();
|
|
342
|
-
if (state2.sessions[chatId]) {
|
|
343
|
-
state2.sessions[chatId].started = true;
|
|
372
|
+
const newSession = createSession(sessionKey, session.cwd, oldName ? oldName + ' (compacted)' : '', engine);
|
|
373
|
+
if (isCodex) {
|
|
374
|
+
// Codex doesn't support --session-id init; store context for first-message injection in engine
|
|
375
|
+
const state2 = loadState();
|
|
376
|
+
if (!state2.sessions[sessionKey]) state2.sessions[sessionKey] = {};
|
|
377
|
+
if (!state2.sessions[sessionKey].engines) state2.sessions[sessionKey].engines = {};
|
|
378
|
+
if (!state2.sessions[sessionKey].engines[engine]) state2.sessions[sessionKey].engines[engine] = {};
|
|
379
|
+
state2.sessions[sessionKey].engines[engine].compactContext = output;
|
|
344
380
|
saveState(state2);
|
|
381
|
+
} else {
|
|
382
|
+
// Claude: warm up the new session immediately via --session-id
|
|
383
|
+
const model = daemonCfg.model || 'opus';
|
|
384
|
+
const initArgs = ['-p', '--session-id', newSession.id, '--model', model];
|
|
385
|
+
if (daemonCfg.dangerously_skip_permissions) initArgs.push('--dangerously-skip-permissions');
|
|
386
|
+
const preamble = buildProfilePreamble();
|
|
387
|
+
const initPrompt = preamble + `Here is the context from our previous session (compacted):\n\n${output}\n\nContext loaded. Ready to continue.`;
|
|
388
|
+
const { error: initErr } = await spawnClaudeAsync(initArgs, initPrompt, session.cwd, 60000);
|
|
389
|
+
if (initErr) {
|
|
390
|
+
await bot.sendMessage(chatId, `⚠️ Summary saved but new session init failed: ${initErr}`);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
const state2 = loadState();
|
|
394
|
+
if (state2.sessions[sessionKey]) {
|
|
395
|
+
state2.sessions[sessionKey].started = true;
|
|
396
|
+
if (state2.sessions[sessionKey].engines && state2.sessions[sessionKey].engines[engine]) {
|
|
397
|
+
state2.sessions[sessionKey].engines[engine].started = true;
|
|
398
|
+
}
|
|
399
|
+
saveState(state2);
|
|
400
|
+
}
|
|
345
401
|
}
|
|
346
402
|
const tokenEst = Math.round(output.length / 3.5);
|
|
347
403
|
await bot.sendMessage(chatId, `✅ Compacted! ~${tokenEst} tokens of context carried over.\nNew session: ${newSession.id.slice(0, 8)}`);
|
|
@@ -355,8 +411,8 @@ function createExecCommandHandler(deps) {
|
|
|
355
411
|
await bot.sendMessage(chatId, '用法: /publish 123456');
|
|
356
412
|
return true;
|
|
357
413
|
}
|
|
358
|
-
const session =
|
|
359
|
-
const cwd = session?.cwd || HOME;
|
|
414
|
+
const { route, session } = getActiveSession(chatId);
|
|
415
|
+
const cwd = session?.cwd || route.cwd || HOME;
|
|
360
416
|
await bot.sendMessage(chatId, `📦 npm publish --otp=${otp} ...`);
|
|
361
417
|
try {
|
|
362
418
|
const result = await runCommand('npm', ['publish', `--otp=${otp}`], { cwd, timeout: 60000 });
|
|
@@ -58,15 +58,71 @@ function createFileBrowser(deps) {
|
|
|
58
58
|
return entry.path;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function appendDebugLog(message) {
|
|
62
|
+
try {
|
|
63
|
+
fs.appendFileSync(path.join(HOME, '.metame', 'daemon.log'), `[${new Date().toISOString()}] [INFO] [file-browser] ${message}\n`);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
async function sendFileButtons(bot, chatId, files) {
|
|
62
|
-
if (
|
|
68
|
+
if (files.size === 0) return;
|
|
63
69
|
const validFiles = [...files].filter(f => fs.existsSync(f));
|
|
64
|
-
if (validFiles.length === 0) return;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
if (validFiles.length === 0) return [];
|
|
71
|
+
const fallbackText = `📂 文件已生成:\n${validFiles.map(filePath => `- ${filePath}`).join('\n')}`;
|
|
72
|
+
const sentMessages = [];
|
|
73
|
+
appendDebugLog(`sendFileButtons chat=${chatId} hasSendFile=${typeof bot.sendFile === 'function'} hasSendButtons=${typeof bot.sendButtons === 'function'} hasEditMessage=${typeof bot.editMessage === 'function'} hasSendCard=${typeof bot.sendCard === 'function'} files=${validFiles.map(filePath => path.basename(filePath)).join(',')}`);
|
|
74
|
+
|
|
75
|
+
// Prefer direct file delivery when the adapter supports it.
|
|
76
|
+
// This matches the user's expectation on mobile and avoids brittle
|
|
77
|
+
// button-card transport on platforms like Feishu.
|
|
78
|
+
if (bot.sendFile) {
|
|
79
|
+
try {
|
|
80
|
+
appendDebugLog(`attempting direct sendFile chat=${chatId}`);
|
|
81
|
+
for (const filePath of validFiles) {
|
|
82
|
+
const msg = await bot.sendFile(chatId, filePath);
|
|
83
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
84
|
+
}
|
|
85
|
+
appendDebugLog(`direct sendFile success chat=${chatId} count=${sentMessages.length}`);
|
|
86
|
+
return sentMessages;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const detail = err && err.stack ? err.stack : String(err && err.message ? err.message : err);
|
|
89
|
+
appendDebugLog(`sendFile failed chat=${chatId} err=${detail.split('\n')[0]}`);
|
|
90
|
+
if (bot.sendMessage) {
|
|
91
|
+
const msg = await bot.sendMessage(chatId, fallbackText);
|
|
92
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
93
|
+
}
|
|
94
|
+
appendDebugLog(`sendFile fallback text chat=${chatId} count=${sentMessages.length}`);
|
|
95
|
+
return sentMessages;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isStreamCardBot = typeof bot.editMessage === 'function' || typeof bot.sendCard === 'function';
|
|
100
|
+
if (!bot.sendButtons || isStreamCardBot) {
|
|
101
|
+
appendDebugLog(`button fallback skipped chat=${chatId} hasSendButtons=${typeof bot.sendButtons === 'function'} isStreamCardBot=${isStreamCardBot}`);
|
|
102
|
+
if (bot.sendMessage) {
|
|
103
|
+
const msg = await bot.sendMessage(chatId, fallbackText);
|
|
104
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
105
|
+
}
|
|
106
|
+
return sentMessages;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
appendDebugLog(`attempting sendButtons chat=${chatId}`);
|
|
111
|
+
const buttons = validFiles.map(filePath => {
|
|
112
|
+
const shortId = cacheFile(filePath);
|
|
113
|
+
return [{ text: `📎 ${path.basename(filePath)}`, callback_data: `/file ${shortId}` }];
|
|
114
|
+
});
|
|
115
|
+
const msg = await bot.sendButtons(chatId, '📂 文件:', buttons);
|
|
116
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
117
|
+
appendDebugLog(`sendButtons success chat=${chatId}`);
|
|
118
|
+
} catch {
|
|
119
|
+
appendDebugLog(`sendButtons failed chat=${chatId}`);
|
|
120
|
+
if (bot.sendMessage) {
|
|
121
|
+
const msg = await bot.sendMessage(chatId, fallbackText);
|
|
122
|
+
if (msg && msg.message_id) sentMessages.push(msg);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return sentMessages;
|
|
70
126
|
}
|
|
71
127
|
|
|
72
128
|
async function sendDirPicker(bot, chatId, mode, title) {
|
package/scripts/daemon-notify.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
function resolveAdminChatId(adapterConfig = {}) {
|
|
4
|
+
const explicitId = String(adapterConfig.admin_chat_id || '').trim();
|
|
5
|
+
if (explicitId) return explicitId;
|
|
6
|
+
const ids = Array.isArray(adapterConfig.allowed_chat_ids) ? adapterConfig.allowed_chat_ids : [];
|
|
7
|
+
return ids[0] || null;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
function createNotifier(deps) {
|
|
4
11
|
const { log, getConfig, getBridges } = deps;
|
|
5
12
|
|
|
@@ -46,16 +53,23 @@ function createNotifier(deps) {
|
|
|
46
53
|
|
|
47
54
|
async function notifyAdmin(message) {
|
|
48
55
|
const config = getConfig();
|
|
49
|
-
const { feishuBridge } = getBridges();
|
|
56
|
+
const { feishuBridge, telegramBridge } = getBridges();
|
|
50
57
|
if (feishuBridge && feishuBridge.bot) {
|
|
51
|
-
const
|
|
52
|
-
const adminId = fsIds[0];
|
|
58
|
+
const adminId = resolveAdminChatId(config.feishu || {});
|
|
53
59
|
if (adminId) {
|
|
54
60
|
try { await feishuBridge.bot.sendMessage(adminId, message); } catch (e) {
|
|
55
61
|
log('ERROR', `Feishu admin notify failed ${adminId}: ${e.message}`);
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
if (telegramBridge && telegramBridge.bot) {
|
|
66
|
+
const adminId = resolveAdminChatId(config.telegram || {});
|
|
67
|
+
if (adminId) {
|
|
68
|
+
try { await telegramBridge.bot.sendMarkdown(adminId, message); } catch (e) {
|
|
69
|
+
log('ERROR', `Telegram admin notify failed ${adminId}: ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
/**
|
|
@@ -97,4 +111,4 @@ function createNotifier(deps) {
|
|
|
97
111
|
return { notify, notifyAdmin, notifyPersonal };
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
module.exports = { createNotifier };
|
|
114
|
+
module.exports = { createNotifier, resolveAdminChatId };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { createCommandSessionResolver } = require('./daemon-command-session-route');
|
|
4
|
+
|
|
3
5
|
function createOpsCommandHandler(deps) {
|
|
4
6
|
const {
|
|
5
7
|
fs,
|
|
@@ -7,9 +9,12 @@ function createOpsCommandHandler(deps) {
|
|
|
7
9
|
spawn,
|
|
8
10
|
execSync,
|
|
9
11
|
log,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadState,
|
|
10
14
|
messageQueue,
|
|
11
15
|
activeProcesses,
|
|
12
16
|
getSession,
|
|
17
|
+
getSessionForEngine,
|
|
13
18
|
listCheckpoints,
|
|
14
19
|
cpDisplayLabel,
|
|
15
20
|
truncateSessionToCheckpoint,
|
|
@@ -20,7 +25,16 @@ function createOpsCommandHandler(deps) {
|
|
|
20
25
|
cleanupCheckpoints,
|
|
21
26
|
getNoSleepProcess,
|
|
22
27
|
setNoSleepProcess,
|
|
28
|
+
getDefaultEngine = () => 'claude',
|
|
23
29
|
} = deps;
|
|
30
|
+
const { getActiveSession } = createCommandSessionResolver({
|
|
31
|
+
path,
|
|
32
|
+
loadConfig,
|
|
33
|
+
loadState,
|
|
34
|
+
getSession,
|
|
35
|
+
getSessionForEngine,
|
|
36
|
+
getDefaultEngine,
|
|
37
|
+
});
|
|
24
38
|
|
|
25
39
|
function clearMessageQueue(chatId) {
|
|
26
40
|
if (messageQueue.has(chatId)) {
|
|
@@ -45,7 +59,7 @@ function createOpsCommandHandler(deps) {
|
|
|
45
59
|
clearMessageQueue(chatId);
|
|
46
60
|
interruptActiveProcess(chatId);
|
|
47
61
|
|
|
48
|
-
const session =
|
|
62
|
+
const { session } = getActiveSession(chatId);
|
|
49
63
|
if (!session || !session.id) {
|
|
50
64
|
await bot.sendMessage(chatId, 'No active session to undo.');
|
|
51
65
|
return true;
|
|
@@ -168,7 +182,7 @@ function createOpsCommandHandler(deps) {
|
|
|
168
182
|
clearMessageQueue(chatId);
|
|
169
183
|
interruptActiveProcess(chatId);
|
|
170
184
|
|
|
171
|
-
const session2 =
|
|
185
|
+
const { session: session2 } = getActiveSession(chatId);
|
|
172
186
|
if (!session2 || !session2.id) {
|
|
173
187
|
await bot.sendMessage(chatId, 'No active session.');
|
|
174
188
|
return true;
|
|
@@ -46,10 +46,39 @@ function decodePacket(text) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function verifyPacket(packet, secret) {
|
|
49
|
-
if (!packet || typeof packet !== 'object' || !packet.sig) return false;
|
|
49
|
+
if (!packet || typeof packet !== 'object' || !packet.sig || !secret) return false;
|
|
50
50
|
return signPacket(packet, secret) === packet.sig;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function isValidPairCode(code) {
|
|
54
|
+
return /^\d{6}$/.test(String(code || '').trim());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function generatePairCode() {
|
|
58
|
+
return String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function deriveSecretFromPairCode(code, chatId) {
|
|
62
|
+
const normalizedCode = String(code || '').trim();
|
|
63
|
+
const normalizedChatId = String(chatId || '').trim();
|
|
64
|
+
if (!isValidPairCode(normalizedCode) || !normalizedChatId) return null;
|
|
65
|
+
return crypto
|
|
66
|
+
.createHash('sha256')
|
|
67
|
+
.update(`metame-remote-dispatch|${normalizedChatId}|${normalizedCode}`)
|
|
68
|
+
.digest('hex');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getRemoteDispatchStatus(config) {
|
|
72
|
+
const rd = normalizeRemoteDispatchConfig(config);
|
|
73
|
+
if (!rd) return null;
|
|
74
|
+
return {
|
|
75
|
+
selfPeer: rd.selfPeer,
|
|
76
|
+
chatId: rd.chatId,
|
|
77
|
+
mode: 'pair-code',
|
|
78
|
+
hasSecret: !!rd.secret,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
// TTL dedup map — prevents replayed packets (5 min window)
|
|
54
83
|
const _seenPackets = new Map();
|
|
55
84
|
const DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
@@ -57,7 +86,6 @@ const DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
|
57
86
|
function isDuplicate(packetId) {
|
|
58
87
|
if (!packetId) return false;
|
|
59
88
|
const now = Date.now();
|
|
60
|
-
// Evict expired entries on each check
|
|
61
89
|
for (const [id, ts] of _seenPackets) {
|
|
62
90
|
if (now - ts > DEDUP_TTL_MS) _seenPackets.delete(id);
|
|
63
91
|
}
|
|
@@ -77,6 +105,10 @@ module.exports = {
|
|
|
77
105
|
encodePacket,
|
|
78
106
|
decodePacket,
|
|
79
107
|
verifyPacket,
|
|
108
|
+
isValidPairCode,
|
|
109
|
+
generatePairCode,
|
|
110
|
+
deriveSecretFromPairCode,
|
|
111
|
+
getRemoteDispatchStatus,
|
|
80
112
|
isDuplicate,
|
|
81
113
|
isRemoteMember,
|
|
82
114
|
};
|