metame-cli 1.6.2 → 1.6.4
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 +12 -2
- package/index.js +86 -6
- package/package.json +1 -1
- package/scripts/agent-intent-shared.js +11 -2
- package/scripts/core/session-source-db.js +125 -0
- package/scripts/daemon-agent-intent.js +51 -15
- package/scripts/daemon-agent-tools.js +52 -3
- package/scripts/daemon-agent-workflow.js +98 -0
- package/scripts/daemon-bridges.js +9 -2
- package/scripts/daemon-claude-engine.js +12 -3
- package/scripts/daemon-command-router.js +9 -20
- package/scripts/daemon-engine-runtime.js +16 -6
- package/scripts/daemon-runtime-lifecycle.js +23 -0
- package/scripts/daemon-user-acl.js +19 -1
- package/scripts/daemon-weixin-bridge.js +6 -2
- package/scripts/daemon.js +50 -4
- package/scripts/docs/hermes-memory-upgrade-converged.md +461 -0
- package/scripts/docs/hermes-memory-upgrade-plan.md +506 -0
- package/scripts/feishu-adapter.js +78 -2
- package/scripts/memory-extract.js +72 -4
- package/scripts/memory-wiki-schema.js +31 -0
- package/scripts/memory.js +8 -2
- package/skills/agent-management/SKILL.md +101 -0
- package/skills/send-to-user/SKILL.md +76 -0
|
@@ -49,7 +49,7 @@ function createBridgeStarter(deps) {
|
|
|
49
49
|
return text || null;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl }) {
|
|
52
|
+
async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl, fromAllowedChat }) {
|
|
53
53
|
const trimmed = String(text || '').trim();
|
|
54
54
|
const normalizedSenderId = normalizeSenderId(senderId);
|
|
55
55
|
if (!trimmed || bypassAcl || !userAcl) {
|
|
@@ -58,10 +58,15 @@ function createBridgeStarter(deps) {
|
|
|
58
58
|
|
|
59
59
|
let userCtx;
|
|
60
60
|
try {
|
|
61
|
-
userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {});
|
|
61
|
+
userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {}, { fromAllowedChat: !!fromAllowedChat });
|
|
62
62
|
} catch {
|
|
63
63
|
return { blocked: false, readOnly: false, senderId: normalizedSenderId };
|
|
64
64
|
}
|
|
65
|
+
// Audit trail for implicit-admin upgrades — these users are NOT in users.yaml
|
|
66
|
+
// and gain admin via group-whitelist trust, so make their action visible.
|
|
67
|
+
if (userCtx && userCtx.implicitAdmin) {
|
|
68
|
+
try { log('INFO', `[ACL] implicit admin via allowed_chat_ids: chat=${chatId} sender=${normalizedSenderId}`); } catch { /* non-fatal */ }
|
|
69
|
+
}
|
|
65
70
|
|
|
66
71
|
const userCmd = userAcl.handleUserCommand(trimmed, userCtx);
|
|
67
72
|
if (userCmd && userCmd.handled) {
|
|
@@ -803,6 +808,7 @@ function createBridgeStarter(deps) {
|
|
|
803
808
|
config: liveCfg,
|
|
804
809
|
senderId,
|
|
805
810
|
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
811
|
+
fromAllowedChat: isAllowedChat,
|
|
806
812
|
});
|
|
807
813
|
if (acl.blocked) return;
|
|
808
814
|
log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
|
|
@@ -850,6 +856,7 @@ function createBridgeStarter(deps) {
|
|
|
850
856
|
config: liveCfg,
|
|
851
857
|
senderId,
|
|
852
858
|
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
859
|
+
fromAllowedChat: isAllowedChat,
|
|
853
860
|
});
|
|
854
861
|
if (acl.blocked) return;
|
|
855
862
|
log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
|
|
@@ -1931,6 +1931,9 @@ function createClaudeEngine(deps) {
|
|
|
1931
1931
|
log('WARN', `Codex thread migrated for ${chatId}: ${prevSessionId.slice(0, 8)} -> ${safeNextId.slice(0, 8)}`);
|
|
1932
1932
|
}
|
|
1933
1933
|
// Keep card header in sync with the real session ID reported by the engine
|
|
1934
|
+
if (!_ackCardHeader && bot.sendCard) {
|
|
1935
|
+
_ackCardHeader = { title: `📎 ${safeNextId.slice(0, 8)}`, color: 'grey' };
|
|
1936
|
+
}
|
|
1934
1937
|
if (_ackCardHeader && _ackCardHeader._baseTitle) {
|
|
1935
1938
|
_ackCardHeader = { ..._ackCardHeader, title: `${_ackCardHeader._baseTitle}(${safeNextId.slice(0, 8)})` };
|
|
1936
1939
|
}
|
|
@@ -1941,7 +1944,11 @@ function createClaudeEngine(deps) {
|
|
|
1941
1944
|
if (_ackCardHeader) {
|
|
1942
1945
|
_ackCardHeader._baseTitle = _ackCardHeader.title; // preserve original title for onSession updates
|
|
1943
1946
|
}
|
|
1944
|
-
|
|
1947
|
+
// For non-bound-project chats: create a minimal header to display session ID (Feishu cards)
|
|
1948
|
+
if (!_ackCardHeader && bot.sendCard && session && session.id) {
|
|
1949
|
+
_ackCardHeader = { title: `📎 ${session.id.slice(0, 8)}`, color: 'grey' };
|
|
1950
|
+
}
|
|
1951
|
+
if (session && session.id && _ackCardHeader && _ackCardHeader._baseTitle) {
|
|
1945
1952
|
_ackCardHeader = { ..._ackCardHeader, title: `${_ackCardHeader._baseTitle}(${session.id.slice(0, 8)})` };
|
|
1946
1953
|
}
|
|
1947
1954
|
|
|
@@ -2336,13 +2343,15 @@ function createClaudeEngine(deps) {
|
|
|
2336
2343
|
log('DEBUG', `[REPLY:${chatId}] sendCard done msgId=${replyMsg && replyMsg.message_id}`);
|
|
2337
2344
|
} else {
|
|
2338
2345
|
log('DEBUG', `[REPLY:${chatId}] sending sendMarkdown`);
|
|
2339
|
-
|
|
2346
|
+
const _textSessionSuffix = session && session.id ? `\n\n📎 ${session.id.slice(0, 8)}` : '';
|
|
2347
|
+
replyMsg = await bot.sendMarkdown(chatId, cleanOutput + _textSessionSuffix);
|
|
2340
2348
|
log('DEBUG', `[REPLY:${chatId}] sendMarkdown done msgId=${replyMsg && replyMsg.message_id}`);
|
|
2341
2349
|
}
|
|
2342
2350
|
}
|
|
2343
2351
|
} catch (sendErr) {
|
|
2344
2352
|
log('WARN', `sendCard/sendMarkdown failed (${sendErr.message}), falling back to sendMessage`);
|
|
2345
|
-
|
|
2353
|
+
const _textSessionSuffix = session && session.id ? `\n\n📎 ${session.id.slice(0, 8)}` : '';
|
|
2354
|
+
try { replyMsg = await bot.sendMessage(chatId, cleanOutput + _textSessionSuffix); } catch (e2) {
|
|
2346
2355
|
log('ERROR', `sendMessage fallback also failed: ${e2.message}`);
|
|
2347
2356
|
}
|
|
2348
2357
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { resolveEngineModel } = require('./daemon-engine-runtime');
|
|
4
|
-
const { createAgentIntentHandler } = require('./daemon-agent-intent');
|
|
5
4
|
const { rawChatId: extractOriginalChatId, isThreadChatId } = require('./core/thread-chat-id');
|
|
6
5
|
const { createWikiCommandHandler } = require('./daemon-wiki');
|
|
7
6
|
|
|
@@ -26,9 +25,9 @@ function createCommandRouter(deps) {
|
|
|
26
25
|
activeProcesses,
|
|
27
26
|
pipeline, // message pipeline — used for interrupt/clearQueue
|
|
28
27
|
log,
|
|
29
|
-
agentTools,
|
|
28
|
+
agentTools: _agentTools,
|
|
30
29
|
pendingAgentFlows,
|
|
31
|
-
pendingActivations,
|
|
30
|
+
pendingActivations: _pendingActivations,
|
|
32
31
|
agentFlowTtlMs,
|
|
33
32
|
getDefaultEngine,
|
|
34
33
|
getDb, // optional — () → DatabaseSync (for wiki commands)
|
|
@@ -42,7 +41,7 @@ function createCommandRouter(deps) {
|
|
|
42
41
|
return Number.isFinite(num) && num > 0 ? num : (10 * 60 * 1000);
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
function
|
|
44
|
+
function _hasFreshPendingFlow(flowKey) {
|
|
46
45
|
if (!pendingAgentFlows) return false;
|
|
47
46
|
const flow = pendingAgentFlows.get(flowKey);
|
|
48
47
|
if (!flow) return false;
|
|
@@ -217,7 +216,7 @@ function createCommandRouter(deps) {
|
|
|
217
216
|
return null;
|
|
218
217
|
}
|
|
219
218
|
|
|
220
|
-
function
|
|
219
|
+
function _getBoundProjectForChat(chatId, cfg) {
|
|
221
220
|
const map = {
|
|
222
221
|
...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
|
|
223
222
|
...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
|
|
@@ -356,18 +355,9 @@ function createCommandRouter(deps) {
|
|
|
356
355
|
});
|
|
357
356
|
}
|
|
358
357
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
attachOrCreateSession,
|
|
363
|
-
normalizeCwd,
|
|
364
|
-
getDefaultEngine,
|
|
365
|
-
loadConfig,
|
|
366
|
-
getBoundProjectForChat,
|
|
367
|
-
log,
|
|
368
|
-
pendingActivations,
|
|
369
|
-
hasFreshPendingFlow,
|
|
370
|
-
});
|
|
358
|
+
// Agent intent classification removed — now handled by agent-management skill.
|
|
359
|
+
// tryHandleAgentIntent was previously created here via createAgentIntentHandler().
|
|
360
|
+
// Explicit /agent commands still work through handleAgentCommand above.
|
|
371
361
|
|
|
372
362
|
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false, _meta = {}) {
|
|
373
363
|
if (text && !text.startsWith('/chatid') && !text.startsWith('/myid')) log('INFO', `CMD [${String(chatId).slice(-8)}]: ${text.slice(0, 80)}`);
|
|
@@ -593,9 +583,8 @@ function createCommandRouter(deps) {
|
|
|
593
583
|
return;
|
|
594
584
|
}
|
|
595
585
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
586
|
+
// Agent intent classification removed — now handled by agent-management skill
|
|
587
|
+
// via Claude's semantic understanding instead of daemon-level regex matching.
|
|
599
588
|
}
|
|
600
589
|
|
|
601
590
|
const daemonCfg = (config && config.daemon) || {};
|
|
@@ -15,6 +15,8 @@ const CODEX_TOOL_MAP = Object.freeze({
|
|
|
15
15
|
web_fetch: 'WebFetch',
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
const CODEX_AUTO_MODEL = 'auto';
|
|
19
|
+
|
|
18
20
|
const ENGINE_TIMEOUT_DEFAULTS = Object.freeze({
|
|
19
21
|
codex: Object.freeze({
|
|
20
22
|
idleMs: 10 * 60 * 1000,
|
|
@@ -80,16 +82,19 @@ const ENGINE_MODEL_CONFIG = Object.freeze({
|
|
|
80
82
|
hint: null,
|
|
81
83
|
},
|
|
82
84
|
codex: {
|
|
83
|
-
main:
|
|
85
|
+
main: CODEX_AUTO_MODEL, // follow official Codex CLI default model
|
|
84
86
|
distill: 'gpt-5.1-codex-mini', // cost-effective mini
|
|
85
87
|
options: [ // quick-pick buttons (official model names)
|
|
86
|
-
{ value:
|
|
87
|
-
{ value: 'gpt-5
|
|
88
|
+
{ value: CODEX_AUTO_MODEL, label: 'auto · 跟随 Codex 官方默认' },
|
|
89
|
+
{ value: 'gpt-5-codex', label: 'gpt-5-codex · 官方滚动别名' },
|
|
90
|
+
{ value: 'gpt-5.5', label: 'gpt-5.5 · 固定版本' },
|
|
91
|
+
{ value: 'gpt-5.4', label: 'gpt-5.4 · 固定版本' },
|
|
92
|
+
{ value: 'gpt-5.3-codex', label: 'gpt-5.3-codex · 固定 Codex' },
|
|
88
93
|
{ value: 'gpt-5.1-codex-max', label: 'gpt-5.1-codex-max · 长任务' },
|
|
89
94
|
{ value: 'gpt-5.1-codex-mini', label: 'gpt-5.1-codex-mini · 轻量' },
|
|
90
95
|
],
|
|
91
96
|
provider: 'openai',
|
|
92
|
-
hint: '
|
|
97
|
+
hint: '推荐 `auto` 或 `gpt-5-codex`,也可直接发送任意 OpenAI 模型名切换',
|
|
93
98
|
},
|
|
94
99
|
});
|
|
95
100
|
|
|
@@ -121,7 +126,8 @@ function looksLikeCodexModel(model) {
|
|
|
121
126
|
const raw = String(model || '').trim().toLowerCase();
|
|
122
127
|
if (!raw) return false;
|
|
123
128
|
return (
|
|
124
|
-
raw
|
|
129
|
+
raw === CODEX_AUTO_MODEL
|
|
130
|
+
|| raw.startsWith('gpt-')
|
|
125
131
|
|| raw.startsWith('o1')
|
|
126
132
|
|| raw.startsWith('o3')
|
|
127
133
|
|| raw.startsWith('o4')
|
|
@@ -129,6 +135,10 @@ function looksLikeCodexModel(model) {
|
|
|
129
135
|
);
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
function isCodexAutoModel(model) {
|
|
139
|
+
return String(model || '').trim().toLowerCase() === CODEX_AUTO_MODEL;
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
function resolveEngineModel(engineName, daemonCfg = {}, overrideModel = '') {
|
|
133
143
|
const engine = normalizeEngineName(engineName);
|
|
134
144
|
const engineCfg = ENGINE_MODEL_CONFIG[engine] || ENGINE_MODEL_CONFIG.claude;
|
|
@@ -393,7 +403,7 @@ function buildCodexArgs(options = {}) {
|
|
|
393
403
|
: ['exec'];
|
|
394
404
|
|
|
395
405
|
args.push('--json', '--skip-git-repo-check');
|
|
396
|
-
if (model) args.push('-m', model);
|
|
406
|
+
if (model && !isCodexAutoModel(model)) args.push('-m', model);
|
|
397
407
|
// -C (cwd) is only supported on fresh exec, not resume
|
|
398
408
|
if (cwd && !isResume) args.push('-C', cwd);
|
|
399
409
|
|
|
@@ -165,8 +165,10 @@ function setupRuntimeWatchers(deps) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
let reloadDebounce = null;
|
|
168
|
+
let suppressNextWatch = false; // guard: skip watcher re-trigger after .bak restore
|
|
168
169
|
fs.watchFile(CONFIG_FILE, { interval: 2000 }, (curr, prev) => {
|
|
169
170
|
if (curr.mtimeMs === prev.mtimeMs) return;
|
|
171
|
+
if (suppressNextWatch) { suppressNextWatch = false; return; }
|
|
170
172
|
if (reloadDebounce) clearTimeout(reloadDebounce);
|
|
171
173
|
reloadDebounce = setTimeout(() => {
|
|
172
174
|
log('INFO', 'daemon.yaml changed on disk — auto-reloading config');
|
|
@@ -176,6 +178,27 @@ function setupRuntimeWatchers(deps) {
|
|
|
176
178
|
adminNotifyFn(`🔄 Config auto-reloaded. ${r.tasks} heartbeat tasks active.`).catch(() => { });
|
|
177
179
|
} else {
|
|
178
180
|
log('ERROR', `Auto-reload failed: ${r.error}`);
|
|
181
|
+
// Attempt auto-restore from backup
|
|
182
|
+
const bakFile = CONFIG_FILE + '.bak';
|
|
183
|
+
try {
|
|
184
|
+
if (!fs.existsSync(bakFile)) {
|
|
185
|
+
log('WARN', 'No .bak file available for auto-restore');
|
|
186
|
+
} else {
|
|
187
|
+
suppressNextWatch = true; // prevent watcher re-trigger from our own copyFileSync
|
|
188
|
+
fs.copyFileSync(bakFile, CONFIG_FILE);
|
|
189
|
+
log('INFO', 'Auto-restored daemon.yaml from .bak');
|
|
190
|
+
const r2 = reloadConfig();
|
|
191
|
+
if (r2.success) {
|
|
192
|
+
log('INFO', `Restored config reload OK: ${r2.tasks} tasks`);
|
|
193
|
+
adminNotifyFn(`⚠️ daemon.yaml 解析失败,已从备份恢复。错误: ${r.error}`).catch(() => { });
|
|
194
|
+
} else {
|
|
195
|
+
log('ERROR', `Backup config also invalid: ${r2.error}. Manual intervention required.`);
|
|
196
|
+
adminNotifyFn(`🚨 daemon.yaml 和备份均无法解析,请手动修复。`).catch(() => { });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (restoreErr) {
|
|
200
|
+
log('ERROR', `Auto-restore from .bak failed: ${restoreErr.message}`);
|
|
201
|
+
}
|
|
179
202
|
}
|
|
180
203
|
}, 1000);
|
|
181
204
|
});
|
|
@@ -178,9 +178,15 @@ const ADMIN_ONLY_ACTIONS = new Set(['system', 'agent', 'config', 'admin_acl']);
|
|
|
178
178
|
* 根据 senderId 解析用户上下文
|
|
179
179
|
* @param {string|null} senderId 飞书 open_id
|
|
180
180
|
* @param {object} config daemon 配置(兼容旧 operator_ids)
|
|
181
|
+
* @param {object} [opts]
|
|
182
|
+
* @param {boolean} [opts.fromAllowedChat] 消息来自 allowed_chat_ids 命中的群。
|
|
183
|
+
* 当为 true 且 senderId 未在 users.yaml 显式登记时,直接判为 admin
|
|
184
|
+
* (群级白名单已经做过准入控制,再叠用户级 operator_ids 是冗余)。
|
|
185
|
+
* users.yaml 里显式登记的角色仍然受尊重。
|
|
181
186
|
* @returns {object} userCtx { senderId, role, name, allowedActions, can(action) }
|
|
182
187
|
*/
|
|
183
|
-
function resolveUserCtx(senderId, config) {
|
|
188
|
+
function resolveUserCtx(senderId, config, opts = {}) {
|
|
189
|
+
const fromAllowedChat = !!(opts && opts.fromAllowedChat);
|
|
184
190
|
const userData = loadUsers();
|
|
185
191
|
// Per-platform bootstrap: Feishu open_id starts with "ou_", Telegram IDs are numeric.
|
|
186
192
|
const allUsers = userData && userData.users ? userData.users : {};
|
|
@@ -194,6 +200,7 @@ function resolveUserCtx(senderId, config) {
|
|
|
194
200
|
const hasConfiguredUsers = hasPlatformAdmin;
|
|
195
201
|
|
|
196
202
|
let role, name, allowedActions;
|
|
203
|
+
let implicitAdmin = false;
|
|
197
204
|
|
|
198
205
|
if (!senderId) {
|
|
199
206
|
// 无 ID(Telegram 等)— 兼容旧逻辑,视为 admin
|
|
@@ -215,6 +222,16 @@ function resolveUserCtx(senderId, config) {
|
|
|
215
222
|
} else {
|
|
216
223
|
allowedActions = [];
|
|
217
224
|
}
|
|
225
|
+
} else if (fromAllowedChat) {
|
|
226
|
+
// Group-whitelist trust: messages reaching us through allowed_chat_ids
|
|
227
|
+
// already passed chat-level access control. Anyone who can speak in the
|
|
228
|
+
// group is treated as admin without needing operator_ids or yaml entry.
|
|
229
|
+
// (Explicit users.yaml entries above still win — admins can demote
|
|
230
|
+
// someone by writing them in.)
|
|
231
|
+
role = 'admin';
|
|
232
|
+
name = senderId.slice(-6);
|
|
233
|
+
allowedActions = ROLE_DEFAULT_ACTIONS.admin;
|
|
234
|
+
implicitAdmin = true;
|
|
218
235
|
} else {
|
|
219
236
|
// 兼容旧 operator_ids:若 senderId 在 operator_ids 中,视为 admin
|
|
220
237
|
const operatorIds = (config && config.feishu && config.feishu.operator_ids) || [];
|
|
@@ -254,6 +271,7 @@ function resolveUserCtx(senderId, config) {
|
|
|
254
271
|
isAdmin: role === 'admin',
|
|
255
272
|
isMember: role === 'member',
|
|
256
273
|
isStranger: role === 'stranger',
|
|
274
|
+
implicitAdmin,
|
|
257
275
|
can(action) { return allowedActions.includes(action); },
|
|
258
276
|
readOnly: role !== 'admin',
|
|
259
277
|
};
|
|
@@ -182,6 +182,8 @@ function createWeixinBridge(deps = {}) {
|
|
|
182
182
|
let currentAccount = null;
|
|
183
183
|
let currentBot = null;
|
|
184
184
|
let missingAccountLogged = false;
|
|
185
|
+
let pollErrorDelay = 1000; // exponential backoff on poll errors
|
|
186
|
+
const MAX_POLL_ERROR_DELAY = 30000;
|
|
185
187
|
|
|
186
188
|
function sameAccount(a, b) {
|
|
187
189
|
if (!a || !b) return false;
|
|
@@ -238,6 +240,7 @@ function createWeixinBridge(deps = {}) {
|
|
|
238
240
|
getUpdatesBuf,
|
|
239
241
|
timeoutMs: liveBridgeCfg.pollTimeoutMs,
|
|
240
242
|
});
|
|
243
|
+
pollErrorDelay = 1000; // reset as soon as HTTP poll succeeds — downstream processMessage errors should not cause poll backoff
|
|
241
244
|
if (resp && typeof resp.get_updates_buf === 'string' && resp.get_updates_buf) {
|
|
242
245
|
getUpdatesBuf = resp.get_updates_buf;
|
|
243
246
|
}
|
|
@@ -263,8 +266,9 @@ function createWeixinBridge(deps = {}) {
|
|
|
263
266
|
});
|
|
264
267
|
}
|
|
265
268
|
} catch (err) {
|
|
266
|
-
log('WARN', `[WEIXIN] poll error: ${err.message}`);
|
|
267
|
-
nextDelayMs =
|
|
269
|
+
log('WARN', `[WEIXIN] poll error: ${err.message} — retrying in ${Math.round(pollErrorDelay / 1000)}s`);
|
|
270
|
+
nextDelayMs = pollErrorDelay;
|
|
271
|
+
pollErrorDelay = Math.min(pollErrorDelay * 2, MAX_POLL_ERROR_DELAY);
|
|
268
272
|
} finally {
|
|
269
273
|
processing = false;
|
|
270
274
|
scheduleNext(nextDelayMs);
|
package/scripts/daemon.js
CHANGED
|
@@ -106,6 +106,9 @@ const SKILL_ROUTES = [
|
|
|
106
106
|
{ name: 'heartbeat-task-manager', pattern: /提醒|remind|闹钟|定时|每[天周月]/i },
|
|
107
107
|
{ name: 'skill-manager', pattern: /找技能|管理技能|更新技能|安装技能|skill manager|skill scout|(?:find|look for)\s+skills?/i },
|
|
108
108
|
{ name: 'skill-evolution-manager', pattern: /\/evolve\b|复盘一下|记录一下(这个)?经验|保存到\s*skill|skill evolution/i },
|
|
109
|
+
// (?<!转) excludes "转发" forwarding semantics (e.g. "把这条消息转发给我")
|
|
110
|
+
// which is conversation relay, not file delivery.
|
|
111
|
+
{ name: 'send-to-user', pattern: /(?<!转)发(?:到|给|出)?\s*(?:我|手机|飞书)|发(?:个|条|份)?\s*(?:文件|附件|图(?:片)?|日志|压缩包|截图|csv|pdf|zip|excel|表格)|(?<!转)发我|给我(?:下载|发个|发份)|send\s+(?:me|file|attachment|to\s+me)|push\s+(?:to\s+)?(?:me|phone|file)|attach\s+file/i },
|
|
109
112
|
];
|
|
110
113
|
|
|
111
114
|
function routeSkill(prompt) {
|
|
@@ -256,7 +259,10 @@ function loadConfig() {
|
|
|
256
259
|
function writeConfigSafe(nextConfig) {
|
|
257
260
|
const tmpFile = `${CONFIG_FILE}.tmp.${process.pid}.${Date.now()}`;
|
|
258
261
|
try {
|
|
259
|
-
|
|
262
|
+
const dumped = yaml.dump(nextConfig, { lineWidth: -1 });
|
|
263
|
+
// Validate: round-trip parse before committing to disk
|
|
264
|
+
yaml.load(dumped);
|
|
265
|
+
fs.writeFileSync(tmpFile, dumped, 'utf8');
|
|
260
266
|
fs.renameSync(tmpFile, CONFIG_FILE);
|
|
261
267
|
} catch (e) {
|
|
262
268
|
try { if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); } catch { }
|
|
@@ -1959,9 +1965,49 @@ const pendingAgentFlows = new Map();
|
|
|
1959
1965
|
const pendingTeamFlows = new Map();
|
|
1960
1966
|
|
|
1961
1967
|
// Pending activation: after creating an agent with skipChatBinding=true,
|
|
1962
|
-
// store here so any new unbound group can activate it with /activate
|
|
1963
|
-
// { agentKey, agentName, cwd, createdAt }
|
|
1964
|
-
|
|
1968
|
+
// store here so any new unbound group can activate it with /activate.
|
|
1969
|
+
// { agentKey, agentName, cwd, createdAt, preCreatedChatId?, preCreatedChatName? }
|
|
1970
|
+
//
|
|
1971
|
+
// Persisted to disk so an auto-update / lid-close / SIGUSR2 restart in the
|
|
1972
|
+
// 30-minute activation window doesn't strand a freshly created Feishu chat
|
|
1973
|
+
// that hadn't bound yet (esp. the orphan path: createChat ok + bind fail).
|
|
1974
|
+
// Stale entries (>30min) are dropped on load.
|
|
1975
|
+
const pendingActivations = new Map();
|
|
1976
|
+
const PENDING_ACTIVATIONS_FILE = path.join(HOME, '.metame', 'pending_activations.json');
|
|
1977
|
+
const PENDING_ACTIVATIONS_TTL_MS = 30 * 60 * 1000;
|
|
1978
|
+
function _persistPendingActivations() {
|
|
1979
|
+
try {
|
|
1980
|
+
const obj = Object.fromEntries(pendingActivations);
|
|
1981
|
+
const tmp = PENDING_ACTIVATIONS_FILE + '.tmp';
|
|
1982
|
+
fs.writeFileSync(tmp, JSON.stringify(obj), 'utf8');
|
|
1983
|
+
fs.renameSync(tmp, PENDING_ACTIVATIONS_FILE);
|
|
1984
|
+
} catch { /* best-effort, never throw from persistence */ }
|
|
1985
|
+
}
|
|
1986
|
+
function _restorePendingActivations() {
|
|
1987
|
+
try {
|
|
1988
|
+
if (!fs.existsSync(PENDING_ACTIVATIONS_FILE)) return;
|
|
1989
|
+
const data = JSON.parse(fs.readFileSync(PENDING_ACTIVATIONS_FILE, 'utf8'));
|
|
1990
|
+
const now = Date.now();
|
|
1991
|
+
let restored = 0;
|
|
1992
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1993
|
+
if (v && v.createdAt && now - v.createdAt < PENDING_ACTIVATIONS_TTL_MS) {
|
|
1994
|
+
pendingActivations.set(k, v);
|
|
1995
|
+
restored += 1;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (restored > 0) log('INFO', `[pending] restored ${restored} pending activation(s) from disk`);
|
|
1999
|
+
} catch { /* corrupt file → start fresh */ }
|
|
2000
|
+
}
|
|
2001
|
+
// Wrap Map.set/.delete so every mutation hits disk. Wrapping the prototype
|
|
2002
|
+
// methods (rather than reassigning) keeps the Map reference stable for callers
|
|
2003
|
+
// that stored it before the wrap (the daemon does not, but defensive).
|
|
2004
|
+
const _origPendingSet = pendingActivations.set.bind(pendingActivations);
|
|
2005
|
+
const _origPendingDelete = pendingActivations.delete.bind(pendingActivations);
|
|
2006
|
+
const _origPendingClear = pendingActivations.clear.bind(pendingActivations);
|
|
2007
|
+
pendingActivations.set = function (k, v) { const r = _origPendingSet(k, v); _persistPendingActivations(); return r; };
|
|
2008
|
+
pendingActivations.delete = function (k) { const r = _origPendingDelete(k); _persistPendingActivations(); return r; };
|
|
2009
|
+
pendingActivations.clear = function () { const r = _origPendingClear(); _persistPendingActivations(); return r; };
|
|
2010
|
+
_restorePendingActivations();
|
|
1965
2011
|
|
|
1966
2012
|
const { handleAdminCommand } = createAdminCommandHandler({
|
|
1967
2013
|
fs,
|