Files
ycode/docs/专家使用逻辑-REA核对-20261016.md
T

146 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 专家使用逻辑: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 状态。