@jesonliu/lark-claudecode-bridge 1.2.0 → 1.3.1

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/dist/index.js CHANGED
@@ -21,12 +21,13 @@ import { PermissionGate, DEFAULT_ALLOW_TOOLS_LIST, DEFAULT_DANGEROUS_COMMANDS, D
21
21
  import { discoverPlugins, resolvePluginPaths } from './executor/plugin-discovery.js';
22
22
  import { createGatewaySender, createNotifyServer, NOTIFY_SERVER_NAME } from './executor/notify-server.js';
23
23
  import { BRIDGE_MCP_JSON, readMcpServersFromJsonFile } from './web/skills-mcp-api.js';
24
- import { FEISHU_NOTIFY_SOP_PROMPT } from './notify-sop.js';
24
+ import { FEISHU_NOTIFY_SOP_PROMPT, TOOL_HINT_PROMPT } from './notify-sop.js';
25
+ import { ensureProfileSettingsFile, resolveAppProfile, resolveAppProfileModel } from './app-profile.js';
25
26
  import { isImageFile } from './util/file-types.js';
26
27
  import { FileTracker } from './util/file-tracker.js';
27
28
  import { SessionStore, migrateLegacySessions } from './session/session-store.js';
28
29
  import { handleCommand, applyWorkspaceSwitch, workspaceListMarkdown } from './session/commands.js';
29
- import { rewriteByTrigger } from './session/triggers.js';
30
+ import { isLocalCommandRewrite, matchTrigger, rewriteByTrigger } from './session/triggers.js';
30
31
  import { Semaphore, channelKey } from './session/channel.js';
31
32
  import { TranscriptWriter, sweepTranscripts } from './transcript/transcript-writer.js';
32
33
  import { nowBeijingISO } from './util/beijing-time.js';
@@ -116,13 +117,8 @@ deps, opts = {}) {
116
117
  // 提问挂起项:requestId → 等待中的问题答案(qa-pick 逐项收集,qa-submit 一次性 resolve)。
117
118
  // 嵌入主卡片后无 cardId,改带 channelKey 定位活跃进度卡
118
119
  const qaPending = new Map();
119
- // 结论尾卡挂起项:requestId → 结论卡上下文。与 planPending 不同——无 Promise 要 resolve
120
- // (任务已结束,确认/提意见是「发起新一轮」),条目仅用于:发起人校验(仅任务发起人可
121
- // 操作)、settled 防重(确认/提意见只生效一次)、尾卡正文快照(settled 回调响应内联换卡
122
- // 替换整卡,必须带回正文)。条目常驻供回看历史(每次有回复的任务一条;超 500 条清最老)
123
- const outputPending = new Map();
124
- // 工作区切换卡挂起项(裸 /ws):requestId → 发卡上下文。与 outputPending 同为「无 Promise
125
- // 挂起」的常驻条目,仅用于:发起记录、切换落盘目标通道(channelKey)、工作区快照(回调按
120
+ // 工作区切换卡挂起项(裸 /ws):requestId → 发卡上下文。「无 Promise 挂起」的常驻条目,
121
+ // 仅用于:发起记录、切换落盘目标通道(channelKey)、工作区快照(回调按
126
122
  // 名查找目标)。stale 失效语义:同通道后发的新卡上完成切换后,旧卡点击一律回「已过期」——
127
123
  // 否则旧卡显示的当前项是错的,用户会在旧卡上误切回旧工作区。上限 100 条删最老(防无界增长)
128
124
  const wsPending = new Map();
@@ -132,6 +128,16 @@ deps, opts = {}) {
132
128
  // 当前存活 query 的 MCP 状态拉取句柄(executor onQuery 注入、任务 finally 删除):
133
129
  // /mcp 命令在任务运行中经此实时拉取;句柄随 query 结束失效,调用侧须 catch
134
130
  const activeQueries = new Map();
131
+ // 触发词两段式(ask_detail)等待补充的挂起项:channelKey → 命中规则与原始消息。
132
+ // 命中「需补充」规则时先追问不入队;同用户下一条非 / 消息视作补充内容,与 rewrite 合并后入队。
133
+ // 超时(SUPPLEMENT_WAIT_MS)自动失效走原流程;任意本地命令(cmd.handled)即取消等待——
134
+ // /stop /new 天然是逃生口,与「本地命令永远优先」原则一致
135
+ const supplementPending = new Map();
136
+ /** 补充等待窗口:超时后挂起项失效,下一条消息照常走原流程(不提示、静默过期) */
137
+ const SUPPLEMENT_WAIT_MS = 10 * 60_000;
138
+ // 配置热重载失败提示的去重标记:失败状态只在「成功 → 失败」跳变时向当前聊天提示一次,
139
+ // 避免配置持续损坏期间每条消息都刷屏;恢复成功后静默复位(不发恢复消息)
140
+ let reloadFailNotified = false;
135
141
  function workspacePath(name) {
136
142
  return config.workspaces.find((w) => w.name === name)?.path ?? config.workspaces[0].path;
137
143
  }
@@ -249,12 +255,25 @@ deps, opts = {}) {
249
255
  // 每条消息先重读 access.json:lcb pair / 运行终端等独立进程批准写盘后,
250
256
  // 长存的内存实例不 reload 会查到旧白名单 → 反复发配对码且整盘覆写抹掉新用户(死循环)
251
257
  deps.access.reload();
252
- // 配置热重载:lcb ws add/remove 独立进程写盘后,本实例下一条消息即读到新工作区
253
- opts.reloadConfig?.();
258
+ // 配置热重载:lcb ws add/remove 独立进程写盘后,本实例下一条消息即读到新工作区。
259
+ // 读取失败沿用旧值继续跑(reloader 容错语义),但必须在用户侧显式提示——静默失败会让
260
+ // 「下一条消息热生效」变成「永不生效直到重启」且无从诊断。仅状态跳变时提示一次防刷屏
261
+ const reloadErr = opts.reloadConfig?.() ?? null;
262
+ if (reloadErr) {
263
+ console.warn('[配置热重载] 读取失败,沿用旧配置:', reloadErr);
264
+ if (!reloadFailNotified) {
265
+ reloadFailNotified = true;
266
+ await deps.gateway.sendTextTo(msg.chatId, `⚠️ 配置热重载失败,本次沿用上次配置(修复 config.yaml 后下一条消息自动恢复):${reloadErr}`)
267
+ .catch(() => { });
268
+ }
269
+ }
270
+ else {
271
+ reloadFailNotified = false;
272
+ }
254
273
  if (!deps.access.isAllowed(msg.userId)) {
255
274
  if (app.role === 'deputy') {
256
275
  // 分身机器人艾特即用(用户决策):不走配对——首条消息自动补录 member(access.json
257
- // 保留审计记录,outputPending 等处的 isAllowed 纵深校验天然兼容);无 admin 权限,
276
+ // 保留审计记录,isAllowed 后续校验天然兼容);无 admin 权限,
258
277
  // 管理命令与工作区切换在各自入口被拒,能力范围由分身配置限定
259
278
  deps.access.addUser(msg.userId, msg.userId, 'member', app.appId);
260
279
  console.log(`${tag}[接入] 分身应用 ${app.appId} 新用户 ${msg.userId} 已自动加入(member,能力按分身配置限定)`);
@@ -291,15 +310,35 @@ deps, opts = {}) {
291
310
  const currentWorkspace = app.role === 'deputy'
292
311
  ? app.allowedWorkspaces[0]
293
312
  : (st?.workspaceName || app.defaultWorkspace || config.defaults.workspace);
294
- // 2.5 触发词映射:必须放在本地命令之前——斜杠触发词(如 /produce)会被 handleCommand
295
- // 的未知命令分支吞掉;rewriteByTrigger 对本地命令(/stop 等)直接放行,不会被劫持
296
- const triggered = rewriteByTrigger(msg.text, app.triggers);
297
- if (triggered !== null) {
298
- await deps.gateway.sendTextTo(msg.chatId, '⚡ 已按触发词转入对应技能流程');
299
- enqueue(key, msg, triggered, currentWorkspace);
313
+ // 2.4 触发词两段式补充(ask_detail):上一条消息命中「需补充」规则且正在等待时,本条
314
+ // 非 / 消息(同一用户)视作补充内容——与原始消息拼接后对该规则重新改写:补充内容即
315
+ // {args}(无占位符时按既有规则自动追加),零新合并逻辑。其他用户消息 / 斜杠消息 /
316
+ // 超时挂起均不在此列,照常走原流程(挂起过期在下方静默清除)
317
+ const pendingSup = supplementPending.get(key);
318
+ if (pendingSup && Date.now() - pendingSup.at > SUPPLEMENT_WAIT_MS)
319
+ supplementPending.delete(key);
320
+ const trimmedText = msg.text.trim();
321
+ if (pendingSup && !trimmedText.startsWith('/') && msg.userId === pendingSup.userId) {
322
+ supplementPending.delete(key);
323
+ const combined = `${pendingSup.originalText} ${trimmedText}`;
324
+ // ?? combined 兜底理论不可达:rule 本身按 combined 仍会命中(首 token/关键词都在)。
325
+ // 关键词形态的改写不含补充内容(matchTrigger 仅斜杠形态自动追加 args)——补充是本次
326
+ // 合并的核心增量,改写结果里没有就显式拼到末尾,绝不把用户刚给的链接/要求吞掉
327
+ const finalPrompt = rewriteByTrigger(combined, [pendingSup.rule]) ?? combined;
328
+ const merged = finalPrompt.includes(trimmedText) ? finalPrompt : `${finalPrompt} ${trimmedText}`;
329
+ await deps.gateway.sendTextTo(msg.chatId, '✅ 已收到补充,任务开始');
330
+ enqueue(key, msg, merged, currentWorkspace);
300
331
  return;
301
332
  }
302
- const cmd = await handleCommand(msg.text, {
333
+ // 2.5 触发词映射:改写结果统一回灌 handleCommand 走同一条优先级链——rewrite 可指向
334
+ // bridge 本地命令(飞书菜单「新会话」→ match=新会话, rewrite=/new),此时本地执行而非
335
+ // 当任务透传(Claude Code 侧没有该命令,headless 只会报未知命令错误);改写成技能/插件
336
+ // 命令或普通 prompt 时 handleCommand 不识别 → taskText 原样带出,文末照旧入队。
337
+ // 本地命令原文(/stop 等)matchTrigger 直接放行返回 null,不会被劫持
338
+ const hit = matchTrigger(msg.text, app.triggers);
339
+ const triggered = hit?.rewritten ?? null;
340
+ const effectiveText = triggered ?? msg.text;
341
+ const cmd = await handleCommand(effectiveText, {
303
342
  channelKey: key,
304
343
  store: deps.store,
305
344
  config,
@@ -338,6 +377,9 @@ deps, opts = {}) {
338
377
  },
339
378
  });
340
379
  if (cmd.handled) {
380
+ // 本地命令处理即取消两段式补充等待(/stop /new 天然逃生口,见 supplementPending 定义)——
381
+ // 不清会让等待期后的下一条普通消息被错误当成「补充」合并入队
382
+ supplementPending.delete(key);
341
383
  if (cmd.card) {
342
384
  // 交互卡片命令(裸 /ws):发卡并注册挂起项供回调切换。快照工作区清单——回调按
343
385
  // 名查找目标,配置热重载改清单不影响已发出卡片的自洽性
@@ -386,7 +428,9 @@ deps, opts = {}) {
386
428
  // 4) 通道级权限闸 reset(spec:「本次会话不再询问」的记忆跨任务生效,直至 /new)
387
429
  // 5) 汇总已取消的排队消息数(用户能看到新建会话的真实影响面)
388
430
  // ——handleCommand 无权访问 wiring 的 runtime/pending map,统一在 wiring 侧做
389
- if (/^\/new(?:\s|$)/.test(msg.text.trim())) {
431
+ // 以生效文本判定(触发词改写出的 /new 同样适用,如菜单「新会话」)——
432
+ // 否则菜单触发的新会话只清指针,排队消息/运行中任务/挂起条目全不清理,与手敲 /new 分叉
433
+ if (/^\/new(?:\s|$)/.test(effectiveText.trim())) {
390
434
  const rt = runtimes.get(key);
391
435
  const cancelled = rt?.queuedCount ?? 0;
392
436
  if (rt) {
@@ -413,12 +457,6 @@ deps, opts = {}) {
413
457
  qaPending.delete(id);
414
458
  p.resolve({});
415
459
  }
416
- // 本用户在本会话的长回复卡确认/提意见按钮一并失效(保留查看全文):新会话已无
417
- // 「上一轮方案」上下文,旧确认按钮再触发只会得到脱离语境的新一轮任务
418
- for (const o of outputPending.values()) {
419
- if (o.chatId === msg.chatId && o.ownerId === msg.userId)
420
- o.settled = true;
421
- }
422
460
  gates.get(key)?.reset();
423
461
  if (cancelled > 0) {
424
462
  await deps.gateway.sendTextTo(msg.chatId, `🗑️ 已因新建会话取消 ${cancelled} 条排队消息`);
@@ -426,7 +464,19 @@ deps, opts = {}) {
426
464
  }
427
465
  return;
428
466
  }
429
- // 3. 普通文本:通道内串行入队(每 key 一条 Promise 链),全局并发由 Semaphore 限制
467
+ // 3. 普通文本:通道内串行入队(每 key 一条 Promise 链),全局并发由 Semaphore 限制。
468
+ // cmd.taskText = handleCommand 未识别时原样带出的文本(触发词改写结果或原文)
469
+ if (triggered !== null && hit) {
470
+ // 两段式触发(ask_detail):命中「需补充」规则先挂起追问、不入队——本通道同用户的
471
+ // 下一条非 / 消息在 2.4 被视作补充合并入队(超时 / 本地命令取消,见 supplementPending)。
472
+ // rewrite 指向本地命令的规则防呆跳过:/new 这类命令无参数可补,追问只会挡住命令执行
473
+ if (hit.rule.askDetail && !isLocalCommandRewrite(hit.rule.rewrite)) {
474
+ supplementPending.set(key, { rule: hit.rule, originalText: trimmedText, userId: msg.userId, at: Date.now() });
475
+ await deps.gateway.sendTextTo(msg.chatId, `⚡ 已命中触发词「${hit.rule.match}」,请直接回复补充内容(如目标链接、具体要求),我会合并后开始任务(10 分钟内有效;发 /stop 取消)`);
476
+ return;
477
+ }
478
+ await deps.gateway.sendTextTo(msg.chatId, '⚡ 已按触发词转入对应技能流程');
479
+ }
430
480
  enqueue(key, msg, cmd.taskText ?? msg.text, currentWorkspace);
431
481
  }
432
482
  async function executeTask(key, msg, prompt, wsName, myEpoch) {
@@ -588,6 +638,8 @@ deps, opts = {}) {
588
638
  // SOP 开关:本任务级现读 config.session.notifySop(缺省 true),决定是否注入推送规范
589
639
  // 提示词(v2 为纯内容分类软约束;旧「>50 行转文件」硬兜底已移除,send_text 恒多卡)
590
640
  const sopEnabled = config.session?.notifySop !== false;
641
+ // 工具可用性说明开关(缺省 true):对抗 dynamic tool loading 下的「核心工具不存在」自查误判
642
+ const toolHintEnabled = config.session?.toolHint !== false;
591
643
  const sentPaths = new Set();
592
644
  const sink = async () => { await activeProgress.get(key)?.sinkToBottom(); };
593
645
  const notifySender = createGatewaySender({
@@ -647,28 +699,51 @@ deps, opts = {}) {
647
699
  console.warn(tag, `[env] ${k} 的值含未展开的 \${...} 引用(env 值不做变量展开),请确认是否误配`);
648
700
  }
649
701
  }
702
+ // 机器人级厂商档案(每任务现读解析,热生效):认证经惰性生成的 per-bot settings 文件以
703
+ // CLI --settings 参数注入(命令行层优先级最高,不被生效目录 settings.json 的 env 块压制);
704
+ // 模型并入 /model 优先级链走 opts.model。档案名不存在仅 warn 回退全局(档案可能后建)
705
+ const appProfile = resolveAppProfile(config, app);
706
+ if (app.profile && !appProfile) {
707
+ console.warn(tag, `[app-profile] 机器人 ${app.name || app.appId} 配置的厂商档案 "${app.profile}" 不存在,本任务跟随全局`);
708
+ }
709
+ const profileModel = resolveAppProfileModel(config, app);
710
+ let profileSettingsPath;
711
+ if (appProfile) {
712
+ try {
713
+ profileSettingsPath = ensureProfileSettingsFile(appProfile);
714
+ }
715
+ catch (e) {
716
+ console.warn(tag, `[app-profile] 档案 "${appProfile.name}" 的 settings 文件生成失败,本任务认证跟随全局:`, e instanceof Error ? e.message : e);
717
+ }
718
+ }
650
719
  const outcome = await deps.executor(prompt, {
651
720
  cwd: workspacePath(wsName),
652
721
  resumeSessionId: resumeId,
653
722
  signal: abort.signal,
654
723
  env: taskEnv,
655
- // 人设 + SOP 软约束:人设裸文本追加在 Claude Code 长系统提示末尾,对模型的约束力
656
- // 天然偏弱(resume 旧会话时历史风格会带偏)——包一层显式遵循框架语显著改善
657
- // 称谓/口吻/能力定位的遵循度(提示工程手段,非硬约束);SOP 摘要附后
724
+ // 人设 + SOP + 工具说明三段软约束(各自独立,按开关参与):人设裸文本追加在 Claude Code
725
+ // 长系统提示末尾,对模型的约束力天然偏弱(resume 旧会话时历史风格会带偏)——包一层
726
+ // 显式遵循框架语显著改善称谓/口吻/能力定位的遵循度(提示工程手段,非硬约束)
658
727
  appendSystemPrompt: (() => {
659
728
  const raw = app.appendSystemPrompt?.trim() ?? '';
660
729
  const persona = raw
661
730
  ? `【机器人人设(用户为该机器人配置,须在每一轮回复中严格遵循,优先级高于默认回复风格)】\n${raw}\n【人设结束】以上人设适用于本会话每一轮回复——包括称呼、口吻、能力定位的自我介绍;即使历史回复风格不同,也从本轮起遵循。`
662
731
  : '';
663
- if (!sopEnabled)
664
- return persona || undefined;
665
- return persona ? `${persona}\n\n${FEISHU_NOTIFY_SOP_PROMPT}` : FEISHU_NOTIFY_SOP_PROMPT;
732
+ return [
733
+ persona || undefined,
734
+ sopEnabled ? FEISHU_NOTIFY_SOP_PROMPT : undefined,
735
+ toolHintEnabled ? TOOL_HINT_PROMPT : undefined,
736
+ ].filter(Boolean).join('\n\n') || undefined;
666
737
  })(),
667
738
  // 计划模式(#6):通道级 /plan 开关——模型先出计划(ExitPlanMode → planAsk 飞书卡片),
668
739
  // 用户批准后 SDK 自动切回可编辑模式继续执行(替代旧工作区 code-dev 类型)
669
740
  ...(state?.planMode ? { permissionMode: 'plan' } : {}),
670
- // 通道级模型覆盖(/model 设置,每任务现读——改完下一条消息即生效);未设 = 跟随 ~/.claude 全局
671
- ...(state?.model ? { model: state.model } : {}),
741
+ // 模型优先级(每任务现读,改完下一条消息即生效):通道 /model 覆盖 > 机器人档案
742
+ // (profile_model > 档案默认 model,经 --model 参数路由,不受 settings.json 遮蔽)>
743
+ // 不传(跟随生效目录 settings.json 全局配置)
744
+ ...(state?.model ? { model: state.model } : profileModel ? { model: profileModel } : {}),
745
+ // 机器人档案认证 settings 文件(--settings):仅含认证键,命令行层压制生效目录 env
746
+ ...(profileSettingsPath ? { extraArgs: { settings: profileSettingsPath } } : {}),
672
747
  // 进程内通知工具(send_text/send_image/send_file):中间产物实时推给当前聊天。
673
748
  // server 实例按任务构造,chatId 在 sender 闭包内硬绑定——模型无法选择接收者。
674
749
  // 配置页 MCP(#12)一并注入:bridge 自管 servers.json 每任务现读(热生效),notify 后展开保名
@@ -781,29 +856,19 @@ deps, opts = {}) {
781
856
  }
782
857
  // 结果回传两段式:finalText 非空即独立多卡直接展示(~3000 字/块,连发间隔 300ms 防飞书
783
858
  // 消息限流)——不论长短都不再进进度卡正文(主卡只留提示行),彻底消除「折叠看不到全文」
784
- // 与「卡片/消息两边重复」(旧三段式 ≤400 终态自载的边界问题)。最后一张卡带「确认方案/
785
- // 按意见修改」引导:点击合成用户消息入队发起新一轮任务(resume 上一会话,上下文天然在场)
859
+ // 与「卡片/消息两边重复」(旧三段式 ≤400 终态自载的边界问题)。尾卡纯展示,想继续对话/
860
+ // 提意见直接回复消息即可
786
861
  if (finalText) {
787
862
  const chunks = chunkText(finalText);
788
863
  const total = chunks.length;
789
864
  const resultReq = {
790
- requestId: randomUUID(), charCount: finalText.length, workspaceName: wsName,
865
+ charCount: finalText.length, workspaceName: wsName,
791
866
  };
792
- outputPending.set(resultReq.requestId, {
793
- req: resultReq,
794
- // 尾卡正文快照:settled 回调响应内联换卡替换整卡,须带回正文重建
795
- tail: { content: chunks[total - 1], index: total, total },
796
- ownerId: msg.userId, chatId: msg.chatId, settled: false,
797
- });
798
- if (outputPending.size > 500) {
799
- // 防无界增长:清最老的条目(Map 迭代序即插入序;被清卡片的确认按钮会提示已处理)
800
- outputPending.delete(outputPending.keys().next().value);
801
- }
802
867
  for (let i = 0; i < total; i++) {
803
868
  if (i > 0)
804
869
  await new Promise((r) => setTimeout(r, 300));
805
870
  if (i === total - 1) {
806
- await deps.gateway.sendCardTo(msg.chatId, buildResultTailCard(resultReq, chunks[i], { index: total, total }, undefined, config.card?.width ?? 'default'));
871
+ await deps.gateway.sendCardTo(msg.chatId, buildResultTailCard(resultReq, chunks[i], { index: total, total }, config.card?.width ?? 'default'));
807
872
  }
808
873
  else {
809
874
  await deps.gateway.sendTextTo(msg.chatId, `${resultChunkHeader(resultReq, i + 1, total)}\n\n${chunks[i]}`);
@@ -1004,52 +1069,6 @@ deps, opts = {}) {
1004
1069
  }
1005
1070
  return { toast: { type: 'info', content: '该提问已被处理或已过期' } };
1006
1071
  }
1007
- // 长回复收起卡(0.20.0):任务已结束,无 Promise 挂起——outputPending 提供发起人校验、
1008
- // settled 防重与文件上下文。「查看完整内容」可反复点击;「确认方案 / 按意见修改」合成
1009
- // 一条用户消息入队,走与真实消息完全相同的排队/resume/进度卡链路发起新一轮任务
1010
- const out = outputPending.get(action.value.requestId);
1011
- if (out) {
1012
- if (action.operatorId !== out.ownerId) {
1013
- console.log(tag, `[卡片回调] 非发起人 ${action.operatorId} 点击长回复卡 ${action.value.requestId},已忽略`);
1014
- return { toast: { type: 'info', content: '仅任务发起人可操作' } };
1015
- }
1016
- const decision = action.value.decision;
1017
- if (out.settled) {
1018
- return { toast: { type: 'info', content: '该回复的处理已提交,无需重复操作' } };
1019
- }
1020
- if (decision === 'output-confirm' || decision === 'output-revise') {
1021
- const feedback = (action.value.feedback ?? '').trim();
1022
- if (decision === 'output-revise' && !feedback) {
1023
- return { toast: { type: 'warning', content: '请先在输入框填写修改意见(或点「确认方案」)' } };
1024
- }
1025
- out.settled = true;
1026
- const prompt = decision === 'output-confirm'
1027
- ? '用户已在飞书点击「确认方案」,确认了上一轮的完整回复(方案),请继续按该方案推进执行。'
1028
- : `用户要求按以下意见调整上一轮完整回复中的方案:\n${feedback}`;
1029
- // 合成用户消息入队:新一轮会 resume 上一会话(「上一轮方案」上下文天然在场)。
1030
- // 发起人必在访问白名单(能发起任务才拿得到卡片),仍做 isAllowed 纵深校验;
1031
- // chatType 不参与任务链路(仅 gateway 消息解析用),合成消息固定 p2p
1032
- if (deps.access.isAllowed(action.operatorId)) {
1033
- const key = channelKey(out.chatId, action.operatorId);
1034
- enqueue(key, {
1035
- chatId: out.chatId, chatType: 'p2p', userId: action.operatorId,
1036
- text: prompt, messageId: `output-${action.value.requestId}`,
1037
- }, prompt, out.req.workspaceName);
1038
- }
1039
- else {
1040
- console.warn(tag, '[结论尾卡] 发起人不在访问白名单,已忽略新一轮请求:', action.operatorId);
1041
- }
1042
- const settledText = decision === 'output-confirm'
1043
- ? '✅ 已确认该方案,正在发起新一轮任务(详情见下方新进度卡)'
1044
- : '✏️ 修改意见已收到,正在发起新一轮任务(详情见下方新进度卡)';
1045
- return {
1046
- toast: { type: 'success', content: settledText },
1047
- // 回调响应内联换卡:替换整张尾卡,正文快照带回(按钮区收为一行 settled 文案)
1048
- card: { type: 'raw', data: buildResultTailCard(out.req, out.tail.content, { index: out.tail.index, total: out.tail.total }, settledText, config.card?.width ?? 'default') },
1049
- };
1050
- }
1051
- return { toast: { type: 'info', content: '该操作无效或已过期' } };
1052
- }
1053
1072
  // 工作区切换卡(裸 /ws):无 Promise 挂起——wsPending 提供 admin 校验、stale 失效
1054
1073
  // 与工作区快照。权限语义与 /ws use 一致是「admin 可切换」而非「仅发起人」:member
1055
1074
  // 能拿卡看列表,点按钮被拒(卡片保持原样,admin 仍可点),与其它卡的 ownerId 校验刻意不同。
@@ -1149,7 +1168,8 @@ deps, opts = {}) {
1149
1168
  * 原地 mutate 下一条消息即生效(env 变更不再要求重启)。
1150
1169
  * 不热应用(且不纳入 sameApps 比较):FeishuGateway 与 Semaphore 均为启动时构造,
1151
1170
  * apps 凭证 / concurrency 变更无法热生效,打警告提示重启,避免「以为已生效」的坑。
1152
- * 读失败(文件被写坏的中间态等)沿用旧值不崩。
1171
+ * 读失败(文件被写坏的中间态等)沿用旧值不崩,并返回用户可读的错误消息(成功返回 null)——
1172
+ * 调用方(processMessage)据此在聊天里显式提示「本次沿用旧配置」,不再静默。
1153
1173
  */
1154
1174
  export function createConfigReloader(config, configPath) {
1155
1175
  return () => {
@@ -1158,21 +1178,23 @@ export function createConfigReloader(config, configPath) {
1158
1178
  fresh = loadConfig(configPath);
1159
1179
  }
1160
1180
  catch (e) {
1161
- console.warn('[配置热重载] 读取失败,沿用旧配置:', e instanceof Error ? e.message : e);
1162
- return;
1181
+ const message = e instanceof Error ? e.message : String(e);
1182
+ console.warn('[配置热重载] 读取失败,沿用旧配置:', message);
1183
+ return message;
1163
1184
  }
1164
1185
  if (!sameApps(fresh.apps, config.apps) || fresh.concurrency !== config.concurrency) {
1165
1186
  console.warn('[配置热重载] 检测到应用列表/凭证或 concurrency 变更,需重启后生效');
1166
1187
  }
1167
- // claude 段仅 mode/profiles 变更才需重启(CLAUDE_CONFIG_DIR 在 createBridge 构造时定妆);
1188
+ // claude 段仅 mode 变更才需重启(CLAUDE_CONFIG_DIR 在 createBridge 构造时定妆);
1168
1189
  // env 变更热生效(每任务 buildTaskEnv 现读)不在此 warn;顶层四字段(auth_token/api_key/
1169
1190
  // base_url/model)变化无需重启——写入侧(配置页 PUT / use-profile / 飞书 /model-profile)
1170
1191
  // managed 模式下已即时 syncManagedClaude 重写托管 settings.json,下一条任务消息即生效,
1171
- // 此处再 warn 会误导「以为没生效」。server 段为启动时定妆照(startBridge 时监听),
1172
- // 运行中变更只能提示重启
1173
- const claudeRestartShape = (c) => JSON.stringify(c ? { mode: c.mode, profiles: c.profiles } : {});
1192
+ // 此处再 warn 会误导「以为没生效」。profiles 变更同样热生效——bot 级档案每任务现读解析
1193
+ // (app-profile.ts),全局顶层四字段亦经各写入侧即时落托管盘,此处不比较 profiles。
1194
+ // server 段为启动时定妆照(startBridge 时监听),运行中变更只能提示重启
1195
+ const claudeRestartShape = (c) => JSON.stringify(c ? { mode: c.mode } : {});
1174
1196
  if (claudeRestartShape(fresh.claude) !== claudeRestartShape(config.claude)) {
1175
- console.warn('[配置热重载] 检测到 claude 模式 / 档案列表变更,需重启后生效');
1197
+ console.warn('[配置热重载] 检测到 claude 认证模式变更,需重启后生效');
1176
1198
  }
1177
1199
  // claude.env 变更探测:托管盘重写要在 mutate config.claude 之前取旧值比较
1178
1200
  const claudeEnvChanged = JSON.stringify(fresh.claude?.env ?? {}) !== JSON.stringify(config.claude?.env ?? {});
@@ -1211,8 +1233,12 @@ export function createConfigReloader(config, configPath) {
1211
1233
  config.apps[i].triggers = fa.triggers;
1212
1234
  config.apps[i].plugins = fa.plugins;
1213
1235
  config.apps[i].env = fa.env;
1236
+ // 机器人级厂商档案热生效:executeTask 每任务经 app-profile.ts 现读解析
1237
+ config.apps[i].profile = fa.profile;
1238
+ config.apps[i].profileModel = fa.profileModel;
1214
1239
  });
1215
1240
  }
1241
+ return null;
1216
1242
  };
1217
1243
  }
1218
1244
  export async function startBridge(configPath = CONFIG_PATH) {