Conversation
去掉「全局会议监听 bot」限制,让每个开了 vcMeetingAgent 的 bot 各自处理自己 收到的会议事件,配合 per-bot 预设(方案 B)。 1. 去全局 listener pin: vcMeetingAgentGlobalListenerAppId() 恒返 undefined, 4 处 guard(restore/rejoin/confirm/push)全短路成「每 bot 各自处理」—— 这本就是 listenerBotAppId 未设时的语义。老 listenerBotAppId 配置忽略并在 首次触发时 warn 一次(break change,不做迁移)。 2. 「未操作选择卡时」概念重命名为「默认行为」(defaultMode/defaultConsumers 两个 i18n 键 zh+en),帮助文案讲清:默认走「启用所选预设」,选「仅监听」则 只记录;未勾选任何默认预设时即使选启用预设也退化为仅监听。超时兜底本就按 defaultMode 走(bootstrap 默认 seed defaultMode:agents),语义不变仅正名。 3. 修离线 bot 在会议 agent 下拉显示 appId: onlineBotName 只查在线注册表, 离线 bot 返 undefined→兜底成 cli_xxx。新增 readPersistedBotNames() 读 bots-info.json 的飞书探测名,离线 bot 也显示友好名。 测试: 两个旧的 pin-拦截测试改写成「pin 被忽略、每 bot 各自入会」;新增 offline-bot-persisted-name 用例;更新「默认行为」文案断言。VC 6 套件 234 绿。 影响面: 仅 VC 会议 consumer 编排 + dashboard 展示。跨 bot: 去 pin 后同一场 会议多个 bot 可各自独立跑各自预设(不防重,符合每 bot 都能进会的初衷)。 不涉及跨平台/跨后端差异。权限自愈(自动补 scope/event)作为后续 PR。 (cherry picked from commit 45f2404d874e4e0e94e83f25cba2b829e647302c)
把当前已 build 部署到 fleet 的 VC 监听相关改动固化成分支基线,便于 Plan B 重构逐阶段 diff / 必要时回滚到当前运行态。含: - 启动时自动探测并订阅 VC 事件(open-platform-automation 只读探测 + event-dispatcher ensureVcMeetingEventsSubscribed) - 会中文字输出默认策略 approval→allow(可 dashboard 切回需授权) - vcMeetingAgent 默认开启(仅 apiOnly / 显式 enabled:false 关闭) - receiver 流式卡片可选暴露(exposeReceiverStreamingCard)+ 相关抑制/文案 - textOutputPolicy / voiceOutputPolicy 配置解析 已去除临时 vc-card-diag 诊断日志。tsc --noEmit 干净。此基线仅作 checkpoint, 其中卡片暴露/抑制类改动将被后续 Plan B 重构(会议 agent 改为普通会话)取代。
方向B重构第一阶段,单独修好主诉求(普通消息/@消息/字幕都能进同一个可交互会话)。
根因与改动:
- activeSessionKey(core/types.ts)不再把带 vcMeetingReceiver 标记的会话按
`vc-receiver:${sessionId}` 单独建 key,统一落到普通 `(chatId, appId)` 槽。
这是"会议监听交互全废"的一行根因——旧 key 把会议会话和普通 chat 会话分成
两个宇宙,普通 IM(按 chat anchor 路由)永远找不到会议会话。
- persistedActiveSessionKey(worker-pool.ts)同步改,保证重启恢复用同一个 key,
不出现 live/restore 双 key 裂脑。
- ensureVcMeetingReceiverSession(daemon.ts)改成"取/建监听群的普通会话":同
(chatId,appId) 槽已有会话则复用并 stamp 会议投递身份 metadata,否则建一个
worker:null 冷会话(形状同普通自建会话,首次投递时冷 fork spawn)。放松旧的
"一 session=一 exact meeting/member/epoch"强校验(普通会话模型下一个群可承载
多场会议投影)。抽出 stampVcMeetingBinding。
- maybeCatchUpVcMeetingConsumerBeforeTurn 不再返回 `vc-receiver:` anchorOverride
(普通会话 anchor=chatId 天然命中);仍保留 ambiguous/lagging 时 block、@mention
follow-up 时 stamp vcMeetingImTurnOrigin。
- dispatch.ts foldableChatSessionAppIds 去掉 vcMeetingReceiver 排除(会议会话现在
就是普通 chat 会话,mention 应折回它);deferredScheduleRun 仍排除。
vcMeetingReceiver 标记保留,从"路由 key"降级为"纯投递/会中发言 metadata"
(Stage6 会中发言仍需)。抑制/守卫/恢复解耦留待后续阶段。
测试:
- 改 vc-meeting-daemon-session(activeKey 现=ordinaryChatKey、无 anchorOverride)、
session-resume(旧"三 vc-receiver key 不塌缩"→Plan B"塌缩到普通槽+胜者保留+
重复 loser 关闭")、dispatch(拆成 deferred 仍排除 + VC 现可折回两条)。
- 新增聚焦测试:activeSessionKey 落普通槽不含 vc-receiver: 前缀且 map 命中。
- tsc --noEmit 干净;相关 VC/routing/lifecycle 单测全绿(vc-daemon-session 126、
session-resume 39、dispatch 93、session-reply-thread-anchor 18、
im-routing/recovery/delivery/lifecycle 506)。全量单测除既有基线(skill-doctor)
与并行争用 flake(隔离复跑均绿)外通过。
未 build 未部署:重构期间只跑 tsc+vitest,不动 live dist。
方向B重构第二阶段。会议 agent 变成监听群普通会话后,原来"凡是会议会话就
一刀切抑制卡片/reaction/输出"的判断会顺带误伤普通用户消息(=之前报的
"手动@机器人也不回复")。核心是引入统一判据区分「会议驱动 turn」与「普通
用户 turn」,只对前者保留 receipt-fenced 抑制,后者一律走正常回复路径。
新增共享判据 isMeetingDrivenTurn(ds,turnId,dispatchAttempt)(worker-pool.ts 模块级):
vcMeetingReceiver && (dispatchAttempt!==undefined || 有 stamped 会议@来源)。
只有字幕投递 turn 或带会议来源的@追问才算 meeting-driven。
改动(全部把 blanket `if(vcMeetingReceiver)` 换成 meeting-driven 判据):
- managedAuxUiSuppressed / managedFinalOutputSuppressed:普通用户 turn 落
ordinaryManagedSuppression;仅 meeting-driven 走 receipt/durable 策略
- worker.on('error') fork 诊断、failOrdinaryImDelivery 投递失败上报:普通
用户 turn 的失败正常上报给用户,仅 meeting-driven 静默 fence 到 receipt 链
- shouldTrackOrdinaryImDelivery:普通用户 turn 现在被 track 进 receipt-ACK
+ 重试(失败能上报);仅 meeting-driven 走 receipt/lease 路径
- finishTurnReactions / noteTurnReceived(daemon.ts):普通用户消息正常
✋→✅ 进度 reaction;仅 stamped 会议@追问不加 reaction
- deliverFinalOutput 最终输出主投递门:managedDecision 只对 meeting-driven turn
计算,普通用户 turn 的 final_output 走普通投递(否则被判 origin_unproven 静默丢)
保留:silent/listener_thread 的 responseMode 群发策略(功能,非 bug)、
evaluateVcMeetingManagedSend 各 policy 输入、sessionReply 的 sourceSessionId
归属。relay/fork/resume/dashboard-trigger 守卫留待 Stage 3;boot-recovery
留待 Stage 4;lifecycle-hooks 对 receiver 的安全 fail-closed 暂留待评估。
删除:流式卡片专用抑制 vcReceiverStreamingCardSuppressed + exposeReceiverStreamingCard
配置字段 + /card 的 vc_receiver 专用文案(i18n zh/en)。dispatchTurn 现在为
每个字幕投递 turn 都 arm 流式卡片(去掉 exposeReceiverStreamingCard opt-in
门)——修好"看不到流式卡片";silent turn 仅抑制 final output,不抑制 live 卡片。
测试:tsc --noEmit 干净;api-only-mode 源锁多断言改为 isMeetingDrivenTurn 三处
新契约;recall-frozen-cards 两测反转(卡片/usage patch 现在该出);
session-lifecycle-start 拆成"普通用户 turn fork 失败该上报"+"字幕投递 fork
失败仍 fence";turn-reactions 反转(会议会话普通 turn 该有 reaction);
bot-registry/command-handler 去 exposeReceiverStreamingCard/vc_receiver 断言。
全部 Stage-2-touched 单测 711 绿(daemon-session 126 等);全量除既有基线
skill-doctor + 并行 flake(隔离复跑绿)外通过。未 build/未部署。
方向B重构第三阶段。会议 agent 变普通会话后,原来禁止它 relay/fork/resume/ 被 dashboard 触发的守卫都不再成立——普通会话本该支持这些操作。 删除的守卫: - transferSession(worker-pool):去 vc_receiver_not_relayable,会议会话可像任何 会话一样 relay(sessionId 不变,投递账本仍按 sessionId 命中) - forkSession(worker-pool):去 vc_receiver_not_forkable,可 fork - 中转目标冲突扫描(worker-pool):去掉把 vcMeetingReceiver 会话排除在同 anchor 兄弟之外的例外——它现在住普通 chat key,同 anchor 就是合法冲突,应计入 - resumeSession(session-manager,pre-lock + 锁内双查):去 vc_receiver_managed, 关闭的会议会话可 resume 回普通 chat 槽(原注释担心的"塌缩进普通槽"正是 Plan B 预期行为);从返回类型 union 里移除该错误码 - dashboard/HTTP trigger 端点(dashboard-ipc):去 managed_receiver_requires_delivery_endpoint, 普通 trigger 可寻址会议会话(botmux send / dashboard);字幕投递仍走自己的 fenced 投递路径,此端点只承载普通用户发起的 turn 测试:tsc --noEmit 干净;session-resume「拒绝 resume 会议 receiver」改为「Plan B 可 resume,此处因 anchor 已被普通 chat 占用而落 anchor_occupied」;dashboard-ipc 「拒绝 resume」改为「Plan B 成功 resume 进普通槽 + fork」。fork/transfer/relay-pickup 的 adopt_* 用例不受影响。relay/fork/resume/dashboard 相关 268 绿;全量除既有基线 skill-doctor + 并行 flake(隔离复跑绿)外通过。未 build/未部署。
方向B重构第四阶段。原计划担心启动恢复(boot recovery)按 vcMeetingReceiver marker 判定"该走恢复",Plan B 下 marker 也在普通用户 turn 会话上,会误伤。 核查后确认**无需改生产代码**: - 恢复入口 ambiguousOnBoot 来自 reconcileVcMeetingDeliveriesOnBoot(扫描投递 账本里 stale 的 dispatched receipt),本就按"有在途投递 receipt"判定,不是 "session 有 marker"。普通用户 turn 没有投递账本条目,结构上不可能进恢复。 - 恢复循环用 findActiveBySessionId(ref.receiverSessionId) 找会话(按 sessionId, 不受 activeSessions key 从 vc-receiver 改成普通 chat key 影响);Stage1 已把 persistedActiveSessionKey 统一成普通 key,重启后会话注册在普通槽,恢复照样找到。 - 会话若在 Stage1 restore-collapse 中作为同 key 冲突 loser 被关闭, findActiveBySessionId 返回 undefined → 落 vcMeetingRuntimeLeaseRecovery (kill+probe 孤儿 pane),是正确的 fail-closed。 - worker.ts reset_ambiguous_receiver 的 marker 校验是"确认目标确实是会议会话" 的正确性 sanity check(仅当会话 active 且 marker 在时才到达),保留。 新增 2 个回归测试锁定不变量:①源锁断言恢复 eligibility 走 ledger 派生的 ambiguousOnBoot + findActiveBySessionId,非 marker 扫描;②无投递 receipt 的 会话(普通用户 turn)isBlocked 返回 false。recovery-lifecycle 6 绿。tsc 干净。
方向B重构第五阶段(durable 状态迁移)。原计划担心真实部署的旧 vcMeetingReceiver 持久化行在重启后需要专门迁移。核查后确认**无需专门迁移代码**: - 旧会议行(带 marker 持久化):重启走 restoreActiveSessions → persistedActiveSessionKey (Stage1 已统一成普通 chat anchor),全部塌缩到普通 (chatId,appId) 槽;有竞争则 CAS 选胜者关重复 loser,无竞争则原样恢复且 marker 保留。已由 Stage1 的 「塌缩」测 + 本阶段新增「独行旧会议行原样恢复」测覆盖。 - 会议运行时监控记录(runtime store,按 meetingId):由 restoreVcMeetingRuntimeSessionsForBot 独立 rehydrate,与 marker key 无关;会议活跃时重新 ensureVcMeetingReceiverSession (Stage1 已改成取/建普通会话)。 - 投递账本 receipt:deliveryKey 含 target.sessionId,而 sessionId 在 key 变更中 稳定不变(只是 activeSessions 的 map KEY 从 vc-receiver 改普通),故 receipt 仍按 sessionId 命中,无需硬迁哈希。 - 全仓已无任何代码构造/读取旧 `vc-receiver:` key 格式(仅剩一句注释)。marker 保留 不删,旧行与新行读取一致,无版本 skew,「读旧 marker 一个 release」的顾虑也消解。 新增 1 个迁移回归测试:独行旧会议行重启后落普通 chat key、marker 作为投递 metadata 原样保留、会话 active。session-resume 40 绿。tsc 干净。
方向B重构第六阶段。申晗定"会中往会议里发文字/语音"是刚需必须保留。 核查确认**无需改代码即完整保留**:整条会中发言链(request-output → daemon /api/vc-meetings/action-request → 转发 listener daemon 的 managed-action)的每个 授权点都按 sessionId + 保留的 vcMeetingReceiver marker + managedTurnOrigin / vcMeetingImTurnOrigin 判定,**没有一个依赖 Stage1 改动的 activeSessions map key**: - action-request 入口(daemon.ts):findActiveBySessionId(按 sessionId)+ `if(!ds?.session.vcMeetingReceiver)` 门(marker 保留 → 仍通过) - managed_turn_origin IPC arm ds.managedTurnOrigin(worker-pool):按 sessionId + worker 代次判定,与 map key 无关 - evaluateVcMeetingManagedSend(worker.ts request-output origin):receiverSession = !!session.vcMeetingReceiver(marker 保留 → true),按 delivery receipt / imOrigin 授权 这正是 Plan B 决定「保留 marker 作纯 metadata 而非删除」的收益。 验证:action-gate 25 / send-policy 16 / action-store 35 / listener-output-protocol / realtime-voice 16 全绿;daemon-session 里 submitManagedImOutput 全链用例(会中文字 输出 202 pending 接受 + 各拒绝路径)在「会议 agent = 普通会话(activeKey === ordinaryChatKey)」前提下通过。新增 1 源锁测试钉住 action-request 仍按保留 marker 识别会议 agent,防未来清理 marker 时静默弄坏会中发言。tsc 干净;全量除既有基线 skill-doctor + 并行 flake(隔离绿)外通过。 至此 Plan B 六阶段全部完成(分支 vc-plan-b),未 build/未部署,待申晗确认后 switch:here + daemon:restart 飞书实测。
Plan B 实测发现的真 bug。会议 agent 现在是普通会话,会穿插普通 IM turn 且在 turn 之间到达 idle。会中发言的授权凭证 ds.managedTurnOrigin 在字幕投递 turn 的 terminal 边缘被清除,而模型往往在投递 turn 结束(或穿插的 IM turn 之后)才真正 执行 request-output——此时 in-memory origin 已没,action-request 报 origin_unproven, 即便持久化投递 receipt 仍完整授权它。 修复:action-request 授权点在 live managedTurnOrigin 缺失、但 CLI 带来了 claimed delivery origin(originTurnId + originDispatchAttempt)时,回退用 evaluateVcMeetingManagedSend 按 sessionId+turnId+dispatchAttempt 对**持久化 receipt** 重新验证(与 final-output 路径同一条权威链):receipt 必须存在于本 receiver session、attempt 匹配、状态为 dispatched/completed、projection 仍 active、且非 silent 投递。只有 listener_thread 决策才合成 verified origin。这不放松任何边界(receipt 本身授权不了的一律拒), 只是跨过 idle 间隙。 安全性:普通用户 turn(om_ turnId 无 receipt)走不进这条回退——claimedAttempt 必须有值且 receipt 必须命中,否则回退不触发,正确落回 IM-origin 路径。 测试:tsc 干净;evaluateVcMeetingManagedSend 的 completed-receipt/allowTerminalReceipt 授权链 + failed/ambiguous 不授权的负例在 send-policy 测已覆盖;新增 api-only-mode 源锁钉住 action-request 的回退分支结构(live 缺失→durable receipt→listener_thread)。 action-gate/action-store/im-reply/dashboard-ipc/delivery-receiver/daemon-session 全绿。
申晗定的方向B。responseMode:silent 本应只表示"不往监听群自动刷屏",却在 evaluateVcMeetingManagedSend 里一票否决了会中 request-output——导致"会议主持" (facilitator 模板自带 responseMode:silent,角色指令却要求会中发言)永远无法在 会里说话,agent 自己推断"physically gated closed"转入纯观察。 根因梳理: - 会中输出(request-output → hub managed-action)与监听群 auto-post(final_output / botmux send)本是两条独立通道。会中输出的授权在 hub 的 authorize 回调 (capability + textOutputPolicy/voiceOutputPolicy),**本就不看 responseMode**; action-request 主路径用 verifyVcMeetingManagedOriginClaim(也不看 silent)。设计 文档(send-policy 注释)早就写明这个分离。 - 但上一个 commit 修 idle-gap 时,回退用了 evaluateVcMeetingManagedSend——它含 silent 一票否决——把 silent 挡会中输出的行为**重新引回**了会中通道。 修法(外科式):给 VcMeetingManagedSendOrigin 加 forInMeetingOutput 标志。设 true 时(仅会中输出授权路径用)仍校验 receipt 身份/在途(存在于本 session、attempt 匹配、active projection、dispatched/completed),但跳过 silent 一票否决——因为 silent 只管 auto-post,会中输出由 hub 的 policy 门控。监听群通道所有调用点 (worker-pool final_output、cli botmux send、sandbox relay)都不带该 flag,silent 继续抑制群发。只有 daemon action-request 的 idle-gap 回退带 forInMeetingOutput。 效果:silent facilitator "群里安静、会里能说"。主路径(live origin 在)本就 silent-free 现在与回退一致。 测试:tsc 干净;send-policy 新增3测(silent 在 in-meeting 通道放行 + 默认 listener 通道仍 silent_delivery 抑制 + forInMeetingOutput 不绕过身份/在途校验); api-only-mode 源锁更新钉 forInMeetingOutput;action-gate/daemon-session/全量单测 除既有基线+并行flake外通过。未 build,下一步 rebuild 重新部署。
上一个 idle-gap 修复只补了 daemon 半边。实测(facilitator 真机)发现 CLI 半边根本 没把投递身份传过来:cli request-output 的 originTurnId/originDispatchAttempt 取自 "当前活跃 turn 的进程树 marker"(resolveSessionContext→live marker),而 marker 的 turn 字段在字幕投递 turn 结束时被清。agent 通常在投递 turn 结束、idle 之后才决定 发言并执行命令,此时 marker 已空→CLI 发的 action-request 不带 originDispatchAttempt →daemon 的 durable 回退条件(claimedAttempt!==undefined)不满足→回退没跑→仍 origin_unproven。 修复: - 新增 latestVcMeetingDeliveryForSession(delivery-store):按 receiverSessionId 反查 最近一条**可授权**(dispatched 或 completed,含刚 completed 的终态)投递 receipt。 区别于 listActiveVcMeetingDeliveriesForSession(跳过 terminal,idle 后返回空)。 - cli/vc-agent request-output:live marker 缺 turnId/dispatchAttempt 时,回退用 latestVcMeetingDeliveryForSession 取回本会话刚处理那轮投递的 deliveryKey+attempt 再发给 daemon。live marker 在时仍优先(in-flight turn)。 安全:回退按 sessionId 查账本,CLI 只能取到**自己会话**的投递(BOTMUX_SESSION_ID); daemon 侧仍用 forInMeetingOutput 的 durable 授权二次校验(receipt 属本 session + dispatched/completed + active projection),CLI 无法伪造他人投递身份。 测试:delivery-store 新增3测(just-completed 终态可取回而 listActive 跳过 / 只返回 最近可授权且排除 failed/abandoned / 未知会话 undefined);tsc 干净;send-policy/ api-only-mode/action-gate/daemon-session/全量单测除既有基线+并行 flake 外通过。 下一步 rebuild 重新部署。
真机复现走通授权后,发现更深一层:会中发言的动作门(vc-meeting-action-gate:566) 要求投递 receipt 处于 dispatched 才允许创建"会中发言"动作。但主持人往往在字幕 投递那轮**处理完(completed)之后**才决定发言,receipt 已是 completed→被拒 delivery_not_dispatched。这与前几关是同一个"假设发言发生在 live turn 进行中"的 老假设,散落在授权链不同层。 修复:动作门状态检查放开为 dispatched 或 completed。安全性:completed 是终态, 不会再有 attempt N+1 接管(那正是这道门原本要防的竞态),且 dispatchAttempt 相等 校验仍钉死到确切那次;failed/ambiguous/abandoned 仍 fail closed。此函数 (requestVcMeetingManagedAction)唯一调用点是 submitVcMeetingManagedAction(会中 managed-action 路径),不影响监听群 auto-post。 授权后流程无更深 receipt-status 门:text(allow)直接 executeVcMeetingManagedProviderPlan 发送,voice(approval)presentVcMeetingManagedApproval 弹审核卡,均不再看 receipt 状态。 测试:action-gate 新增2测(completed 投递可创建动作 / ambiguous 仍拒); delivery-store + send-policy + daemon-session + api-only-mode 全绿(280 affected)。 tsc 干净。下一步 rebuild 重新部署,这次整条链(CLI 回取身份→daemon durable 授权→ 动作门接受 completed→发送/审核)走通。
四项联动修复,全部针对真机实测暴露的问题: 1. 会中发言授权第5道门:action-request 的 durable receipt 回退原先只在 live origin 已被清(idle)时才跑,而 live 验证只认 rotating capability, 非沙盒会话没有 capability 传输通道(managedOriginChannelRequired 仅 darwin isolate/credential-only bwrap)→ 投递 turn 进行中发言必然 origin_unproven。回退条件放宽为「live 验证失败即可用 durable receipt 重验」,receipt 校验强度不变(sessionId+attempt 匹配+dispatched/completed +projection 活跃),不放大授权面。source-lock 测试同步锁新语义。 2. 字幕投递响应性:授权用户(instruction source)的发言/聊天一律视为 fast signal(原先只有 @ 聊天和带问号的发言),操作者说话即时注入不再 等 30s tick;默认 minBatchChars 400→1,去掉「攒够字数才投」的默认 行为(保留 per-bot 配置可调回)。 3. 会议主持模板 v2:参会者点名/提问/明确请求发言时必须立即通过会中 输出响应,直接请求优先于「是否有主持价值」的自行判断;门禁拒绝时 不静默放弃。 4. 语音默认策略:realtimeVoice.enabled 本身就是显式 opt-in,开启后 默认与文字对齐为直接发送(allow),可经 per-bot voiceOutputPolicy 收紧为 approval/deny;未开 realtimeVoice 仍硬 deny 不变。 dashboard 会话状态投影:worker init 完成但首个空闲提示未出现期间 (首 turn 进行中,screen updates 被抑制),状态从「启动中」改投「工作中」 ——会议会话 spawn 即注入长投递 turn,原先整个首 turn 都显示启动中。 测试:11 相关套件 495 测全绿;voice 审核路径用例显式钉 voiceOutputPolicy:'approval'(它们测的就是审核流,不该依赖默认值); 新增授权用户发言即时注入/状态投影/第5关 source-lock 回归。
VC 会议设置(consumer profiles section)此前只能编辑角色预设,per-bot 的 会中输出策略(文字/语音 allow/approval/deny、realtimeVoice 开关)只能手改 bots.json——「默认放行、可在 dashboard 切回审核」承诺的 dashboard 半边 一直缺位。本次补全: - GET /api/vc-meeting/consumer-profiles 的 agentOptions 附带每个 bot 的 已配置策略与实际生效值(与 daemon 默认推导逻辑保持一致:文字默认 allow;语音未开 realtimeVoice 硬 deny、开启后默认 allow) - PUT 新增可选 botOutputPolicies:先严格校验(未知 appId/非法值/重复 422 拒绝,不写任何东西),快照提交成功后经 config-store 的带锁 read-modify-write(rmwBotEntry)写 bots.json,再对涉及 bot 热重载; 写失败 503 fail-loud,前端重新 GET 收敛真实落盘状态 - 设置页新增「Bot 会中输出策略」子块:每 bot 一行,文字/语音三态下拉 (默认/直接发送/需审核/禁止)+实时语音开关+生效值提示;有自定义配置 或在线的 bot 排前,滚动容器防 49 bot 撑爆页面;仅提交与载入基线有 差异的行,避免并发覆盖其它 bot - i18n 中英词条与样式 测试:dashboard-vc-consumer-profiles-api 54 测全绿(新增 patch 应用/ 校验拒绝/写失败 503/生效值推导 4 用例;DTO 断言补新字段)。
第5关放宽回退条件后,原日志固定写「live origin cleared at turn terminal」 已不准确——非沙盒会话在投递 turn 进行中(live origin 仍在、只是没有 capability 传输通道)也会走这条回退。日志按实际情形分叉,避免后续排查 被措辞误导。
会议预设从 per-bot 配置升级为全 fleet 共享目录,任意 bot 被拉进会即可用; 并补齐入会所需的两项确定性默认,让"装好即用"真正成立。 主要改动: - 角色预设改为全局共享目录(vcMeetingAgent.consumerCatalog),读路径把执行方 绑定为收到会议事件的 bot 自己;预设类型上不再带 agentAppId,从根上消除 "拉 A 进会却把 B 拉进监听群"。退役 daemon 启动时的 per-bot 预设播种写盘。 - 读/存校验分向:读路径逐条 forgiving(坏条目只丢自己,Dashboard 与 daemon 丢同一批),存路径严格给字段级错误。 - per-bot 默认角色覆盖:新增 meetingConsumer.catalogDefaultConsumerId,每个 bot 可从共享目录挑一个进会默认角色,不挑则跟随全局默认。 - 入会身份 larkCliProfile 缺失时读路径默认成 bot 自己的 appId(确定性、非跨 app、 无 secret),配合已有的自动 profile 注册,不再需要手动"配置权限"才能入会。 - relay/seed(Claude Code fork,落盘 JSONL 同构)opt-in reliableTurnTerminal, 可当会议 agent。 - 实时语音默认开启(未显式关即视为开);语音 WebSocket 按需建连,只在真要发言时 才连,纯监听 bot 不空挂。 - 启动权限体检覆盖"默认开"的 bot(改走 vcMeetingAgentConfigActive),缺 VC/实时 语音 scope 时启动即报错并私信 owner;"配置权限"按钮降级为仅当确实缺权限时的兜底。 测试:VC 相关 6 个测试文件 410 用例全绿;tsc + build 通过。 Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts: # src/global-config.ts
|
自动化代码评审初步发现以下问题,建议合入前处理;最终以维护者审阅结论为准。
验证:当前 head |
「按 Bot 的会议开关」表随 fleet 变大后上下滚动体验差、无法定位;每行 bot 名后 平铺的能力警告(尤其「未开沙盒:会议为不可信输入,bot凭证暴露…」)很长,喧宾夺主。 - 加搜索框:按 bot 名称 / appId 过滤行,保存/加载期间冻结。 - 能力缺口收成一个 ⚠,完整文案放 hover title,不再平铺整行。 - 无匹配时给空态提示。 测试:UI 21 用例全绿(新增搜索过滤用例 + 警告 title 断言)。 Co-Authored-By: Claude <noreply@anthropic.com>
…ck剥离 复审发现"VC/实时语音默认开(未配=开,只false才关)"翻转后,"关"这一侧两处没接住: - normalizer 只 `if(enabled===true)` 留 true、丢 enabled:false:默认开翻转后 undefined=active,丢 false=反转→显式 opt-out round-trip 后失效(重载即复活)。 vcMeetingAgent 顶层 + realtimeVoice 两个 normalizer 都补 `enabled:false` 保留。 - bindVcMeetingConsumerCatalogToBot 的 seeded 块 fallback:catalog 显式清空 / 全无效两处原样 `return cfg`,把焊死 foreign agentAppId 的播种块交回→"拉A拉B" 从 fallback 复活。改成 seeded 情形 fallback 剥成仅监听(operator 内容仍原样返回)。 回归测试均过真实 parse→gate composition(非手搓对象),并经反向变异证非 vacuous: 两处 normalizer round-trip 测 + 两处 seeded fallback 剥离测,还原修复即变红。 VC 全套 9 文件 830 测绿;tsc + build 通过。 Co-Authored-By: Claude <noreply@anthropic.com>
改了什么
会议角色预设从「每个 bot 各自配置」升级为「全 fleet 共享目录」,并补齐入会所需的确定性默认,让「任意 bot 被拉进会即可用」真正成立。同时把这条线上此前散落的会中发言修复(Plan B 重构、idle-gap 授权链等)一并收拢。
主要改动:
vcMeetingAgent.consumerCatalog;读路径把执行方绑定为「收到会议事件的 bot 自己」,预设类型上不再带agentAppId,从根上消除「拉 A 进会却把 B 拉进监听群」。退役 daemon 启动时的 per-bot 预设播种写盘(既会永久遮蔽共享目录、又是拉错 bot 的源头)。meetingConsumer.catalogDefaultConsumerId,每个 bot 可从共享目录挑一个进会默认角色,不挑则跟随全局默认(Dashboard「按 Bot 会议开关」行加下拉)。larkCliProfile缺失时读路径默认成 bot 自己的 appId(确定性、非跨 app、无 secret),配合已有的自动 profile 注册,不再需要先手动点「配置权限」才能入会。修此前「拉非默认 bot 进会报缺 larkCliProfile」。reliableTurnTerminal(已核实其 transcript 带权威end_turn+ 回合标记)。修「relay bot 入会报 turn_terminal_unsupported」。vcMeetingAgentConfigActive,不再只认显式enabled:true),缺 VC/实时语音 scope 时启动即报错并私信 owner;「配置权限」按钮降级为仅当确实缺权限时的兜底。为什么
vcMeetingAgent,旧模型下被拉进会只能干听、选不到预设;且旧 bootstrap 会把「另一个 bot」的 appId 焊进预设,导致拉错 bot 进群。larkCliProfile入会身份——旧实现两样都要人工配,产品化「装好即用」被这两道坎挡住。二者都是确定性默认,本就不该要人工。影响面
bot-registry.ts(新增catalogDefaultConsumerId归一化)、global-config.ts(共享目录结构解析)、daemon.ts(effectiveVcMeetingAgentConfig读路径绑定 + 入会身份/实时语音默认)、im/lark/event-dispatcher.ts(启动权限体检门)、dashboard.ts+dashboard/vc-consumer-profiles-api.ts+ 前端 section/i18n。relay/seed两个 Claude-family 适配器的reliableTurnTerminalopt-in;共用基类createClaudeFamilyAdapter未改,其它 20+ CLI 不受影响。测试验证
飞书真机验证项(部署后):拉非默认 bot / relay bot 进会能选到预设并入会;per-bot 默认角色生效;实时语音按需发言。
🤖 Generated with Claude Code