# 专家使用逻辑:REA 源码核对 2026-10-16;WorkBuddy 5.8.2。只读已保存 REA 导出,没有操作原版 UI、召唤、发消息或修改生产代码。下述是固定版本源码的控制流结论;未用行为实测覆盖所有并发、失败和窗口状态。 核心结论:**没有按 expertId 搜索并任意复用历史会话的逻辑**。市场默认召唤准备首页草稿;显式携带 `targetConversationId` / `targetSessionId` 才绑定指定会话。新输入框加号通过独立 provider 流程切换当前输入上下文,与市场共用 `wb.experts.summon` 底层,但不是同一个 React 入口。 ## 证据与可复核锚点 已对以下两个 REA JSON 的全部文本重新计算 SHA-256: - `.tmp/rea-runtime-scripts.json`:339 模块;Evidence `ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb`。 - `.tmp/rea-expert-runtime-scripts-20261016.json`:329 模块;Evidence `ev_e91787b1d4dea651582c6cf12827775b9068b0ee8b80c68ba08d4c1d87d94e90`。 两个导出中本报告使用的同名模块 SHA 一致。原包基线 root SHA 为 `035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918`。以下函数范围单位均为已导出 JS 的 **UTF-16 code unit,start 包含/end 不包含**,不是文件行号;以 Babel AST 核对具名函数范围。 | 模块简称 | 原 `renderer/assets` 文件 | SHA-256 | |---|---|---| | C | `contexts-BMeWSUHk.js` | `5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b` | | P | `workbuddy-provider-DkH2YFYM.js` | `b588e875c469978f216beec86bdf4b6c33a31c0266f9bf996881b8dbf37b5dc7` | | H | `home-CC-czDJL.js` | `121d5a07acccb09e48f20f3a94f9fac8b10770e4a82bca1916d2cccd333f8d14` | | I | `src-DbE1_mgM.js` | `6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4` | | D | `desktop-sdk-setup-C2AUiJmx.js` | `ac3bdeb6d81776491e175ddfae262e64f77d3dc9c0821d6075a91bf1b889dc23` | | L | `wb-input-add-B045GnIg.js` | `7842fa4321d5351bf6c822bbd0253e0d6d6e68cc9cfae3f2470b3d493c23daf4` | ## 市场召唤与会话目标 `C.useExpertSummon$1 [3909983,3917240)` 先调用 expert API 获取已准备的专家(`createExpertApi [3560992,...]` 明确 `summonExpert => getExperts().summon`),合并本地化名称、职业、默认提问、头像、plugin/agent/runtimeIdentity/source 等资料。随后按以下顺序分流: | 目标 | 确切行为 | |---|---| | 合法 Office 返回目标 | 校验 conversation ID、`window:office:` window ID、`returnConversationCreated` 布尔值。新创建目标先 bind;通过 window message 投递带 `suppressDefaultInitPrompt:true` 的专家,再 focus 返回窗口。不会降级成任意其他会话。 | | `targetConversationId` | `returnExpertToConversation` 绑定该 ID,导航回此会话并发布 `prepare-input`,`preserveBlocks:true`,expert 带 `suppressDefaultInitPrompt:true`。没有查找“以前和同专家聊过的会话”。 | | 无 target session/conversation | `conversations.prepareInput({target:{type:'new'},contentBlocks,expert,...})`;SDK 导航 home、发布输入意图,**不在此创建会话或发送**。 | | `targetSessionId` 字符串 | `session.prepare({targetSessionId,context:{expert...}})`,成功后导航指定 ID。`C.useAdapterSession [4306763,4308361)` 在 `expert && targetSessionId` 时调用 `setSelectedExpertForSession`,返回同一 ID。 | `C.bindExpertToConversation [3907799,3908569)` 要求 `conversations.get(id)` 找到目标,否则抛错;经 UI config 或 core config 设置专家、来源与 runtime identity,然后将非 craft 模式改为 craft。`returnExpertToConversation [3908569,3909521)` 的普通返回带 `expertSummonReturn`,包含 expertId、defaultInitPrompt、inputSnapshotToken,之后以 preserveBlocks 发布输入选择。 `C.readExpertSummonReturnIntent [3919686,3920377)` 只读取导航 state 中明确给定的返回 ID;非法 Office 返回信息会丢弃 conversation target。`C.useExpertSummonFlow [3920501,3924917)` 将这些目标原样下传;没有“按 expertId 复用历史”的候选列表/匹配算法。 `D.prepareInput [375609,378110)` 对 `target:new` 导航 home。对当前 active conversation 的 `submitMode:none` 有专门就地更新输入分支;对非当前 conversation 携带 expert 的普通 prepareInput 则明确报“不支持”。市场返回现有会话通过独立 bind+input-intent 流程,不是绕过这项限制偷偷创建另一个会话。 `H` 首页创建回调 `[43600,49000)` 是后续按发送才调用 `wb.conversations.create` 的地方,携带 expert/tag/locale/mode 等;创建并配置成功后按发送前捕获的 revision 清 new-task expert,preparedContext 也只在对象仍是原对象时清除。不能把“已准备专家草稿”当成“已创建专家会话”。 ## 新加号、旧加号与共享底层 新输入框 `I.ExpertsSubmenu [1574199,1579192)` 中普通列表和搜索结果都调用同一个 `onSelect(expert,{position})`。selected 只决定勾选显示,**该函数没有相同 ID 点击短路**。同步传输中/`cloudStatus:checking` 禁用行;搜索有 debounce、IME composition、失败重试;置顶与搜索列表都不改变召唤会话目标。 `I.AddMenu` 专家分支 `[1603400,1604590)`: 1. 保存旧 draft expert;需要 local/cloud transfer 时,先等待 provider.onChange 返回准备好的专家,才放入 draft。 2. 普通选择先设置 `{...expert,suppressAutoSummon:true}` 并关闭菜单,再 await provider.onChange;失败恢复保存的旧 draft expert。 3. “召唤更多”调用 provider.onSummonMore,不自动发消息。 连接路径是 `C.useSharedWbInputProviders [4601705,...] → useWbExpertProvider [3957662,3961924) → useExpertSummon [3951518,3954856)`。会话输入将 conversationId/onExpertChange 传给 provider;首页不传现有 conversationId,而由首页 selection owner 持有选择。`useSharedWbInputProviders` 还在非 silent 操作调用 `shouldBlockExpertChange`;被 team guard 拒绝会抛 `Expert change blocked`。 旧 `L.ExpertsSubmenu [46657,约50700)` 确实存在另一条路:普通最近项用 `onSelect`、点击已选 ID 只关菜单;搜索项用 `onSummonSearchResult`,返回 false 不关菜单。`C.useExpertSearchSummon [4311266,4314603)` 属于此类 legacy main-content 召唤流程,默认将当前 conversationId 作为 targetSessionId,失败按 summonTimestamp 恢复 ref/专家模式。 **不能用旧菜单“已选项直接返回”规则替代新 cr 菜单;也不能用旧搜索独立入口推断新菜单搜索一定走市场 React flow。** 本轮没有查看原版 UI,具体正在显示哪一个菜单实例需由运行时 REA 证据确定;源码中两套路径和新 home/conversation 接线均已明确。 ## 默认提示词、用户草稿与清除 默认文本由输入 owner 的 onChange/applyPrefill 处理。`I.ExpertChip [1693259,约1695700)` 只负责显示/准备中状态、silent hydrate 和清除专家;它本身不写默认提示词。 | 场景 | 源码行为 | |---|---| | 市场默认进入首页 | `C.createExpertSummonInputSnapshot [3886918,3887267)` 将文本判成 user / expert-default / empty。user 文本原 blocks 保留并加 suppressDefaultInitPrompt;其余情况保留非文本资源并置入新默认提示词。无新 default 时保留原 snapshot blocks。 | | 首页直接选择 | `H.useHomeExpertSelection [61126,64079)` 设置 expert/craft;允许 default 且没有非文本 block 时调用 applyPrefill(default)。存在非文本 block 时不会用该调用覆盖整组 blocks。 | | 首页清除 chip | 同一函数的 `expert=null` 分支设置 expert 空并 dismiss dependencies,**没有 applyPrefill(null)**。`setExpert` reducer 只修改 selection,因此不能宣称首页清专家必清默认文本。 | | 会话直接选择 | `C.ConversationInputImpl` 的 `be [7073311,7074328)` 将模式改 craft、写 config expertId;未 suppress 时 applyPrefill(default 或 null)。 | | 会话清除 chip | be 收到 null,清 config expertId、依赖 gate,并 applyPrefill(null);此调用仍受输入是否已由用户编辑保护。 | | 已有会话/持久化恢复 | `suppressDefaultInitPrompt:true` 用于避免恢复绑定时重新填默认文本;同一专家 hydrate 保留现有 suppression 标志。 | `I.createInputBoxApi [1298488,1300741)` 的 applyPrefill 有具体保护:`canAutoFill = draftEmpty || !userEditedFlag`;用户 setText/appendText 或 origin:user 的 setBlocks 更新该标志。传 null 时只在可自动填情况下去除 text blocks,保留非文本资源;用户输入返回 `skipped-not-autofilled`。传新 blocks 时用户已编辑返回 `skipped-user-input`。这是输入来源状态机制,不能仅凭字符串等于 default 就认定可覆盖。 首页 scope 恢复则 `suppressDefaultInitPrompt:true,suppressAutoSummon:true`,不主动重新填默认文本。市场回现有 conversation 虽显式 suppress,**另有一次性的 return-intent 消费流程负责决定该填什么,不能一概不填 default**:`C.ConversationInputImpl [7079170,7081100)` 读取 `expertSummonReturn`,校验 snapshot.sourceSessionId(不匹配则消费 token/清导航 state、停止),按 `conversationId:expertId:snapshotToken` 防重复。用户文本 snapshot 原 blocks 恢复;empty/expert-default/无 snapshot 时有新 default 则保留 snapshot 非文本 blocks 并 applyPrefill 新 default;无 default 但有 snapshot 则恢复 snapshot blocks。最后消费 snapshot token、清 return state。绑定与输入恢复是两个步骤。 旧 main-content return-state 默认填充还检查当前文本是否已经是同一 default;不能将这一 legacy 检查当成新 ConversationInput be 或上述 return effect 的唯一规则。 ## 同专家重复选择、并发与回滚 `C.useExpertSummon [3951518,3954856)` 的去重 key 是 `conversationId 或 account scope : id : marketExpertId : updatedAt`: - 相同 key 正在召唤:await 进行中的同一个 promise,不重复发起。 - `silent:true` 且 lastSuccessfulKey 相同:跳过;silent 会加 suppressDefaultInitPrompt。 - **普通用户再次选择同 ID,即使上一轮成功,也不按 ID 永久跳过**;继续 summon。updatedAt、market ID、scope 改变同样影响 key。 - `silent && applicationId && !conversationId` 有特殊仅更新当前选择、不做底层召唤的分支。 - cloud transport 直接 await onChange 并写 cloud recents;local transport 乐观更新选择,再 summon、归一化数据、写 recents、await owner.onChange,最后提交。 每次选择/清除递增 generation;结果应用、loading 清理与 local failure 回滚先检查是否仍是最新 generation。clear 先使旧请求过期、清 preparing/last-success key,await owner.onChange(null),仅最新操作清 provider selection。没有看到此 hook 主动 AbortController 中断已在执行的 summon;它主要抑制旧结果应用,不等于取消下载/后台请求。 local 正常召唤 500ms 后仍未完才出准备 toast;成功后 preparing 状态延迟 400ms 清除,错误清 preparing、恢复调用前 selectedExpert 并 toast。相同 key 的并发请求复用、不同 key 的并发请求以最新 generation 为准;不是把整个产品所有状态做成原子事务。 边界必须如实保留:AddMenu catch 和 Chip clear catch 直接恢复捕获的旧 draft expert,没有在该组件层检查 generation;provider 有 generation 保护不证明任意 owner 切换/快速连点都绝对安全。召唤中 recents 写入也不属于选择回滚的事务。`useHomeExpertSelection` 的失败 snapshot 只覆盖 appExpert/storeExpert/storeMode,没有完整 blocks 快照。 首页另有独立 generation 防旧依赖结果覆盖新选择;依赖失败时仅当前操作恢复选择和模式,并显示警告。scope 切换会递增 generation、dismiss gate;team 选择在 cloud 首页会改 local task target 并保留 incoming expert。具体依赖授权由 gate 处理,不以“select 完成”冒充授权完成。 ## provider hydrate、持久化与发送前检查 新任务专家持久状态 `C [613922,619317)`:按 account scope(user/enterprise/account type)隔离,并且 local/cloud 分槽。内存 cache+串行 storage write;无效存储项删除。加载请求带 load generation 和 mutation generation,晚到的旧读不能覆盖期间新选择。同 ID hydrate 合并元数据但保留 suppressDefaultInitPrompt/suppressAutoSummon,**不增加 selection revision**;普通 selection 改变增加 revision。clear-if-revision 同时核对 accountScope/revision,避免发送结束误清之后的新选择。 `H.useRuntimeScopedExpertSelection [59422,60323)`:general scope 用上述持久状态;非 general scope 用按 selectionScopeKey 的内存 Map;切 scope 保存/恢复各自选择。`isSameExpert` 比较 id 和 localPath,不能据此宣称元数据、default 文本或活动 backend 都完全相同。 目录 hydrate `C.useExpertDirectory [869768,874949)` 同时处理 selected、recent、本地自定义与别名;selected official get 和 local custom get 通过 Promise.allSettled,official 成功优先、local fallback(`resolveSelectedExpert [863201,863725)`)。异步目录 cleanup 标志防旧 effect 应用;按 id/marketExpertId 留住当前选中,不因部分列表加载暂时丢失。这个过程补 display/meta,与发起新召唤分开。 实际 runtime 另由 `P` 维护 `selectedExpertBySession` + `__pending_expert__`: - `setSelectedExpertForSessionInternal [612975,614938)` 保留同 ID 激活元数据,补齐可用指令;session 级 selection update 串行。不同注册插件先 disable 旧插件,再更新 craft/expert/runtimeIdentity;相同 craft/id/runtimeIdentity 可省 launchConfig 更新。这是 runtime 更新去重,**不是新菜单普通点击去重**。 - `setSelectedExpertForSessionIfEmpty [612787,612975)` 在 hydration 前后都检查 session 是否已有 selection;expectedConfig 防覆盖。clear-if-current `[615109,615331)` 使用 expected expertId。 - `hydrateSelectedExpertForRuntime [431496,433326)`:my-experts 优先本地 get;否则按 source/enterprise/locale 拉 getExpert,补显示与 runtime identity。既无 legacy prompt、无 activation meta、也不是有效 custom root 时才 activatePlugin,最终仍无可用 instructions 抛错。 - `ensureUsableExpertForSelection [430965,431496)` 的 hydration 错误在已有 name/profession 时有降级为该文本 prompt 的分支。因此仅 chip 显示成功不能证明原专家完整指令加载成功。 - 加载已有会话 `[388000,389700)` 按持久化 expertId 恢复,2 次重试+初次,间隔500ms;isCurrentLoad 防旧 load 落地。失败记录 warning 并设 ID 文本 fallback,不能称原角色能力已完整恢复。 - 发送方法在 `[400400,...] / [406143,...]` 先 await selection update,再 ensure launch config。`ensureSessionLaunchConfigBeforePrompt [434623,...]` hydrate 默认 3 次重试+初次,间隔500ms;失败时若已有 usable binding 保留,否则清无效 expertId/runtimeIdentity。该降级行为不同于直接在 UI 假显示绑定成功。 清除现有会话 `P.clearSelectedExpertForSessionInternal [615331,615919)` 删除选择、通知监听、disable session 插件、清 launch expert/locale/runtimeIdentity,随后 best-effort deactivate。模式仍写 craft;清 expert 不等于退回其他模式,也不清会话历史。 ## team、确认与取消 市场 `useExpertSummonFlow` 的 team/金融风险确认取消仅关闭 dialog、清 pending,不调用底层召唤。team 确认可按用户 scope 持久 suppress;金融确认是当天 suppress 或旧 forever 标志。原版确认逻辑包括 automation 的独立分支,不能默认所有入口均创建聊天。 `C.useTeamExpertSwitchGuard [6065611,6066278)` 检查该会话已使用 team 或有 team 历史;换到不同的非空 expertId 才阻止。clear(null) 不匹配这个换专家条件。召唤更多在已用 team 时同样要求确认。 确认后的 `confirmTeamExpertSwitchAction [6064556,6064853)` 清 preparedContext;有新 expert 则准备新首页专家 context/default,然后 goHome;召唤更多再导航 experts。它不在原 team 会话里直接替换专家。确认弹窗 `closeOnEscape:false`;取消不执行该动作。此处阻止“同一已使用团队会话任意换另一个专家”的行为,是与普通 agent 会话的重要差别。 ## 对 YCode 对齐的具体约束与未验证项 1. 市场默认准备新草稿、picker 的召唤更多显式带返回 owner、返回只绑定该 ID,与源码匹配;不做按 expertId 搜索历史复用。直接侧栏打开市场是否带 return target 取决于入口导航 state,不能一概处理。 2. 如产品 store 将“同 ID”全部短路,尚缺原版普通重召唤和 updatedAt/runtimeIdentity 变化的恢复路径;至少区分 user/silent/inflight,以及内容版本和已激活状态。 3. 提示词使用 owner/applyPrefill 状态处理;首页 clear 与会话 clear 源码不同。用户输入、附件、已自动填默认、无 default 的情况都要分别验收。 4. 选择、依赖准备、backend runtime 激活、消息发送是不同状态。保留 source/runtimeIdentity 和逐会话绑定,旧请求晚到不能串 owner;不宣称原版所有组件回滚都已完全竞态安全。 5. 本报告只静态核对已保存原版代码。未新增原版运行时证明:current UI 路由每个 legacy/new 分支、极速切换/账号切换、失败时默认文本最终画面、Office/云同步、team 确认后的全部 state 均仍需独立 REA 场景验收。未验证模型实际遵循专家角色。 建议以以下状态矩阵做自身产品回归,再用正式 REA 取得原版同场景行为;目前此矩阵仅验收建议,未声称全部运行: | 操作 | 需核对的状态 | |---|---| | 侧栏直接进市场后召唤 | 首页草稿;0 次发送;不任意导航历史同专家会话 | | 会话 picker 召唤更多再返回 | 指定原 conversationId;user blocks 保留;expert-default 换新 default;附件保留 | | user 点击当前专家 / silent hydrate | user 可重召唤;silent 相同成功 key 去重;updatedAt 改变需要准备新内容 | | 清 chip | 首页/会话两种 owner;空文本/自动default/用户编辑/附件组合;clear 后晚到召唤不复选 | | A慢→B快 / A慢→clear / A失败→B | 最新选择成立;不恢复过时owner;loading/toast/依赖gate和错误事实一致 | | 首页 local/cloud 或账户切换 | selection 槽隔离;旧load不覆盖新选择;发送后revision保护 | | 重启恢复会话 | source/runtimeIdentity/Agent/Skill绑定可恢复;不可只显示chip而失去原指令 | | 已用 team 换专家/召唤更多 | 确认后新首页;取消不变;同专家和clear边界单独测 | 修改文件仅本报告。验证为:339+329 模块 SHA 全部匹配、相关具名函数 Babel AST 范围核对、调用点/参数/分支逐项阅读;未运行生产测试或修改原版/YCode 状态。