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.
Files changed (44) hide show
  1. package/README.md +187 -48
  2. package/index.js +148 -9
  3. package/package.json +6 -3
  4. package/scripts/daemon-admin-commands.js +254 -9
  5. package/scripts/daemon-agent-commands.js +64 -6
  6. package/scripts/daemon-agent-tools.js +26 -5
  7. package/scripts/daemon-bridges.js +110 -20
  8. package/scripts/daemon-claude-engine.js +704 -268
  9. package/scripts/daemon-command-router.js +24 -8
  10. package/scripts/daemon-default.yaml +28 -4
  11. package/scripts/daemon-engine-runtime.js +275 -0
  12. package/scripts/daemon-exec-commands.js +10 -4
  13. package/scripts/daemon-notify.js +37 -1
  14. package/scripts/daemon-runtime-lifecycle.js +2 -1
  15. package/scripts/daemon-session-commands.js +52 -4
  16. package/scripts/daemon-session-store.js +2 -1
  17. package/scripts/daemon-task-scheduler.js +87 -28
  18. package/scripts/daemon-user-acl.js +26 -9
  19. package/scripts/daemon.js +81 -17
  20. package/scripts/distill.js +323 -18
  21. package/scripts/docs/agent-guide.md +12 -0
  22. package/scripts/docs/maintenance-manual.md +119 -0
  23. package/scripts/docs/pointer-map.md +88 -0
  24. package/scripts/feishu-adapter.js +6 -1
  25. package/scripts/hooks/stop-session-capture.js +243 -0
  26. package/scripts/memory-extract.js +100 -5
  27. package/scripts/memory-nightly-reflect.js +196 -11
  28. package/scripts/memory.js +134 -3
  29. package/scripts/mentor-engine.js +405 -0
  30. package/scripts/platform.js +2 -0
  31. package/scripts/providers.js +169 -21
  32. package/scripts/schema.js +12 -0
  33. package/scripts/session-analytics.js +245 -12
  34. package/scripts/skill-changelog.js +245 -0
  35. package/scripts/skill-evolution.js +288 -5
  36. package/scripts/usage-classifier.js +1 -1
  37. package/scripts/daemon-admin-commands.test.js +0 -333
  38. package/scripts/daemon-task-envelope.test.js +0 -59
  39. package/scripts/daemon-task-scheduler.test.js +0 -106
  40. package/scripts/reliability-core.test.js +0 -280
  41. package/scripts/skill-evolution.test.js +0 -113
  42. package/scripts/task-board.test.js +0 -83
  43. package/scripts/test_daemon.js +0 -1407
  44. package/scripts/utils.test.js +0 -192
@@ -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(chatId, normalizeCwd(res.data.cwd), p.name || agentName || res.data.projectKey || '');
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 at all guide to manual bind
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 = { createAgentCommandHandler };
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] = { name: safeName, cwd: resolvedDir, nicknames: [safeName] };
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] = { name: safeName, cwd: resolvedDir, nicknames: [safeName] };
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
  }
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ let userAcl = null;
4
+ try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
5
+
3
6
  function createBridgeStarter(deps) {
4
7
  const {
5
8
  fs,
@@ -15,6 +18,52 @@ function createBridgeStarter(deps) {
15
18
  pendingActivations, // optional — used to show smart activation hint
16
19
  } = deps;
17
20
 
21
+ async function sendAclReply(bot, chatId, text) {
22
+ if (!text) return;
23
+ try {
24
+ if (bot.sendMarkdown) await bot.sendMarkdown(chatId, text);
25
+ else await bot.sendMessage(chatId, text.replace(/[*_`]/g, ''));
26
+ } catch { /* non-fatal */ }
27
+ }
28
+
29
+ function normalizeSenderId(senderId) {
30
+ if (senderId === undefined || senderId === null) return null;
31
+ const text = String(senderId).trim();
32
+ return text || null;
33
+ }
34
+
35
+ async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl }) {
36
+ const trimmed = String(text || '').trim();
37
+ const normalizedSenderId = normalizeSenderId(senderId);
38
+ if (!trimmed || bypassAcl || !userAcl) {
39
+ return { blocked: false, readOnly: false, senderId: normalizedSenderId };
40
+ }
41
+
42
+ let userCtx;
43
+ try {
44
+ userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {});
45
+ } catch {
46
+ return { blocked: false, readOnly: false, senderId: normalizedSenderId };
47
+ }
48
+
49
+ const userCmd = userAcl.handleUserCommand(trimmed, userCtx);
50
+ if (userCmd && userCmd.handled) {
51
+ await sendAclReply(bot, chatId, userCmd.reply);
52
+ return { blocked: true, readOnly: !!userCtx.readOnly, senderId: normalizedSenderId };
53
+ }
54
+
55
+ const publicCmds = Array.isArray(userAcl.PUBLIC_COMMANDS) ? userAcl.PUBLIC_COMMANDS : [];
56
+ const isPublic = publicCmds.includes(trimmed.toLowerCase());
57
+ const action = userAcl.classifyCommandAction(trimmed);
58
+ const allowed = isPublic || (typeof userCtx.can === 'function' && userCtx.can(action));
59
+ if (!allowed) {
60
+ await sendAclReply(bot, chatId, `⚠️ 当前权限不足(角色: ${userCtx.role})\n命令类型: ${action}\n请联系管理员授权。`);
61
+ return { blocked: true, readOnly: true, senderId: normalizedSenderId };
62
+ }
63
+
64
+ return { blocked: false, readOnly: !!userCtx.readOnly, senderId: normalizedSenderId };
65
+ }
66
+
18
67
  // Returns the best pending activation for a given chatId (excludes self-created)
19
68
  function getPendingActivationForChat(chatId) {
20
69
  if (!pendingActivations || pendingActivations.size === 0) return null;
@@ -67,12 +116,26 @@ function createBridgeStarter(deps) {
67
116
  if (update.callback_query) {
68
117
  const cb = update.callback_query;
69
118
  const chatId = cb.message && cb.message.chat.id;
119
+ const senderId = cb.from && cb.from.id ? String(cb.from.id) : null;
70
120
  bot.answerCallback(cb.id).catch(() => { });
71
121
  if (chatId && cb.data) {
72
122
  const liveCfg = loadConfig();
73
123
  const allowedIds = (liveCfg.telegram && liveCfg.telegram.allowed_chat_ids) || [];
74
124
  if (!allowedIds.includes(chatId)) continue;
75
- handleCommand(bot, chatId, cb.data, liveCfg, executeTaskByName).catch(e => {
125
+ const isBindCmd = cb.data.startsWith('/agent bind')
126
+ || cb.data.startsWith('/agent-bind-dir')
127
+ || cb.data.startsWith('/browse bind')
128
+ || cb.data === '/activate';
129
+ const acl = await applyUserAcl({
130
+ bot,
131
+ chatId,
132
+ text: cb.data,
133
+ config: liveCfg,
134
+ senderId,
135
+ bypassAcl: !allowedIds.includes(chatId) && !!isBindCmd,
136
+ });
137
+ if (acl.blocked) continue;
138
+ handleCommand(bot, chatId, cb.data, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
76
139
  log('ERROR', `Telegram callback handler error: ${e.message}`);
77
140
  });
78
141
  }
@@ -83,6 +146,7 @@ function createBridgeStarter(deps) {
83
146
 
84
147
  const msg = update.message;
85
148
  const chatId = msg.chat.id;
149
+ const senderId = msg.from && msg.from.id ? String(msg.from.id) : null;
86
150
 
87
151
  const liveCfg = loadConfig();
88
152
  const allowedIds = (liveCfg.telegram && liveCfg.telegram.allowed_chat_ids) || [];
@@ -93,7 +157,8 @@ function createBridgeStarter(deps) {
93
157
  || trimmedText.startsWith('/browse bind')
94
158
  || trimmedText === '/activate'
95
159
  );
96
- if (!allowedIds.includes(chatId) && !isBindCmd) {
160
+ const isAllowedChat = allowedIds.includes(chatId);
161
+ if (!isAllowedChat && !isBindCmd) {
97
162
  log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
98
163
  bot.sendMessage(chatId, unauthorizedMsg(chatId)).catch(() => {});
99
164
  continue;
@@ -108,6 +173,15 @@ function createBridgeStarter(deps) {
108
173
  const fileId = msg.document ? msg.document.file_id : msg.photo[msg.photo.length - 1].file_id;
109
174
  const fileName = msg.document ? msg.document.file_name : `photo_${Date.now()}.jpg`;
110
175
  const caption = msg.caption || '';
176
+ const acl = await applyUserAcl({
177
+ bot,
178
+ chatId,
179
+ text: caption || '[file-upload]',
180
+ config: liveCfg,
181
+ senderId,
182
+ bypassAcl: !isAllowedChat && !!isBindCmd,
183
+ });
184
+ if (acl.blocked) continue;
111
185
 
112
186
  const session = getSession(chatId);
113
187
  const cwd = session?.cwd || HOME;
@@ -123,7 +197,7 @@ function createBridgeStarter(deps) {
123
197
  ? `User uploaded a file to the project: ${destPath}\nUser says: "${caption}"`
124
198
  : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
125
199
 
126
- handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName).catch(e => {
200
+ handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
127
201
  log('ERROR', `Telegram file handler error: ${e.message}`);
128
202
  });
129
203
  } catch (err) {
@@ -134,7 +208,17 @@ function createBridgeStarter(deps) {
134
208
  }
135
209
 
136
210
  if (msg.text) {
137
- handleCommand(bot, chatId, msg.text.trim(), liveCfg, executeTaskByName).catch(e => {
211
+ const text = msg.text.trim();
212
+ const acl = await applyUserAcl({
213
+ bot,
214
+ chatId,
215
+ text,
216
+ config: liveCfg,
217
+ senderId,
218
+ bypassAcl: !isAllowedChat && !!isBindCmd,
219
+ });
220
+ if (acl.blocked) continue;
221
+ handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
138
222
  log('ERROR', `Telegram handler error: ${e.message}`);
139
223
  });
140
224
  }
@@ -182,27 +266,24 @@ function createBridgeStarter(deps) {
182
266
  || trimmedText.startsWith('/browse bind')
183
267
  || trimmedText === '/activate'
184
268
  );
185
- if (!allowedIds.includes(chatId) && !isBindCmd) {
269
+ const isAllowedChat = allowedIds.includes(chatId);
270
+ if (!isAllowedChat && !isBindCmd) {
186
271
  log('WARN', `Feishu: rejected message from ${chatId}`);
187
272
  const msg = unauthorizedMsg(chatId);
188
273
  (bot.sendMarkdown ? bot.sendMarkdown(chatId, msg) : bot.sendMessage(chatId, msg)).catch(() => {});
189
274
  return;
190
275
  }
191
276
 
192
- const operatorIds = (liveCfg.feishu && liveCfg.feishu.operator_ids) || [];
193
- if (operatorIds.length > 0 && senderId && !operatorIds.includes(senderId) && !isBindCmd) {
194
- log('INFO', `Feishu: read-only message from non-operator ${senderId} in ${chatId}: ${(text || '').slice(0, 50)}`);
195
- if (text && text.startsWith('/')) {
196
- await (bot.sendMarkdown ? bot.sendMarkdown(chatId, '⚠️ 该操作需要授权,请联系管理员。') : bot.sendMessage(chatId, '⚠️ 该操作需要授权,请联系管理员。'));
197
- return;
198
- }
199
- if (text) {
200
- await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, senderId, true);
201
- }
202
- return;
203
- }
204
-
205
277
  if (fileInfo && fileInfo.fileKey) {
278
+ const acl = await applyUserAcl({
279
+ bot,
280
+ chatId,
281
+ text: text || '[file-upload]',
282
+ config: liveCfg,
283
+ senderId,
284
+ bypassAcl: !isAllowedChat && !!isBindCmd,
285
+ });
286
+ if (acl.blocked) return;
206
287
  log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
207
288
  const session = getSession(chatId);
208
289
  const cwd = session?.cwd || HOME;
@@ -218,7 +299,7 @@ function createBridgeStarter(deps) {
218
299
  ? `User uploaded a file to the project: ${destPath}\nUser says: "${text}"`
219
300
  : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
220
301
 
221
- await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName);
302
+ await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
222
303
  } catch (err) {
223
304
  log('ERROR', `Feishu file download failed: ${err.message}`);
224
305
  await bot.sendMessage(chatId, `❌ Download failed: ${err.message}`);
@@ -227,6 +308,15 @@ function createBridgeStarter(deps) {
227
308
  }
228
309
 
229
310
  if (text) {
311
+ const acl = await applyUserAcl({
312
+ bot,
313
+ chatId,
314
+ text,
315
+ config: liveCfg,
316
+ senderId,
317
+ bypassAcl: !isAllowedChat && !!isBindCmd,
318
+ });
319
+ if (acl.blocked) return;
230
320
  log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
231
321
  const parentId = event?.message?.parent_id;
232
322
  if (parentId) {
@@ -238,7 +328,7 @@ function createBridgeStarter(deps) {
238
328
  log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
239
329
  }
240
330
  }
241
- await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, senderId);
331
+ await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
242
332
  }
243
333
  });
244
334