Parcourir la source

添加源码阅读路线与函数追踪流程图

leon il y a 2 semaines
Parent
commit
9bfa4b5659

+ 2 - 0
README.md

@@ -2,6 +2,8 @@
 
 智服中台面向多商家电商客服与售后场景,将消费者咨询、订单查询、知识检索、换货申请、人工审批和 OMS 履约连接为完整业务流程。项目包含消费者 H5、商家后台、平台后台、Agent 服务、独立 OMS 服务,以及模型用量、费用和运行监控能力。
 
+源码阅读:[八条阅读路线与文件、函数追踪流程图](docs/reading-routes/README.md)。
+
 ## 本机演示访问与账户
 
 以下为当前本机演示环境的访问地址与账号密码,地址均通过 `127.0.0.1` 在运行项目的电脑上访问。

+ 527 - 0
docs/architecture/conversation-message-flow.md

@@ -0,0 +1,527 @@
+# 对话消息、Agent worker 与前端展示流程
+
+代码依据:2026-09-30 当前工作区文件。主流程使用消费者 H5 的 `chat` 页面,人工客服流程见第 8 节。图中的函数名、事件名、表名与状态名均取自当前代码。
+
+## 1. 一次普通对话的调用顺序
+
+```mermaid
+sequenceDiagram
+    autonumber
+    actor User as 客户
+    participant UI as H5 chat 页面
+    participant API as FastAPI
+    participant DB as MySQL
+    participant Worker as Agent worker
+    participant Redis as Redis state
+    participant Model as 模型服务
+    UI->>API: 查询或创建会话,读取历史和当前运行
+    API->>DB: 查询 conversations / conversation_events / agent_runs
+    API-->>UI: 会话状态、消息历史、当前运行
+    UI->>API: GET /conversations/{id}/events,Last-Event-ID
+    API-->>UI: SSE connected 注释,保持连接
+    User->>UI: 输入消息并点击发送
+    UI->>API: POST /conversations/{id}/messages<br/>text + expected_control_version + Idempotency-Key
+    API->>DB: 开始事务,校验身份并锁定会话
+    API->>DB: 检查幂等键、控制版本与活动运行
+    API->>DB: INSERT agent_runs,status=QUEUED
+    API->>DB: UPDATE conversations.active_run_id 和 sequence
+    API->>DB: INSERT conversation_events,type=message.user
+    API->>DB: 提交事务
+    API-->>UI: HTTP 201,EventView,payload.run_id
+    Note over UI: 返回序号较新时保存运行标识,清空输入<br/>消息列表由服务端事件更新
+    Worker->>DB: execute_one → reconcile → claim<br/>查询任务,锁定租户、会话与运行
+    Worker->>Redis: acquire 申请模型并发额度
+    Redis-->>Worker: 额度准入结果
+    Worker->>DB: RUNNING,lease_token + 1<br/>lease_until=当前时间+125秒,提交事务
+    Worker->>DB: load → model<br/>保存 ModelAttempt 与 agent.model_started
+    Worker->>DB: BudgetCallback 预占 token<br/>已配置 MoneyPolicy 时预占金额
+    Worker->>Model: ChatOpenAI.bind_tools(...).ainvoke(...)
+    Model-->>Worker: 完整 AIMessage 与 usage
+    Worker->>DB: 预算结算;独立保存 ModelAttempt 用量
+    opt 模型要求调用工具
+        Worker->>DB: tools / followup 执行业务并保存 ToolMessage
+        Worker->>DB: 保存 agent.tools_completed 与 next_node
+        Worker->>Model: 使用工具结果继续模型调用
+        Model-->>Worker: 完整最终答复与 usage
+    end
+    Worker->>DB: 校验租约、权限、控制权、引用与记忆版本
+    Worker->>DB: 同一事务保存 COMPLETED 和 message.assistant<br/>清除 active_run_id,提交事务
+    Worker->>Redis: finally 释放并发额度
+    loop SSE 每轮查询后等待0.5秒
+        API->>DB: read_events 查询 sequence 大于当前游标的事件
+        DB-->>API: 已提交事件,每批最多50条
+        API-->>UI: SSE id / event / data
+    end
+    UI->>UI: receiveEvent 按序号去重、核验控制版本、更新状态
+    UI->>UI: shared.messages → messageItems → Vue 消息气泡
+    UI-->>User: 显示完整答复和原始消息时间
+```
+
+图中接口路径省略统一前缀 `/openapi/v1`。SSE 从建立连接后便持续读取,客户消息和运行事件可以在执行过程中先后到达。数据库提交后,HTTP 响应、worker 领取和 SSE 读取由各自进程调度,三者的先后顺序没有固定保证。
+
+普通成功运行通过 `message.assistant` 提交最终答复,同时数据库状态变为 `COMPLETED`。前端据此清除当前活动运行并展示正文。
+
+代码入口:[customer.js:254](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:254)、[conversations/api.py:185](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:185)、[worker.py:137](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:137)、[runtime.py:284](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:284)、[stream.py:131](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/stream.py:131)。
+
+## 2. 前端如何建立会话并发送消息
+
+```mermaid
+flowchart TD
+    A[进入消费者 chat 页面] --> B[loadPage 建立本次页面 generation]
+    B --> C[ensureConversation]
+    C --> D{已有 conversationId?}
+    D -->|已有| E[GET 当前会话]
+    D -->|没有| F[GET 当前店铺本人会话列表]
+    F --> G{列表存在会话?}
+    G -->|存在| H[使用返回列表第一项]
+    G -->|没有| I[POST 创建会话,保存聊天与控制状态]
+    E --> J[保存 conversation 和 control_owner]
+    H --> J
+    I --> J
+    J --> K[refreshMessages 按 after 读取历史<br/>每页100条,按 sequence 去重]
+    K --> L[重新读取会话<br/>存在 active_run_id 时读取运行]
+    L --> M[读取人工协助状态<br/>connectMessages 建立 SSE]
+    M --> N[connected=true]
+    N --> O[send 检查登录、输入、连接和 busy]
+    O --> P{存在活动运行?}
+    P -->|没有| Q[ensureConversation 获取当前控制版本]
+    P -->|等待 clarification| Q
+    P -->|其他活动状态| S[发送入口停止本次操作]
+    Q --> T[组合 aiContext 与问题文本<br/>检查总长度不超过4000字符]
+    T --> Clarify{正在等待 clarification?}
+    Clarify -->|是的| R[POST /agent/runs/id/inputs<br/>等待标识、运行版本、输入框回答]
+    Clarify -->|其他情况| U[生成或复用本次消息 Idempotency-Key]
+    U --> V[api.send → api.request → fetch POST]
+    V --> W[HTTP 201 返回 EventView]
+    R --> X[HTTP 202 返回输入收据和原运行状态]
+    W --> Y[存在 run_id 且返回序号较新时更新 active_run_id<br/>移除本次页面幂等键]
+    X --> InputResult[interrupt_id 仍匹配时更新 activeRun<br/>移除本次输入幂等键]
+    InputResult --> Z[清空输入,showLatestMessages<br/>SSE 事件到达后展示消息]
+    Y --> Z
+```
+
+`ensureConversation()` 使用已保存的会话 ID;没有 ID 时使用列表第一项,列表为空才创建。后端会话列表按照 UUID 升序返回。`consult(context)` 从商品或订单入口保存 `state.aiContext`,然后进入 `#/customer/chat`。
+
+| 请求位置 | 字段 | 用途与约束 |
+| --- | --- | --- |
+| Header | `Authorization: Bearer ...` | 消费者当前会话令牌 |
+| Header | `Idempotency-Key` | 1~64 字符;同一次提交结果未知时复用原键 |
+| JSON | `text` | 后端限制 1~4000 字符;页面会加入商品或订单咨询上下文 |
+| JSON | `expected_control_version` | 最近读取的会话控制版本,至少为 1 |
+
+页面输入控件最多 2000 字符,加入咨询对象后再次检查 4000 字符上限。`send()` 成功后更新运行标识并清空输入,服务端事件负责向 `shared.messages` 添加消息。`result.sequence > state.after` 检查处理 SSE 提前送达的情况。
+
+页面的 `key(action, body)` 将幂等键保存在 `state.commandKeys`。普通 HTTP 请求默认超时为 20 秒;网络结果未知时保留页面键,客户重试相同提交时继续使用。`createApi.request()` 同时通过 `pendingKeys` 管理请求键,页面显式传入的键优先生效。
+
+代码:[会话与历史读取](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:136)、[页面加载](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:373)、[消费者接口映射](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/services/customer.js:15)、[HTTP 客户端](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/api.js:44)。
+
+## 3. 后端如何将消息与任务一起入库
+
+```mermaid
+flowchart TD
+    A[POST messages<br/>FastAPI 校验 Message 与 Idempotency-Key] --> B[sessions.begin 开始事务]
+    B --> C[authenticate 校验令牌、租户、账户<br/>access 锁定并检查会话访问权限]
+    C --> D[accept_message 计算请求 SHA-256]
+    D --> E[prior 查询同会话、同操作者、同键的事件]
+    E --> F{已有事件?}
+    F -->|同摘要| G[返回原 EventView]
+    F -->|摘要不同| X[抛出 BusinessError<br/>事务撤销本次未提交修改]
+    F -->|没有| H[检查 expected_control_version<br/>客服还需已接管且为 assigned_staff_id]
+    H --> I{CUSTOMER 且 control_owner=AGENT?}
+    I -->|符合| J[确认 active_run_id 为空<br/>读取最近12条历史消息<br/>连同本次文本限制24000字符]
+    J --> K[queue_run 读取发布版本和模型配置<br/>检查账户活动任务少于3个]
+    K --> L[session.add AgentRun<br/>session.flush 执行 INSERT,status=QUEUED]
+    L --> M[设置 run.conversation_id、control_version<br/>设置 conversation.active_run_id]
+    I -->|客户由人工接待或客服回复| N[run_id=null]
+    M --> O[emit:conversation.sequence + 1<br/>session.add ConversationEvent]
+    N --> O
+    O --> P[客户消息按授权情况添加 MemoryJob<br/>已分配客服时添加通知]
+    P --> Q[构造 EventView]
+    Q --> R[正常退出 sessions.begin<br/>flush 剩余变更并 COMMIT]
+    G --> R
+    R --> S[HTTP 201 返回持久事件]
+    H -. 校验失败 .-> X
+    J -. 存在活动运行 .-> X
+    K -. 配置或数量不符合 .-> X
+```
+
+`message()` 将这些操作放在同一个 `async with sessions.begin()` 中。`session.add()` 把对象交给当前 SQLAlchemy Session;`flush()` 将 SQL 发送给数据库,事务在正常退出上下文时提交。任何一步抛出异常,本次事务中尚未提交的修改一起撤销。
+
+`queue_run()` 中的 `flush()` 发生在调用方设置 `conversation_id` 和 `control_version` 之前。运行插入、后续关联字段、会话活动标识和用户事件最终一起提交。worker 和 SSE 使用各自 Session 查询已提交数据;数据库连接使用 `READ COMMITTED`。
+
+| 数据表 | 本次写入或更新 | 后续读取方 |
+| --- | --- | --- |
+| `conversations` | 保存客户、店铺和控制状态;`active_run_id` 指向运行;`sequence` 分配下一个事件序号 | 页面、worker、SSE |
+| `conversation_events` | `message.user` 或 `message.staff`;`payload={text, run_id}`;请求键、摘要、控制版本、创建时间 | 历史接口、SSE、下一次对话上下文 |
+| `agent_runs` | 客户处于 AGENT 模式时创建:`QUEUED/model`、上下文、调用身份、模型参数、发布快照、期限 | Agent worker |
+| `memory_jobs` | 已授权记忆的客户消息创建任务,关联会话与事件序号 | memory worker |
+| `notifications` | 已分配客服时,由 `notify()` 为仍有权限的员工保存客户消息通知 | 商家通知接口 |
+
+`conversation_events` 保存页面可读取的会话事件;`agent_runs.messages` 保存 LangChain 格式的模型输入、助手工具调用和工具结果。
+
+- `authenticate()` 校验 AccessSession、租户与账户状态;写入请求锁定操作者记录。`access()` 锁定会话,校验客户本人或客服的店铺权限。
+- `prior()` 位于控制版本检查之前。同键同内容返回原事件;同键不同内容返回 `409 IDEMPOTENCY_CONFLICT`。
+- 会话行锁保护 `active_run_id` 与 `sequence`。同一会话已有活动运行时返回 `409 RUN_IN_PROGRESS`。
+- 最近最多 12 条历史消息恢复正序后追加当前消息,正文合计最多 24000 字符。客户历史使用 `HumanMessage`,助手及客服历史使用 `AIMessage`。
+- `queue_run()` 在接受新运行时读取店铺当前发布,已有会话的新运行也使用此时的配置。模型名、地址、凭据环境变量名、超时、输出限制、提示词及 `graph_version` 保存在运行快照中。进行中的运行、补充输入、审批等待及进程恢复继续使用原运行快照。当前代码登记的最新图版本为 `after-sales-v10`。
+- 新运行期限为 10 分钟。账户中未过期的 `QUEUED`、`RUNNING`、`WAITING_USER`、`WAITING_APPROVAL` 合计达到 3 个时,返回 `429 AGENT_QUEUE_FULL`。
+
+代码:[message 与 accept_message](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:185)、[queue_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/service.py:20)、[emit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/service.py:31)、[会话数据模型](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/models.py:10)、[运行数据模型](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/models.py:10)、[数据库连接](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/core/database.py:20)。
+
+## 4. worker 如何启动、领取和循环处理任务
+
+容器 `agent` 服务使用 `python -m zhifu.operations.worker agent`。开发环境也支持 `python -m zhifu.modules.agent_runtime.worker`;该入口提供 `--once`,执行一次领取和处理后退出。
+
+```mermaid
+flowchart TD
+    A[operations.worker.run agent] --> B[登记停止信号<br/>进入 run_monitored]
+    B --> C[heartbeat agent<br/>每5秒写 Redis,记录20秒有效]
+    C --> D[启动 agent_runtime.worker.main 任务<br/>读取 Settings 与 AgentSettings]
+    D --> E[创建数据库 engine 和 sessions<br/>连接 Redis state<br/>建立知识检索 HTTP 与 Milvus 客户端]
+    E --> F[execute_one]
+    F --> G[reconcile 核对等待中的业务运行<br/>每轮最多100个]
+    G --> H[生成 capacity_id<br/>10秒期限内执行 claim]
+    H --> I[开始领取事务<br/>锁定 CapacityPolicy 并读取租户预留额度]
+    I --> J[筛选存在任务且未满并发的租户<br/>优先未用完预留额度的租户<br/>随后按 selected_at 与 Tenant.id 排序]
+    J --> K{取得租户且重新检查额度通过?}
+    K -->|没有通过| N[本次没有领取任务]
+    K -->|通过| L[更新 tenant_dispatch.selected_at<br/>按 created_at、id 选择任务]
+    L --> M[锁定 Conversation<br/>重新锁定并检查 AgentRun]
+    M --> O{任务已超过 deadline_at?}
+    O -->|已经超过| P[FAILED,RUN_DEADLINE<br/>finish_run 保存失败事件并释放会话]
+    P --> N
+    O -->|仍然有效| Q[Redis acquire 原子检查<br/>全局、交互、后台及租户预留额度]
+    Q --> R{取得并发额度?}
+    R -->|没有取得| N
+    R -->|取得| S[旧 STARTED 尝试改为 UNKNOWN<br/>RUNNING,lease_token + 1<br/>lease_until=当前时间+125秒]
+    S --> T[提交领取事务<br/>返回 run_id 与 lease_token]
+    T --> U[核对旧模型尝试的预算<br/>build_graph,执行期限120秒]
+    U --> V[finally 释放 Redis 并发额度]
+    N --> V
+    V --> W{本次是否处理运行?}
+    W -->|已经处理| F
+    W -->|没有处理| X[等待0.5秒]
+    X --> F
+    B -. 收到停止信号 .-> Y[取消业务任务并等待清理<br/>关闭客户端,删除进程心跳]
+```
+
+`claim()` 接受三类候选:
+
+1. `QUEUED`:尚待执行或重新排队的运行。
+2. `RUNNING` 且 `lease_until < now()`:执行租约已经过期的运行。
+3. `WAITING_USER`、`WAITING_APPROVAL` 且 `deadline_at <= now()`:需要结束的过期等待运行。
+
+领取使用 MySQL 行锁协调多个 worker,在取得租户锁后重新统计仍有有效租约的运行数量。默认租户并发为 2,有 `TenantQuota.interactive_limit` 时使用该租户配置。全局模型并发默认 4,交互预留默认 2,后台预留默认 1;具体部署值由配置确定。
+
+Redis `acquire()` 使用 Lua 一次检查并更新 `agent:active:global`、`:interactive`、`:background` 与 `:owners`。额度记录有效期为 150 秒;`execute_one()` 的 `finally` 主动释放本次 `capacity_id`。Redis 更新和 MySQL 提交各自执行,释放逻辑和有效期限负责退出后的额度清理。
+
+每个 `main()` 循环等待当前 `execute_one()` 完成后才领取下一项。因此单个进程一次执行一个运行,多个进程通过共同的数据库记录和 Redis 额度协调并发。
+
+`heartbeat()` 的 Redis 记录表示进程存活。`AgentRun.lease_until` 是单次领取的数据库执行期限,当前实现领取时设置一次。每次领取增加 `lease_token`,节点提交必须携带相同 token;旧进程的迟到结果无法通过租约检查。
+
+图中的循环表示正常处理路径。一般执行异常会在记录运行失败后继续抛出,结束当前进程;预算准入错误记录失败后允许循环继续。容器配置使用 `restart: unless-stopped`。
+
+代码:[容器配置](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/compose.runtime.yaml:50)、[统一进程入口](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/operations/worker.py:49)、[进程心跳](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/core/telemetry.py:31)、[claim](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:28)、[execute_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:137)、[main](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:210)、[并发额度](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/capacity.py:75)。
+
+## 5. worker 如何执行模型、工具和最终提交
+
+```mermaid
+flowchart TD
+    A[graph.ainvoke<br/>run_id、token、recursion_limit=24] --> B[load<br/>locked_run 后读取数据库 next_node]
+    B --> C{next_node}
+    C -->|model| D[事务一:locked_run<br/>核验引用与记忆快照<br/>检查模型次数、step 与上下文长度]
+    D --> E[保存 STARTED ModelAttempt<br/>model_calls + 1<br/>emit agent.model_started,提交事务]
+    E --> F[事务外读取已授权记忆<br/>构造 ChatOpenAI 与工具定义]
+    F --> G[BudgetCallback.on_chat_model_start<br/>预占 token;配置 MoneyPolicy 时预占金额]
+    G --> H[模型网络调用<br/>max_retries=0,返回完整 AIMessage]
+    H --> I[预算回调处理 usage<br/>独立事务保存 ModelAttempt RECEIVED 与 usage]
+    I --> J[事务二:重新 locked_run<br/>检查引用、记忆版本和工具参数]
+    J --> K[追加 AIMessage,step + 1]
+    K --> L{存在 tool_calls?}
+    L -->|没有| M[COMPLETED,next_node=done<br/>version + 1,清除 lease_until]
+    M --> N[finish_run<br/>emit message.assistant<br/>清除 conversation.active_run_id]
+    N --> O[提交事务,图执行结束]
+    L -->|存在| P[保存 next_node=tools 并提交]
+    P --> Q[工具节点:检查工具名称和参数<br/>每轮最多4个工具]
+    C -->|tools| Q
+    Q --> R{工具种类}
+    R -->|search_knowledge| S[事务外查询知识服务<br/>事务内再次校验结果权限和版本]
+    R -->|普通业务工具| T[invoke_tool<br/>执行业务检查与读写]
+    R -->|request_clarification| U[pause_for_input 保存等待事件<br/>随后保存 ToolMessage 和 step]
+    R -->|create_exchange_proposal| V[创建方案;保存 ToolMessage<br/>step 加一]
+    R -->|create_followup_request| W[先提交 next_node=followup]
+    C -->|followup| W2[followup 节点执行业务登记]
+    W --> W2
+    W2 --> X[同一事务保存业务收据、通知、ToolMessage<br/>next_node=model,emit agent.tools_completed]
+    S --> Y[保存 ToolMessage,step + 1<br/>next_node=model,emit agent.tools_completed]
+    T --> Y
+    X --> D
+    Y --> D
+    U --> Z[追加助手问题;emit agent.tools_completed<br/>WAITING_USER 或 WAITING_APPROVAL,next_node=wait]
+    V --> BusinessWait{发布版本启用 business_wait?}
+    BusinessWait -->|已经启用| PauseBusiness[pause_for_business 保存等待事件]
+    PauseBusiness --> Z
+    BusinessWait -->|没有启用| ContinueModel[next_node=model<br/>emit agent.tools_completed]
+    ContinueModel --> D
+    Z --> Z2[提交事务,图执行结束<br/>保留活动运行等待后续输入或业务事件]
+    C -->|done 或 wait| Z3[图执行结束]
+```
+
+`build_graph()` 先进入 `load`,按照数据库中的 `run.next_node` 继续。工具执行结果、`step`、`next_node` 和模型上下文保存在 MySQL,重新领取后继续读取这些记录。
+
+`locked_run()` 在关键节点读取与结果提交时检查:运行身份有效、租户和账户相符、店铺有效、权限有效、会话由 AGENT 控制、控制版本一致、活动运行相同、状态为 `RUNNING`、token 相同、租约和任务期限均未到期。
+
+模型调用的提交位置:
+
+1. 调用前事务写入 `ModelAttempt(status=STARTED)` 和 `agent.model_started`,增加持久化的 `model_calls`。
+2. 模型请求在业务事务外执行;`BudgetCallback` 在发送前预占 token,租户配置 `MoneyPolicy` 时同时预占金额,返回后处理真实 `usage`。
+3. 独立事务保存 `ModelAttempt(status=RECEIVED, usage=...)`,保留已经发生的用量。
+4. 重新获得运行锁并通过校验后,保存模型消息和后续节点。最终答复、`COMPLETED` 和清除 `active_run_id` 在同一事务提交。
+
+普通工具由 `invoke_tool()` 调用业务服务。工具命令标识为 `agent:{UUID(bytes=run.id).hex}:{run.step}:{index}`,相同持久化步骤重新执行时保持一致,供业务服务核验幂等收据。`request_clarification` 必须单独调用;`create_followup_request` 也必须单独调用,并使用 `followup` 节点。方案等待取决于 `GRAPH_RELEASES[run.graph_version]["business_wait"]`;关闭此能力的历史发布版本按普通工具路径继续模型节点。
+
+| 限制 | 当前代码中的值 |
+| --- | --- |
+| 单次领取的图执行期限 | 120 秒 |
+| 数据库执行租约 | 125 秒 |
+| Redis 并发额度有效期 | 150 秒 |
+| 单次模型超时 | 发布快照的 `model_timeout`;配置默认 30 秒,上限 30 秒,并受剩余任务期限约束 |
+| 模型 SDK 自动重试 | `max_retries=0` |
+| 每个运行模型调用上限 | 6 次,通过 `model_calls < 6` 检查 |
+| 节点步骤限制 | 通过 `step < 20` 检查 |
+| 单轮工具数量 | 最多 4 个 |
+| 运行上下文长度 | `json.dumps(run.messages, ensure_ascii=False)` 最多 32000 字符 |
+| 默认最大输出 | 1024 token;配置允许 128~4096,运行使用发布快照 |
+
+代码:[locked_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:47)、[工具节点](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:95)、[build_graph](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:244)、[模型节点](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:284)、[预算回调](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget_callback.py:23)、[finish_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/service.py:96)。
+
+## 6. SSE 如何读取数据库并让前端显示消息
+
+```mermaid
+flowchart TD
+    A[connectMessages<br/>GET events,携带 Authorization 和 Last-Event-ID] --> B[API 认证、会话权限和游标检查]
+    B --> C[Redis Lua 申请全局与账户 SSE 额度]
+    C --> D[返回 text/event-stream<br/>浏览器验证响应后调用 onOpen]
+    D --> E[generate 立即发送 connected 注释]
+    E --> F[read_events:独立短事务<br/>重新验证身份、会话权限和游标]
+    F --> G[查询 sequence 大于 cursor 的事件<br/>升序返回,每批最多50条]
+    G --> H[逐项推进服务端 cursor<br/>过滤当前控制版本不适用的 Agent 事件]
+    H --> I[发送 id、event 和 data]
+    I --> J[fetch 正文流 → TextDecoderStream<br/>EventSourceParserStream → JSON.parse]
+    J --> K[核验 sequence、id、type、control_version]
+    K --> L[receiveEvent:sequence 去重<br/>更新 state.after]
+    L --> M[过滤旧控制版本<br/>更新 owner、active_run_id 和等待状态]
+    M --> N[shared.messages.push]
+    N --> O[messageItems 生成正文或服务提示]
+    O --> P[Vue 以 sequence 为 key 更新页面<br/>ExpandableText 显示正文]
+    P --> Q[查看最新消息时自动滚动<br/>阅读历史时增加未读数量]
+    I --> R{连接达到约50秒?}
+    R -->|尚未达到| S[等待0.5秒]
+    S --> F
+    R -->|已经达到| T[结束响应,释放 SSE 额度]
+    T --> U[浏览器正常读完后<br/>携带最新 state.after 再次订阅]
+    U --> A
+```
+
+SSE 从 `conversation_events` 查询消息。worker 保存结果后,正在运行的 `generate()` 在后续查询中读取已提交事件,再通过 HTTP 响应正文发送给浏览器。Redis 在此流程中管理连接额度;会话事件的内容和序号保存在 MySQL。
+
+请求路径为 `GET /openapi/v1/conversations/{conversation_id}/events`。初次订阅默认 `Last-Event-ID=0`;当前页面已经读取历史时使用其 `state.after`。每轮查询在独立事务中验证 token、账户、租户与会话权限,查询完成后释放数据库连接。API 使用 `asyncio.Semaphore(settings.pool_size)` 限制同时进行的 SSE 数据库读取。
+
+持久事件结构如下,标识和正文仅说明字段形式。
+
+```text
+id: 7
+event: message.assistant
+data: {"sequence":7,"type":"message.assistant","control_version":1,"created_at":"2026-09-30T00:00:00+00:00","payload":{"text":"已查询到订单状态。","run_id":"{run_id}"}}
+
+```
+
+`id` 等于 `sequence`,`event` 等于 `type`,`data` 为完整 `EventView`。服务端立即发送 `connected` 注释,每 15 秒发送心跳,约 50 秒结束连接,发送超时为 5 秒。前端单次连接总期限为 65 秒,使用 `EventSourceParserStream` 处理 SSE 分段与心跳,解析缓冲上限为 1 MiB。
+
+默认全局最多 200 条 SSE 连接,每账户最多 3 条;部署可修改。Redis 连接额度有效期为 60 秒,连接结束通过 `release()` 清除。服务端每轮还检查续传游标:小于 `stream_cursor` 返回 `410 EVENT_CURSOR_EXPIRED`,大于会话最新 `sequence` 返回 `422 INVALID_EVENT_CURSOR`。
+
+前端 `receiveEvent()` 的执行顺序为:检查 `sequence <= state.after` 并跳过重复事件;新事件立即更新 `state.after`;过滤旧控制版本;修改会话与运行状态;最后向 `shared.messages` 添加事件。`messageItems` 重新计算后,Vue 更新页面。
+
+| 持久事件 | 前端处理与显示 |
+| --- | --- |
+| `message.user` | 从 `payload.text` 或 `payload.answer` 生成客户气泡,显示“已发送”;携带 `run_id` 时登记活动运行。 |
+| `agent.model_started` | 保存进度事件;页面根据活动运行显示查询提示。该事件没有专门把本地 `activeRun.status` 改为 `RUNNING` 的分支。 |
+| `agent.tools_completed` | 保存工具进度事件。普通消息区域根据消息类型生成正文。 |
+| `message.assistant` | 显示完整 AI 答复;携带 `waiting` 时保存等待信息,普通完成答复清除适用的活动运行。 |
+| `run.waiting` | 保存 `interrupt_id`、运行版本、问题及等待类型;显示补充信息或业务操作入口。 |
+| `run.resumed` | 更新活动运行,清除等待信息;存在 `proposal_id` 时重新读取业务卡片。 |
+| `message.staff` | 显示人工客服正文。 |
+| `control.changed` | 更新控制权、控制版本、当前运行及人工协助状态,显示服务方变化提示。 |
+| `handoff.requested` | 保存已申请人工协助状态,并显示提示。 |
+| `run.failed` / `run.cancelled` | 清除适用的活动运行,显示失败或停止提示。 |
+
+`messageItems` 将客户、助手和客服正文交给 `ExpandableText`;时间使用服务端事件的原始 `created_at`。浏览器距离消息列表底部小于 48 px 时,新消息自动滚动到最新位置;阅读历史时保留当前位置并增加未读数量。用户发送消息或点击新消息按钮时显示最新消息。
+
+| 连接情况 | 处理规则 |
+| --- | --- |
+| 服务端正常结束响应 | `connectMessages()` 自动继续循环,以最后事件序号订阅。 |
+| `410 EVENT_CURSOR_EXPIRED` 或流内 `stream.expired` | 本次连接循环自动恢复一次:清空本地历史和游标,重新读取消息历史、当前会话和活动运行,再订阅。历史接口仍可读取持久事件。 |
+| `401 / 403 / 404` | 停止连接并清理适用的当前会话内容;认证失效时清除对应登录会话。 |
+| `429 SSE_LIMIT` | 根据 `Retry-After` 显示等待秒数,之后允许用户重新连接。 |
+| 网络异常或连接超时 | 显示错误,用户点击“重新连接”触发页面重新加载与订阅。 |
+| 页面切换或组件卸载 | 中止旧连接;`generation / valid()` 限制旧页面的请求修改当前页面。 |
+
+服务端过滤 `agent.*` 和 `message.assistant` 的过期控制版本;前端实时处理另外过滤 `run.*` 的旧控制版本。历史读取保留原始事件及原始控制版本。
+
+代码:[SSE 查询](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/stream.py:50)、[SSE 生成器](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/stream.py:131)、[前端 SSE 读取](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/api.js:11)、[receiveEvent 与重连](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:177)、[气泡与滚动](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:343)、[页面模板](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:466)。
+
+## 7. 等待输入、业务确认、失败和任务恢复
+
+```mermaid
+stateDiagram-v2
+    [*] --> QUEUED: 消息与运行提交成功
+    QUEUED --> RUNNING: worker 领取,取得新 lease_token
+    RUNNING --> COMPLETED: 提交完整助手答复
+    RUNNING --> WAITING_USER: 保存补充问题或待确认方案
+    WAITING_USER --> QUEUED: 接受 clarification 输入
+    WAITING_USER --> WAITING_APPROVAL: 核对客户已确认方案
+    WAITING_USER --> QUEUED: 已提交业务结果允许继续生成
+    WAITING_APPROVAL --> WAITING_USER: 方案需要修订
+    WAITING_APPROVAL --> QUEUED: 核对有效业务结果
+    RUNNING --> RUNNING: 租约到期后重新领取
+    QUEUED --> FAILED: 超过执行期限
+    RUNNING --> FAILED: 执行、预算或权限检查失败
+    WAITING_USER --> FAILED: 超过等待期限或权限失效
+    WAITING_APPROVAL --> FAILED: 超过等待期限或权限失效
+    QUEUED --> CANCELLED: 用户停止或客服接管
+    RUNNING --> CANCELLED: 用户停止或客服接管
+    WAITING_USER --> CANCELLED: 用户停止或客服接管
+    WAITING_APPROVAL --> CANCELLED: 用户停止或客服接管
+    COMPLETED --> [*]
+    FAILED --> [*]
+    CANCELLED --> [*]
+```
+
+状态转换发生在对应处理函数的身份、状态、版本和幂等检查通过以后。
+
+**补充信息。** `pause_for_input()` 将运行改为 `WAITING_USER / wait`,增加 `version`,清除 `lease_until`,等待期限设置为 24 小时。`waiting` 保存 `interrupt_id`、`run_version`、问题和候选信息;同事务保存带 `waiting` 的 `message.assistant` 和 `run.waiting`。本次 graph 结束后释放并发额度,会话保留原 `active_run_id`。
+
+前端发现等待类型为 `clarification` 时,发送 `POST /openapi/v1/agent/runs/{run_id}/inputs`:
+
+```json
+{
+  "interrupt_id": "{current_interrupt_id}",
+  "expected_run_version": 2,
+  "answer": "客户补充的信息"
+}
+```
+
+`accept_input()` 检查幂等收据、等待状态、运行版本、等待标识、期限和会话控制权。成功时追加 `HumanMessage`,保存 `RunInputEvent`,清空等待信息,将原运行恢复为 `QUEUED / model`,更新本次访问 token,重新设置 10 分钟执行期限;同事务保存 `message.user` 和 `run.resumed`。worker 随后领取原 `run_id`。
+
+**方案确认与主管审批。** `pause_for_business()` 保存 `business_ticket_id`、业务版本及等待信息。草稿和需要修订的方案进入 `WAITING_USER`,已提交审批的方案进入 `WAITING_APPROVAL`。客户确认和主管决定通过业务接口保存。每轮 worker 开始时,`reconcile()` 最多核对 100 个未过期的业务等待运行,验证持久业务事件、客户确认、主管权限及方案版本,再保存输入收据和更新后的等待状态,或者重新排队并保存 `run.resumed`。
+
+**失败记录。** graph 执行异常后,`execute_one()` 在独立事务中检查运行仍为 `RUNNING` 且 `lease_token` 相同,然后保存 `FAILED`、`error_code`,增加版本,清除等待与租约,并保存 `run.failed`、清除会话活动运行。仍为 `STARTED` 的尝试按情况改为 `NOT_SENT` 或 `UNKNOWN`。预算准入拒绝允许 worker 继续循环;其他执行异常继续传播。上述处理完成后,`finally` 释放 Redis 额度。
+
+**进程恢复。** 进程异常终止后仍为 `RUNNING` 的记录,在执行租约到期后满足重新领取条件。新领取增加 `lease_token`,旧 `STARTED` 尝试改为 `UNKNOWN` 并核对预算,graph 从 MySQL 的 `next_node` 和 `messages` 继续。模型结果尚未保存时可能产生新的模型尝试,各次尝试保留独立计量记录。已经保存为 `FAILED` 的任务不在 `claim()` 的候选条件中。
+
+**停止与清理。** 用户停止调用 `/agent/runs/{id}/cancel`,保存 `CANCELLED` 和 `run.cancelled`。统一进程入口管理停止信号与心跳;`main()` 的 `finally` 关闭 Redis 和数据库 engine,`retrieval_client()` 上下文关闭知识服务客户端。容器按配置处理进程重新启动。
+
+代码:[补充信息](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/inputs.py:63)、[输入 API](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/api.py:182)、[业务等待](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/business_wait.py:30)、[业务核对](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/business_wait.py:157)、[失败处理](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:173)、[用户停止](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/api.py:155)。
+
+## 8. 人工客服接管和回复如何返回消费者页面
+
+```mermaid
+sequenceDiagram
+    autonumber
+    participant Staff as 商家后台
+    participant API as FastAPI
+    participant DB as MySQL
+    participant H5 as 消费者 H5
+    Staff->>API: POST conversations/id/takeover,控制版本和幂等键
+    API->>DB: 事务:权限和控制版本检查,锁定会话
+    API->>DB: 取消活动运行,清除 active_run_id
+    API->>DB: HUMAN,指定客服,control_version 加一
+    API->>DB: 保存 control.changed,提交
+    API-->>Staff: 返回控制事件
+    Staff->>API: GET conversation 和 messages
+    API-->>Staff: 当前控制状态与历史
+    API->>DB: 消费者 SSE 读取新事件
+    API-->>H5: control.changed
+    H5->>API: POST messages,客户在 HUMAN 模式发送
+    API->>DB: 保存 message.user,run_id 为 null,提交
+    API-->>H5: HTTP 201
+    API-->>H5: SSE message.user
+    Staff->>API: 打开会话或点击刷新消息
+    API->>DB: GET 会话状态和消息历史
+    API-->>Staff: 客户消息
+    Staff->>API: POST messages,客服回复
+    API->>DB: 检查当前接管员工,保存 message.staff,提交
+    API-->>Staff: HTTP 201
+    Staff->>API: 重新读取会话与消息
+    API-->>Staff: 显示已发送回复
+    API->>DB: 消费者 SSE 读取 message.staff
+    API-->>H5: SSE message.staff
+    H5->>H5: messageItems 生成客服气泡
+```
+
+商家后台 `control(action)` 显示确认窗口,再发送 `takeover` 或 `release`。后端要求员工具备 `conversations:manage`;交还控制权要求当前员工是已接管客服。成功后增加 `control_version` 并保存 `control.changed`。交还后客户再次发言,按 `AGENT` 模式创建新运行。
+
+客服回复要求 `HUMAN`、`assigned_staff_id` 等于当前员工,且具有 `conversations:reply`。回复使用同一个消息接口、幂等键和控制版本检查,保存 `message.staff`。模型结果提交时的 `locked_run()` 继续检查会话控制权和版本。
+
+商家会话页通过 `conversation()` 查询详情和前 100 个历史事件,“读取后续消息”调用 `moreMessages()`,使用 `after=messageSequence` 继续。打开会话、点击刷新,以及本人回复或控制操作成功后重新读取消息。商家页面每 30 秒更新的是员工通知和未读数量;聊天内容按上述页面操作读取。
+
+商家 `presentEvent()` 将客户、客服和助手正文转为消息条目,将模型开始、工具完成等信息放入“服务处理记录”。消费者 H5 持续使用第 6 节的 SSE 流程接收人工回复。
+
+代码:[后端控制权操作](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:264)、[商家读取、控制和回复接口](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/services/merchant.js:43)、[商家页面操作](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/merchant.js:125)、[商家通知定时器](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/merchant.js:117)。
+
+## 9. 接口、版本字段和事务查询位置
+
+普通消息请求如下,标识为占位符。
+
+```http
+POST /openapi/v1/conversations/{conversation_id}/messages
+Authorization: Bearer {access_token}
+Content-Type: application/json
+Idempotency-Key: {same_operation_uuid}
+
+{
+  "text": "请帮我查询订单状态",
+  "expected_control_version": 1
+}
+```
+
+成功响应为 `201 EventView`,主要字段为 `sequence`、`type`、`control_version`、`created_at` 和 `payload`。`payload.run_id` 关联异步运行;`payload.text` 是已保存正文。字段校验会去除请求字符串首尾空白,并拒绝未定义字段。
+
+| 接口或处理阶段 | 结果与处理规则 |
+| --- | --- |
+| `POST /conversations` | 客户在具备 `orders:read` 的店铺创建会话;初始控制方为 `AGENT`;幂等作用域为租户与客户。 |
+| `POST /conversations/{id}/messages` | 成功保存消息返回 `201`;`CONTROL_CONFLICT`、`RUN_IN_PROGRESS`、`IDEMPOTENCY_CONFLICT` 返回 `409`。 |
+| 消息受理的任务上限 | 账户活动任务达到上限时返回 `429 AGENT_QUEUE_FULL`;默认重试等待信息由服务端返回。 |
+| 消息受理的模型配置检查 | 模型名称或地址不可用时返回 `503 MODEL_NOT_CONFIGURED`;发布版本检查也可能拒绝受理。 |
+| `GET /conversations/{id}/messages` | `after >= 0`,`limit=1–100`,响应 `{items, sequence}`;分页使用实际最后一项的序号。 |
+| `GET /conversations/{id}/events` | SSE 通过 Bearer 认证;窗口过期返回 `410`,游标超过范围返回 `422`,连接限额返回 `429`。 |
+| `GET /agent/runs/{id}` | 读取当前运行状态、版本和等待信息,并执行运行访问权限检查。 |
+| `POST /agent/runs/{id}/inputs` | 接收等待中的补充信息,成功返回 `202` 和原运行更新状态;检查等待标识、运行版本和幂等键。 |
+| worker 模型预算检查 | token 或适用金额预算不足、缺少价格配置时保存 `run.failed`;消息受理的 `201` 已表示客户正文保存成功。 |
+
+表中接口路径统一带 `/openapi/v1` 前缀;身份、权限或参数不符合时返回相应 `401 / 403 / 404 / 422`。数据库幂等记录和版本检查由对应接口在事务中执行。
+
+| 标识或字段 | 控制的内容 |
+| --- | --- |
+| `conversation_id` | 当前客户会话及其全部持久事件。 |
+| `run_id` | 某次异步运行,包括后续补充信息和业务等待。 |
+| `sequence` | 会话事件顺序,供历史分页、SSE 续传和前端去重使用。 |
+| `control_version` | 会话控制权版本,接管和交还时增加。 |
+| `AgentRun.version` / `run_version` | 运行状态版本,补充信息与业务恢复时检查。 |
+| `lease_token` | 某次 worker 领取的执行权,重新领取时增加。 |
+| `interrupt_id` | 当前等待步骤标识,限定输入所恢复的等待。 |
+| `Idempotency-Key` | 同一操作重试标识,作用域和内容检查由具体接口确定。 |
+
+| 事务边界 | 一起提交的主要数据 |
+| --- | --- |
+| 接受客户消息 | 新运行、会话活动运行指针、事件序号、`message.user`,以及适用的记忆任务和通知。 |
+| worker 领取 | 租户调度时间、旧模型尝试状态、`RUNNING`、新执行 token 及租约。Redis 额度通过独立 Lua 调用取得。 |
+| 模型开始 | `ModelAttempt STARTED`、模型调用计数、`agent.model_started`。 |
+| 预算与用量 | 预算预留、结算及尝试用量使用各自短事务,保留已经发生的消耗。 |
+| 工具完成 | 工具业务变更、`ToolMessage`、步骤、后续节点和适用进度或等待事件。 |
+| 最终答复 | 完整 `AIMessage`、`COMPLETED`、运行版本、清除租约、`message.assistant`、清除 `active_run_id`。 |
+| 补充信息 | 输入收据、上下文、原运行重新排队、客户消息、`run.resumed`。 |
+
+查询单次对话时,可从 `conversation_id` 找到 `conversation_events.payload.run_id`,据此读取 `agent_runs` 和 `model_attempts`,再用事件 `sequence` 核对浏览器的 `state.after`。
+
+本文依据当前源码说明执行过程。文档检查覆盖 Mermaid 解析、图形渲染和代码引用;业务接口与生成模型调用未执行。

+ 173 - 0
docs/reading-routes/01-conversation.md

@@ -0,0 +1,173 @@
+# 路线 01:前后端咨询与人工协作
+
+## 学习场景
+
+客户在消费者 H5 发送问题,页面持续接收处理状态与答复;客服在商家后台接管会话并发送人工回复。沿着消息、事件序号和控制版本,追踪浏览器与后端如何共同维护同一场会话。
+
+图中后端路径相对 `src/zhifu/`;`frontend/`、`scripts/` 等路径相对仓库根目录。连续箭头表示函数直接调用;间断箭头注明 HTTP、框架回调或持久数据传递。框架节点、配置节点和数据节点单独标明。图中只展示本路线需要追踪的分支。返回[路线目录](README.md)。
+
+## 图 1:客户消息进入会话事务
+
+```mermaid
+flowchart TD
+    UI["frontend/src/pages/customer.js<br/>Vue 模板:van-form @submit"]
+    SEND["frontend/src/pages/customer.js<br/>send() / perform(action)<br/>校验发送条件并执行 action"]
+    ENSURE["frontend/src/pages/customer.js<br/>ensureConversation()<br/>读取或创建当前会话"]
+    SERVICE["frontend/src/services/customer.js<br/>send() → request()<br/>附带正文与控制版本"]
+    HTTP["frontend/src/api.js<br/>request()<br/>添加认证、幂等键并 fetch"]
+    API["modules/conversations/api.py<br/>message()<br/>开启消息事务"]
+    AUTH["modules/identity/auth.py<br/>authenticate()<br/>验证访问会话"]
+    ACCESS["modules/conversations/service.py<br/>access()<br/>检查会话权限并锁定会话"]
+    ACCEPT["modules/conversations/api.py<br/>accept_message() / prior()<br/>检查重复请求与控制版本"]
+    QUEUE["modules/agent_runtime/service.py<br/>queue_run()<br/>创建待执行运行"]
+    EMIT["modules/conversations/service.py<br/>emit()<br/>分配 sequence 并登记事件"]
+    DB[("MySQL 持久数据<br/>AgentRun、Conversation<br/>ConversationEvent")]
+    WORKER["modules/agent_runtime/worker.py<br/>claim()<br/>独立工作进程领取任务"]
+    UI -. "Vue 调用提交处理函数" .-> SEND
+    SEND -->|action 回调:查询当前状态| ENSURE
+    SEND -->|action 回调:普通消息分支| SERVICE
+    SERVICE --> HTTP
+    HTTP -. "POST /openapi/v1/conversations/{id}/messages;FastAPI 路由" .-> API
+    API --> AUTH
+    API --> ACCESS
+    API --> ACCEPT
+    ACCEPT -->|客户处于 AGENT 模式| QUEUE
+    ACCEPT --> EMIT
+    QUEUE -. "同一事务登记运行" .-> DB
+    EMIT -. "message 事务提交消息及会话变化" .-> DB
+    DB -. "读取已提交的待执行运行" .-> WORKER
+```
+
+`send()` 的普通消息分支向会话接口提交消息。`ensureConversation()` 根据本地是否已有 `conversationId`,调用会话详情,或者查询会话列表并按需创建会话。`accept_message()` 先检查幂等记录,已有记录直接返回原事件;新请求继续检查 `expected_control_version`。
+
+客户在 `AGENT` 模式下发送普通消息时,`queue_run()` 登记运行,当前会话保存 `active_run_id`。消息、运行和会话变化在 `message()` 的同一事务内提交。HTTP `201` 表示消息已被接受,后续答复通过图 2 的事件连接到达。`HUMAN` 模式客户消息与客服消息直接登记会话事件。
+
+等待补充信息时,`send()` 调用 `services/customer.js` 的 `resume()`,请求 `/openapi/v1/agent/runs/{run_id}/inputs`。补充信息、模型调用和工具执行继续阅读 [路线 02:Agent 运行过程](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/docs/reading-routes/02-agent-runtime.md)。
+
+### 图 1 源码导航
+
+1. [send()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:254)、[perform()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:90)、[ensureConversation()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:136):确认按钮条件、普通发送与补充信息分支。
+2. [createCustomerService()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/services/customer.js:2)、[request()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/api.js:45):观察接口路径、Bearer token、幂等键与错误处理。
+3. [message()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:185)、[accept_message()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:205)、[prior()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:165):定位事务、控制版本与幂等记录。
+4. [authenticate()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/auth.py:50)、[access()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/service.py:13)、[queue_run()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/service.py:20)、[emit()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/service.py:31)、[claim()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:28):区分消息事务和后续工作进程。
+
+## 图 2:历史读取、SSE 订阅与断线续传
+
+```mermaid
+flowchart TD
+    WATCH["frontend/src/pages/customer.js<br/>Vue watch:props.page 变化"]
+    LOAD["frontend/src/pages/customer.js<br/>loadPage()<br/>建立页面请求代次"]
+    HISTORY["frontend/src/pages/customer.js<br/>refreshMessages()<br/>按 after 读取历史与当前运行"]
+    CONNECT["frontend/src/pages/customer.js<br/>connectMessages()<br/>维护连接及续传循环"]
+    CLIENT["frontend/src/services/customer.js<br/>events()<br/>选择当前会话事件地址"]
+    FETCH["frontend/src/api.js<br/>events()<br/>fetch 与 EventSourceParserStream"]
+    API["modules/conversations/stream.py<br/>events()<br/>检查权限、游标与 Redis 额度"]
+    RESPONSE["modules/conversations/stream.py<br/>LeasedEventSourceResponse.__call__()<br/>管理响应及额度清理"]
+    GENERATE["modules/conversations/stream.py<br/>generate()<br/>循环读取并生成 SSE 事件"]
+    READ["modules/conversations/stream.py<br/>read_events()<br/>每轮重新验证身份与访问权限"]
+    DB[("MySQL 持久数据<br/>ConversationEvent、Conversation<br/>会话身份及授权记录")]
+    RECEIVE["frontend/src/pages/customer.js<br/>receiveEvent()<br/>处理序号、控制版本与页面状态"]
+    RELEASE["modules/conversations/stream.py<br/>release()<br/>释放 Redis 连接额度"]
+    WATCH -. "Vue 调用监听函数" .-> LOAD
+    LOAD --> HISTORY
+    LOAD -->|有效聊天页面且读取成功| CONNECT
+    CONNECT --> CLIENT
+    CLIENT --> FETCH
+    FETCH -. "GET events;Last-Event-ID;FastAPI 路由" .-> API
+    API -. "返回响应对象;ASGI 调用 __call__" .-> RESPONSE
+    RESPONSE -. "EventSourceResponse 消费异步生成器" .-> GENERATE
+    GENERATE --> READ
+    DB -. "每轮短事务读取已提交事件" .-> READ
+    GENERATE -. "HTTP SSE 正文:完整事件" .-> FETCH
+    FETCH -->|调用传入的 onEvent 回调| RECEIVE
+    CONNECT -->|410:本轮连接循环恢复一次| HISTORY
+    GENERATE -->|finally| RELEASE
+    RESPONSE -->|响应结束时 finally| RELEASE
+```
+
+`loadPage()` 关闭上一条连接,以 `generation` 和 `valid()` 限定当前页面请求。`refreshMessages()` 通过历史接口分页读取事件,并查询会话和活动运行;成功后 `connectMessages()` 建立 SSE。`api.events()` 使用依赖 `EventSourceParserStream` 读取完整事件,随后调用页面传入的 `onEvent`。
+
+服务端每轮 `read_events()` 使用独立事务验证 token、账户、租户和会话权限,查询完成后释放数据库连接。`generate()` 推送事件的 `id`、`event` 和 JSON `data`;当前传输单位为完整持久事件。每次连接约 50 秒结束,客户端正常结束后携带 `state.after` 继续订阅。
+
+`receiveEvent()` 跳过已处理序号,再更新 `state.after`;旧控制版本的助手、Agent 进度和运行事件随后被过滤。游标过期时,连接循环自动重新读取历史和当前状态一次。网络异常显示重新连接入口;页面切换或组件卸载会中止旧连接。`release()` 同时由生成器与响应生命周期保护,重复调用保持幂等。
+
+### 图 2 源码导航
+
+1. [loadPage() 与 watch 注册](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:373)、[refreshMessages()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:152):跟踪初始化、历史分页和页面代次。
+2. [connectMessages()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:216)、[客户端 events()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/api.js:11)、[receiveEvent()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/customer.js:177):跟踪续传游标及 UI 状态。
+3. [服务端 events()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/stream.py:84)、[generate()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/stream.py:131)、[read_events()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/stream.py:50):检查订阅与每轮查询的边界。
+4. [LeasedEventSourceResponse.__call__()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/stream.py:41)、[release()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/stream.py:125)、[history()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:135):核对提前断开、额度清理和历史记录读取。
+
+## 图 3:客服接管、人工回复与消费者页面更新
+
+```mermaid
+flowchart TD
+    CONTROL["frontend/src/pages/merchant.js<br/>control(action)<br/>请求确认接管或交还"]
+    CONFIRM["frontend/src/main.js<br/>confirm()<br/>保存确认窗口与操作回调"]
+    ACCEPT["frontend/src/App.js<br/>accept()<br/>确认后执行已保存回调"]
+    SERVICE["frontend/src/services/merchant.js<br/>control() / command() / request()<br/>附带 expected_control_version"]
+    HTTP["frontend/src/api.js<br/>request()<br/>发送商家身份请求"]
+    API["modules/conversations/api.py<br/>control()<br/>权限、幂等和控制版本检查"]
+    EMIT["modules/conversations/service.py<br/>emit()<br/>登记 control.changed"]
+    REPLY["frontend/src/pages/merchant.js<br/>reply()<br/>发送人工回复并刷新会话"]
+    REPLY_SERVICE["frontend/src/services/merchant.js<br/>reply() / command() / request()<br/>附带会话控制版本"]
+    MESSAGE["modules/conversations/api.py<br/>message() / accept_message()<br/>确认当前接管客服并登记回复"]
+    REFRESH["frontend/src/services/merchant.js<br/>conversation()<br/>重新查询详情与消息历史"]
+    QUERY["modules/conversations/api.py<br/>get_conversation() / history()<br/>读取可访问的会话与事件"]
+    DB[("MySQL 持久数据<br/>Conversation、AgentRun<br/>ConversationEvent")]
+    SSE["modules/conversations/stream.py<br/>read_events() / generate()<br/>已有 SSE 连接读取新事件"]
+    CUSTOMER["frontend/src/pages/customer.js<br/>receiveEvent()<br/>显示控制变化与人工消息"]
+    CONTROL --> CONFIRM
+    CONFIRM -. "Vue 展示窗口;用户确认触发 accept" .-> ACCEPT
+    ACCEPT -->|执行回调中的 run 与 action| SERVICE
+    SERVICE --> HTTP
+    HTTP -. "POST conversations/{id}/{action};FastAPI 路由" .-> API
+    API --> EMIT
+    EMIT -. "事务提交控制变化与适用的运行取消" .-> DB
+    REPLY -->|run 的 action 回调| REPLY_SERVICE
+    REPLY_SERVICE --> HTTP
+    HTTP -. "POST conversations/{id}/messages;FastAPI 路由" .-> MESSAGE
+    MESSAGE -. "同一事务提交 message.staff" .-> DB
+    ACCEPT -->|控制请求成功后,回调继续调用| REFRESH
+    REPLY -->|回复成功后,action 回调继续调用| REFRESH
+    REFRESH -->|经服务 request 包装| HTTP
+    HTTP -. "GET 会话详情与 messages;FastAPI 路由" .-> QUERY
+    DB -. "事务查询已提交记录" .-> QUERY
+    DB -. "读取已提交的控制变化与人工消息" .-> SSE
+    SSE -. "HTTP SSE,经图 2 客户端处理" .-> CUSTOMER
+```
+
+`control()` 要求 `conversations:manage` 权限;接管成功后将控制方设为 `HUMAN`,增加 `control_version`,记录 `assigned_staff_id`,并取消适用的活动运行。交还要求当前员工就是已接管客服。交还之后,客户下一次普通发言创建新的 Agent 运行。
+
+客服回复要求 `conversations:reply` 权限、`HUMAN` 控制方,以及匹配的 `assigned_staff_id`。商家页面打开会话、执行刷新、回复成功或控制操作成功时读取详情与历史。消费者通过持续 SSE 接收 `message.staff`;商家通知的 30 秒轮询负责通知和未读数量。
+
+### 图 3 源码导航
+
+1. [页面 control() 与 reply()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/pages/merchant.js:126)、[confirm()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/main.js:140)、[accept()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/App.js:64)、[服务 control() 与 reply()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/services/merchant.js:62):核对确认步骤、请求字段和成功后刷新。
+2. [后端 control()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:264)、[accept_message()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:205):定位接管、交还和人工回复的权限检查。
+3. [conversation()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/src/services/merchant.js:44)、[get_conversation()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:122)、[history()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:135):核对商家页面读取的数据。
+4. [finish_run()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/service.py:96):理解运行提交时对控制方、控制版本和活动运行的检查。
+
+## 关键数据与状态
+
+- `conversation_id` 标识会话;`active_run_id` 指向当前活动运行。客户普通消息被接受后,后端可能创建新的 `AgentRun`。
+- `sequence` 为会话持久事件序号;`state.after` 为前端已经处理到的序号。历史分页、SSE 续传和消息去重使用这些字段。
+- `control_owner` 为 `AGENT` 或 `HUMAN`;`control_version` 在接管、交还时增加。请求中的 `expected_control_version` 保护当前操作,事件中的版本帮助页面识别旧回复。
+- `Idempotency-Key` 与消息摘要共同限定同一请求。会话消息及控制操作的作用域为租户、会话和操作者。
+- `stream_cursor` 表示可续传窗口边界;游标过期使用 `410 EVENT_CURSOR_EXPIRED`。持久历史继续由 `messages` 接口提供。
+- `WAITING_USER`、`WAITING_APPROVAL`、`run.waiting`、`run.resumed` 支持补充信息与业务等待;`run.failed`、`run.cancelled` 使页面清理对应活动运行。
+
+## 阅读检查问题
+
+1. 同一条消息由于网络原因重复提交时,前后端分别在哪里保留或检查幂等标识?为何返回 `201` 后页面仍可能收到 `run.failed`?
+2. `state.after`、`sequence`、`stream_cursor` 各自控制什么?游标过期与普通连接结束分别经过哪些函数?
+3. 客服接管时,后端怎样取消已有运行,消费者页面怎样处理迟到的助手事件,客服回复又受到哪些身份条件限制?
+
+## 测试文件阅读入口
+
+本路线只阅读用例及其断言;编写本文未执行这些用例,也未调用生成模型。
+
+- [test_conversations.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_conversations.py:47):接管与交还、并发接管、消息幂等、SSE 续传、权限失效与保留窗口。
+- [test_sse_limits.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_sse_limits.py:18):Redis 原子连接额度与共享限制。
+- [customer-sse.mjs](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/tests/customer-sse.mjs:53):浏览器接收人工消息、正常续传、断网恢复、页面离开及游标过期。
+- [merchant-conversation.mjs](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/frontend/tests/merchant-conversation.mjs:56):商家队列进入、人工接管、回复与客户历史可见性。

+ 176 - 0
docs/reading-routes/02-agent-runtime.md

@@ -0,0 +1,176 @@
+# 路线 02:Agent 执行、暂停与恢复
+
+## 学习场景
+
+客户要求查询订单,随后申请换货。沿着同一个 `run_id` 阅读请求受理、模型调用、工具执行、客户补充信息、客户确认和主管审批,观察每一步怎样保存进度、检查权限和继续执行。
+
+图中后端路径相对 `src/zhifu/`。连续箭头表示直接函数调用,箭头文字注明调用条件;间断箭头明确标注 HTTP、框架调度、对象返回或持久数据关系。框架节点和存储节点单独标明类型。`load`、`model`、`followup` 是 `build_graph` 内部函数,`execute` 是 `build_tool_graph` 内部函数。返回[路线目录](README.md)。
+
+## 图 1:请求受理、发布配置与工作进程调度
+
+```mermaid
+flowchart TD
+    HTTP["HTTP 请求<br/>POST /openapi/v1/agent/runs"]
+    A["modules/agent_runtime/api.py<br/>enqueue<br/>身份、店铺权限和幂等检查"]
+    B["modules/agent_runtime/service.py<br/>queue_run<br/>配置快照、任务数量和运行期限"]
+    C["modules/merchant/service.py<br/>current_release_version<br/>读取店铺当前发布版本"]
+    R[("MySQL<br/>modules/merchant/models.py<br/>AgentRelease:发布配置")]
+    D[("MySQL<br/>modules/agent_runtime/models.py<br/>AgentRun:QUEUED 与 release_snapshot")]
+    E["modules/agent_runtime/worker.py<br/>main → execute_one<br/>持续领取运行"]
+    F["modules/agent_runtime/business_wait.py<br/>reconcile<br/>核对业务等待,见图 3"]
+    G["modules/agent_runtime/worker.py<br/>claim<br/>租户调度、期限和执行租约"]
+    H["modules/agent_runtime/capacity.py<br/>acquire<br/>申请并发额度"]
+    I["modules/agent_runtime/runtime.py<br/>build_graph → build_tool_graph<br/>构建主图及工具子图"]
+    J["LangGraph 框架<br/>graph.ainvoke<br/>按 next_node 调度,见图 2"]
+    HTTP -. "HTTP / FastAPI 路由分发" .-> A
+    A -->|新请求| B
+    A -. "相同幂等键及内容:读取原运行并复核权限" .-> D
+    B --> C
+    B -. "按版本读取 manifest" .-> R
+    B -. "事务提交:保存 graph_version 和模型配置" .-> D
+    E -->|每轮首先调用| F
+    E -->|核对之后领取| G
+    D -. "跨进程:读取排队任务或过期租约" .-> G
+    G -->|选定租户后| H
+    G -. "事务提交:RUNNING,lease_token 增加" .-> D
+    E -->|领取成功后| I
+    E -->|调用已构建的图| J
+    I -. "返回已编译图对象" .-> J
+```
+
+`enqueue` 对重复请求执行 `validate_result_access`,当前权限撤销后不能继续读取旧工具结果。新运行保存店铺发布配置;没有店铺发布记录时使用 `AgentSettings` 与当前 `GRAPH_VERSION`。`queue_run` 验证 `graph_version` 已登记在 `GRAPH_RELEASES` 中,当前默认版本为 `after-sales-v10`。
+
+`claim` 优先考虑商家预留额度,再按租户最近获得调度机会的时间选择租户,租户内部按创建时间选择任务。Redis 额度不足时保持排队。租约更新后,旧工作进程无法通过 `locked_run` 的 `lease_token` 检查。
+
+会话消息的另一个入口是 `conversations/api.py` 的 `accept_message`:它准备最近消息、调用同一个 `queue_run`,再保存 `conversation_id`、`control_version` 与 `active_run_id`。该调用与独立运行入口共用后续执行流程。
+
+### 图 1 源码导航
+
+1. [api.py:enqueue](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/api.py:60)、[validate_result_access](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/api.py:128):找到相同幂等键的查询和权限复核。
+2. [service.py:queue_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/service.py:20)、[merchant/service.py:current_release_version](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/merchant/service.py:78):追踪 `release_snapshot`、`graph_version` 与 `deadline_at` 的来源。
+3. [config.py:GRAPH_RELEASES](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/config.py:104):核对不同版本的 `clarification`、`business_wait`、`followup` 开关。
+4. [worker.py:execute_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:137)、[claim](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:28)、[capacity.py:acquire](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/capacity.py:75):分别追踪工作进程、数据库领取和 Redis 并发控制。
+5. [runtime.py:build_graph](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:244)、[conversations/api.py:accept_message](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:205):查看图构建与会话入口的连接位置。
+
+## 图 2:LangGraph 调度模型、工具与最终答复
+
+```mermaid
+flowchart TD
+    G["LangGraph 框架<br/>graph.ainvoke / add_conditional_edges<br/>依据节点返回的 next_node 调度"]
+    L["modules/agent_runtime/runtime.py<br/>load<br/>读取已保存的 next_node"]
+    M["modules/agent_runtime/runtime.py<br/>model<br/>校验引用、读取记忆、调用模型并保存响应"]
+    T["modules/agent_runtime/runtime.py<br/>execute<br/>工具子图中的执行节点"]
+    F["modules/agent_runtime/runtime.py<br/>followup<br/>处理单独的回访登记工具"]
+    K["modules/agent_runtime/runtime.py<br/>locked_run<br/>身份、控制权、租约和店铺权限检查"]
+    V["modules/agent_runtime/tools.py<br/>model_tools → tool_definitions<br/>按 graph_version 声明工具"]
+    P["LangChain / 模型服务<br/>ChatOpenAI.bind_tools(...).ainvoke<br/>返回答复或 tool_calls"]
+    I["modules/agent_runtime/tools.py<br/>invoke_tool<br/>参数校验与业务服务调用"]
+    W["modules/agent_runtime/inputs.py:pause_for_input<br/>modules/agent_runtime/business_wait.py:pause_for_business<br/>保存等待状态,见图 3"]
+    C["modules/conversations/service.py<br/>finish_run<br/>保存最终会话事件并释放 active_run_id"]
+    G -. "框架调度:START → load" .-> L
+    G -. "框架调度:next_node=model" .-> M
+    G -. "框架调度:next_node=tools,进入工具子图" .-> T
+    G -. "框架调度:next_node=followup" .-> F
+    L --> K
+    M -->|调用模型前及提交结果前| K
+    T -->|读取工具请求及提交工具结果前| K
+    F --> K
+    M --> V
+    M -. "SDK 调用并通过 HTTP 访问模型服务" .-> P
+    T -->|订单、工单、创建方案等工具| I
+    F -->|create_followup_request| I
+    T -->|补充信息,或方案创建后的业务等待| W
+    M -->|无 tool_calls:设置 COMPLETED 后调用| C
+    L -. "节点返回 next_node" .-> G
+    M -. "节点返回 tools 或 done" .-> G
+    T -. "节点返回 model、followup 或 wait" .-> G
+    F -. "节点返回 model" .-> G
+```
+
+`build_graph` 通过 `add_node` 注册内部函数,通过 `add_conditional_edges` 定义运行方向。工具节点返回后,由 LangGraph 根据 `next_node` 调度下一节点。`done` 与 `wait` 都连接 `END`;前者对应完成,后者保留等待状态,后续通过图 3 的入口继续原运行。
+
+工具执行与权限检查使用同一套版本清单。`execute` 对每个工具参数调用 Pydantic 校验,并用 `agent:{run_id}:{step}:{index}` 生成业务幂等标识。工具结果、步骤编号和下一节点在数据库事务中共同提交。`create_followup_request` 必须单独调用;工具节点先保存 `next_node=followup`,由框架继续执行回访节点。知识检索使用专用分支:事务外检索,返回后调用 `verified_results`,具体流程见路线 04。
+
+`model` 在网络调用前保存 `ModelAttempt`,通过 `BudgetCallback` 检查预算与记录计量。模型网络请求处于两个短事务之间;返回后保存已经产生的用量,再检查运行权限、知识引用和记忆版本。工作进程遇到异常时保存 `FAILED` 和 `error_code`,并在 `finally` 中释放并发额度。
+
+### 图 2 源码导航
+
+1. [runtime.py:build_graph](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:244)、[load](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:247)、[model](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:284):结合函数末尾的图注册代码阅读调度关系。
+2. [runtime.py:locked_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:47):查看 `token_hash`、会话控制版本、租约与店铺状态检查。
+3. [runtime.py:build_tool_graph / execute](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:95)、[followup](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:252):追踪工具结果、幂等标识和下一节点的共同提交。
+4. [tools.py:model_tools](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/tools.py:65)、[tool_definitions](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/tools.py:49)、[invoke_tool](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/tools.py:79):比较工具声明和执行时采用的同一版本规则。
+5. [budget_callback.py:BudgetCallback](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget_callback.py:24)、[conversations/service.py:finish_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/service.py:96):阅读 LangChain 回调和最终会话事件的保存位置。
+
+## 图 3:补充信息、客户确认与主管审批
+
+```mermaid
+flowchart TD
+    T["modules/agent_runtime/runtime.py<br/>execute<br/>执行需要等待的工具"]
+    P["modules/agent_runtime/inputs.py<br/>pause_for_input<br/>等待客户补充信息"]
+    B["modules/agent_runtime/business_wait.py<br/>pause_for_business<br/>等待方案确认或审批"]
+    R[("MySQL<br/>modules/agent_runtime/models.py<br/>AgentRun + RunInputEvent:状态和输入收据")]
+    H["HTTP / 客户操作<br/>POST /openapi/v1/agent/runs/{run_id}/inputs"]
+    A["modules/agent_runtime/api.py<br/>inputs → owned_run<br/>校验本人运行与当前权限"]
+    I["modules/agent_runtime/inputs.py<br/>accept_input<br/>校验等待类型、版本、期限与幂等键"]
+    X["HTTP / 客户或主管操作<br/>确认方案、修订方案、提交审批决定"]
+    Y["modules/after_sales/api.py<br/>confirm / revise / decide → command<br/>提交相应业务操作"]
+    S["modules/after_sales/service.py<br/>AfterSalesService.execute<br/>调用 confirm、revise 或 decide"]
+    E[("MySQL<br/>modules/after_sales/models.py<br/>Ticket + Confirmation + Approval + BusinessEvent")]
+    W["modules/agent_runtime/worker.py<br/>execute_one<br/>每轮核对业务等待"]
+    Q["modules/agent_runtime/business_wait.py<br/>reconcile → apply_event<br/>核验当前方案、业务事件与授权"]
+    C["modules/agent_runtime/worker.py<br/>claim<br/>重新领取同一个 run_id"]
+    T -->|request_clarification| P
+    T -->|创建方案且版本启用 business_wait| B
+    P -. "同事务:WAITING_USER,expected_input_type=clarification" .-> R
+    B -. "同事务:WAITING_USER 或 WAITING_APPROVAL" .-> R
+    H -. "HTTP / FastAPI 路由分发" .-> A
+    A -->|owned_run 返回后| I
+    I -. "同事务:保存输入收据并设置 QUEUED/model" .-> R
+    X -. "HTTP / FastAPI 路由分发" .-> Y
+    Y --> S
+    S -. "事务提交:当前业务状态、确认和审批证据" .-> E
+    W --> Q
+    R -. "读取业务等待运行" .-> Q
+    E -. "跨进程:读取已提交业务事件和当前版本" .-> Q
+    Q -->|DRAFT、NEEDS_REVISION 或 WAITING_REVIEW| B
+    Q -. "其他有效业务状态:保存收据并设置 QUEUED/model" .-> R
+    Q -. "权限或业务证据检查失败:设置 FAILED" .-> R
+    R -. "跨进程:领取 QUEUED,等待到期则记录 FAILED" .-> C
+```
+
+图中的 `A`、`Y`、`Q` 分别合并显示同一文件内的直接调用;这些调用对应各自入口函数的实际代码。`inputs` 调用 `owned_run` 后才调用 `accept_input`。售后 `command` 调用 `AfterSalesService.execute`;后者根据 `action` 调用对应业务方法。`reconcile` 调用 `apply_event`,统一处理业务检查异常。
+
+补充信息只允许 `expected_input_type=clarification`,必须匹配当前 `interrupt_id` 与 `run_version`。方案确认和审批等待通过售后接口产生业务记录。`apply_event` 检查当前工单版本、客户确认、主管审批及主管当前权限;相同业务事件由 `business_cursor` 和输入收据约束重复处理。方案修订后,等待信息改用新的方案版本及等待标识。
+
+规则失效或订单版本变化时,`apply_event` 可以将工单置为 `NEEDS_REVISION` 并继续等待;订单快照仅仅超过时效时返回 `False`,等待订单同步。审批人权限撤销时,`reconcile` 保存 `FAILED` 和 `APPROVER_REVOKED`。会话接管与取消通过状态、控制版本和租约检查阻止后续提交。
+
+`api.py` 的 `resume` 供 `SERVICE` 身份核对已保存输入收据;输入受理或业务核对事务已经完成重新排队。该接口返回原收据与当前运行状态。
+
+### 图 3 源码导航
+
+1. [inputs.py:pause_for_input](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/inputs.py:63)、[accept_input](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/inputs.py:87):追踪等待标识、输入收据和重新排队。
+2. [api.py:inputs](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/api.py:182)、[owned_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/api.py:102)、[resume](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/api.py:204):核对客户输入与内部收据查询各自的身份要求。
+3. [business_wait.py:pause_for_business](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/business_wait.py:30)、[reconcile](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/business_wait.py:157)、[apply_event](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/business_wait.py:62):按调用方向阅读等待保存、事件核对和业务分支。
+4. [after_sales/api.py:command](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:45)、[confirm](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:142)、[decide](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:173)、[AfterSalesService.execute](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:199):找到产生业务证据的接口事务。
+
+## 关键数据与状态
+
+- [AgentRun](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/models.py:10):`messages`、`step`、`next_node` 保存执行进度;`graph_version` 与 `release_snapshot` 保存本次运行采用的配置;恢复时继续使用这些配置。
+- `status` 包括 `QUEUED`、`RUNNING`、`WAITING_USER`、`WAITING_APPROVAL`、`COMPLETED`、`FAILED`、`CANCELLED`。`version` 处理客户端并发;`lease_token` 处理工作进程执行权;`control_version` 处理会话接管。
+- [RunInputEvent](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/models.py:60):通过租户、运行、幂等键和等待标识约束输入记录。业务恢复收据还保存 `source_event_id`、方案版本和工单版本。
+- [ModelAttempt](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/models.py:86):区分模型请求已经开始、响应已收到和结果未知,保留调用次数及实际用量。
+- 新运行及恢复后的执行期限为 10 分钟;补充信息和业务等待期限为 24 小时。工作进程单次执行限制 120 秒,领取租约为 125 秒。每次运行最多 6 次模型调用、20 个执行步骤,每轮最多 4 个工具调用;单个账户未结束运行数量限制为 3。
+
+## 阅读检查问题
+
+1. 工作进程在工具事务提交后终止,新的工作进程依据哪些字段继续执行?同一方案为何不会重复创建?
+2. 店铺发布了新的 Agent 配置,正在等待客户补充信息的运行恢复后采用哪个版本?工具声明与执行怎样使用这个版本?
+3. 客户补充信息、客户确认方案、主管审批分别保存什么记录?审批后撤销主管权限,运行恢复过程中哪一步会拒绝继续?
+
+## 测试文件阅读入口
+
+本路线仅阅读以下用例及其断言,没有执行测试或模型调用。测试文件中的运行条件和断言用于理解设计边界。
+
+- [test_agent_runtime.py:test_tool_graph_creates_one_proposal_on_replay](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_agent_runtime.py:63) 与 [test_expired_lease_resumes_committed_tool_without_duplicate](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_agent_runtime.py:151):阅读工具重复执行、租约过期及原工具结果复用。
+- [test_agent_inputs.py:test_resume_same_run_atomic_idempotency_and_event](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_agent_inputs.py:54) 与 [test_registered_old_graph_remains_executable](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_agent_inputs.py:138):阅读相同输入并发重试和旧图版本继续执行。
+- [test_business_wait.py:test_confirmation_approval_restore_same_run_once](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_business_wait.py:67)、[test_revision_replaces_wait_and_requires_new_confirmation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_business_wait.py:106)、[test_takeover_and_approver_revocation_prevent_resume](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_business_wait.py:150):阅读业务恢复、方案修订、会话接管与授权撤销。

+ 168 - 0
docs/reading-routes/03-after-sales.md

@@ -0,0 +1,168 @@
+# 路线 03:从换货方案、客户确认和审批追踪到 OMS
+
+## 学习场景
+
+客户为已签收商品申请换货,确认当前方案,主管审核通过,履约工作进程向 OMS 提交操作。沿这个场景观察资格校验、版本检查、事务、消息投递、数量预占和外部结果恢复。
+
+图中的后端路径均相对 `src/zhifu/`。连续箭头表示直接函数调用,标注“框架调用”的箭头表示 FastAPI 分发;间断箭头表示 HTTP、消息或持久数据连接。数据节点使用类名,函数节点使用源码中的函数名。同一调用者的多条连续箭头表示它分别调用相关函数,执行条件写在箭头或正文中。返回[路线目录](README.md)。
+
+## 图 1:创建方案、客户确认与主管审批
+
+三个操作分别来自独立 HTTP 请求。`command` 建立事务并认证,`execute` 根据 `action` 检查权限、读取幂等收据并选择业务方法。
+
+```mermaid
+flowchart TB
+    HTTP["FastAPI<br/>三个独立 POST 请求"]
+    API["modules/after_sales/api.py<br/>create_proposal / confirm / decide<br/>提供各自的 action"]
+    CMD["modules/after_sales/api.py<br/>command<br/>建立业务事务"]
+    AUTH["modules/identity/auth.py<br/>authenticate<br/>确认当前身份"]
+    EXEC["modules/after_sales/service.py<br/>AfterSalesService.execute<br/>权限、幂等与业务分派"]
+    CREATE["modules/after_sales/service.py<br/>AfterSalesService.create<br/>保存 DRAFT 方案及规则依据"]
+    CONFIRM["modules/after_sales/service.py<br/>AfterSalesService.confirm<br/>保存客户确认与 PENDING 审批"]
+    DECIDE["modules/after_sales/service.py<br/>AfterSalesService.decide<br/>校验审批人并记录决定"]
+    VALIDATE["modules/after_sales/service.py<br/>AfterSalesService.validate_proposal<br/>读取方案引用的规则和订单版本"]
+    EVALUATE["modules/after_sales/service.py<br/>AfterSalesService.evaluate → assess<br/>校验时效、签收、范围与数量"]
+    CHANGED["modules/after_sales/service.py<br/>AfterSalesService.changed<br/>增加工单版本"]
+    EMIT["modules/after_sales/service.py<br/>AfterSalesService.emit<br/>登记业务事件"]
+    EVENT[("modules/after_sales/models.py<br/>BusinessEvent<br/>与业务变更共同提交")]
+    RECEIPT[("modules/after_sales/models.py<br/>CommandReceipt<br/>保存请求摘要与响应")]
+    HTTP -->|框架调用| API
+    API --> CMD
+    CMD --> AUTH
+    CMD -->|认证成功后| EXEC
+    EXEC -->|action=create| CREATE
+    EXEC -->|action=confirm| CONFIRM
+    EXEC -->|action=decide| DECIDE
+    CREATE --> EVALUATE
+    CONFIRM --> VALIDATE
+    DECIDE --> VALIDATE
+    VALIDATE --> EVALUATE
+    CONFIRM --> CHANGED
+    DECIDE --> CHANGED
+    CREATE --> EMIT
+    CHANGED --> EMIT
+    EMIT -.->|事务内保存| EVENT
+    EXEC -.->|读取重试收据或保存新响应| RECEIPT
+```
+
+`create` 使用当前规则建立 `ProposalVersion`,保存 `rule_version`、`order_version` 和 `citation`。`confirm`、`decide` 使用该方案引用的版本重新检查;订单来源版本变化会要求修订。图中 `evaluate → assess` 表示 `evaluate` 直接调用 `assess`。
+
+`confirm` 只接受订单所属客户,检查方案版本与工单版本后进入 `WAITING_REVIEW`。`decide` 检查当前审批仍为 `PENDING`、客户确认存在,以及审批人和方案提交者不同;通过后进入 `APPROVED` 并产生 `after_sale.approved`。`execute` 的当前权限检查发生在读取并返回旧收据之前。
+
+### 图 1 源码导航
+
+- [API:command 与事务入口](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:45)、[create_proposal](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:123)、[confirm](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:142)、[decide](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:173)。
+- [execute:身份类型、店铺权限、接管权、幂等收据](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:199)。客服创建或修订方案时,需要当前客户会话的接管权及对应控制版本。
+- [create](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:413)、[validate_proposal](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:453)、[confirm](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:470)、[decide](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:504)。
+- [evaluate](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:136)、[assess](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:107):五分钟订单快照时效、签收时间、申请期限、商品类型和数量规则。
+- [changed](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:448)、[emit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:141)、[CommandReceipt 与 BusinessEvent](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/models.py:141)。
+
+## 图 2:审批事件触发履约准备
+
+工作进程读取已经提交的 `BusinessEvent`。发布循环与消费循环独立运行;消息只传递事件标识等信息,`prepare_event` 根据 `event_id` 读取数据库中的业务事件。
+
+```mermaid
+flowchart TB
+    EVENT[("modules/after_sales/models.py<br/>BusinessEvent<br/>已提交的审批事件")]
+    PUB["modules/integrations/worker.py<br/>publish_one<br/>领取发布租约"]
+    SEND["modules/integrations/worker.py<br/>Broker.publish_event<br/>等待 RabbitMQ 发布确认"]
+    MQ[("RabbitMQ<br/>zhifu.events / zhifu.fulfillment")]
+    CONSUME["modules/integrations/worker.py<br/>consume_one<br/>取得消息并管理确认"]
+    PRE["modules/integrations/fulfillment.py<br/>prepare_event<br/>读取事件、检查消费收据"]
+    ORDER["modules/integrations/client.py<br/>OmsClient.order<br/>取得最新订单快照"]
+    OMS["oms/api.py<br/>create_app.get_order<br/>返回凭据范围内订单"]
+    PREPARE["modules/integrations/fulfillment.py<br/>prepare<br/>复查审批、规则和订单,预占数量"]
+    OP[("modules/integrations/models.py<br/>ExternalOperation / Reservation<br/>PREPARED 操作与 HELD 数量")]
+    RECEIPT[("modules/integrations/models.py<br/>ConsumerReceipt<br/>已处理的事件标识")]
+    EVENT -.->|数据库查询 PENDING 事件| PUB
+    PUB --> SEND
+    SEND -.->|发布持久消息| MQ
+    MQ -.->|queue.get 取得消息| CONSUME
+    CONSUME --> PRE
+    EVENT -.->|按 event_id 读取| PRE
+    PRE -->|审批事件且工单仍为 APPROVED| ORDER
+    ORDER -.->|"HTTP GET /orders/{order_id};FastAPI 分发"| OMS
+    PRE -->|取得快照后建立事务| PREPARE
+    PREPARE -.->|检查通过后共同保存| OP
+    PRE -.->|与履约准备共同提交| RECEIPT
+    RECEIPT -.->|重复事件直接结束| PRE
+```
+
+`prepare` 重新检查租户、店铺、审批员工状态及 `approvals:decide` 权限,同时核对方案版本、确认记录、规则有效性、连接器配置和 OMS 订单归属。业务条件变化时,工单进入 `NEEDS_REVISION`;可用数量扣除当前 `HELD` 数量后不足时,进入 `FAILED`。
+
+准备成功时,`ExternalOperation`、`Reservation` 和工单 `PROCESSING` 状态共同提交。`consume_one` 在处理事务完成后确认消息。发布确认和消费收据分别处理发布端重发与消费端重复执行。
+
+### 图 2 源码导航
+
+- [worker.main:建立发布、消费和执行循环](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:117)。图中省略循环节点,三个循环由同一个工作进程启动并各自推进。
+- [publish_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:78)、[Broker.publish_event](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:54)、[consume_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:104)。
+- [prepare_event](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:46)、[OmsClient.order](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/client.py:45)、[OMS get_order](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/oms/api.py:156)。外部订单查询在履约准备事务之外完成。
+- [prepare](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:85)、[ExternalOperation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/models.py:23)、[Reservation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/models.py:53)、[ConsumerReceipt](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/models.py:71)。
+
+## 图 3:OMS 受理、完成与网络结果恢复
+
+`execute_due` 从数据库领取到期操作,每次增加 `lease_token`。首次领取提交操作;后续领取先查询同一 `operation_id`,查询返回 404 才提交操作。仓库使用带有 `can_fulfill` 权限的凭据调用完成接口。
+
+```mermaid
+flowchart TB
+    OP[("modules/integrations/models.py<br/>ExternalOperation<br/>PREPARED / UNKNOWN / ACCEPTED")]
+    LOOP["modules/integrations/worker.py<br/>main.executor<br/>持续领取到期操作"]
+    EXEC["modules/integrations/fulfillment.py<br/>execute_due<br/>取得租约并执行请求"]
+    CLIENT["modules/integrations/client.py<br/>OmsClient.process<br/>按领取次数提交或查询"]
+    SUBMIT["oms/api.py<br/>create_app.submit<br/>幂等受理并扣减数量"]
+    QUERY["oms/api.py<br/>create_app.query<br/>查询当前操作结果"]
+    EXCHANGE[("oms/models.py<br/>Exchange<br/>保存外部状态与版本")]
+    WAREHOUSE["仓库请求<br/>具有 can_fulfill 权限"]
+    COMPLETE["oms/api.py<br/>create_app.complete<br/>推进为 COMPLETED"]
+    RECORD["modules/integrations/fulfillment.py<br/>record_result<br/>核对租约与外部版本"]
+    TICKET[("modules/after_sales/models.py<br/>Ticket<br/>保存工单状态")]
+    VIEW["modules/after_sales/api.py<br/>get_ticket<br/>通过 ticket_view 返回当前进度"]
+    LOOP --> EXEC
+    OP -.->|数据库查询到期操作| EXEC
+    EXEC --> CLIENT
+    CLIENT -.->|HTTP PUT;首次或查询返回 404;FastAPI 分发| SUBMIT
+    CLIENT -.->|HTTP GET;后续领取;FastAPI 分发| QUERY
+    SUBMIT -.->|同一事务保存结果与订单数量| EXCHANGE
+    EXCHANGE -.->|读取状态与版本| QUERY
+    WAREHOUSE -.->|"HTTP POST /exchanges/{operation_id}/complete;FastAPI 分发"| COMPLETE
+    COMPLETE -.->|事务提交 COMPLETED 与新版本| EXCHANGE
+    EXEC -->|process 返回结果或 None 后| RECORD
+    RECORD -.->|保存操作状态与下次执行时间| OP
+    RECORD -.->|保存 PROCESSING / RESULT_UNKNOWN / COMPLETED / FAILED| TICKET
+    TICKET -.->|后续 GET 请求读取| VIEW
+```
+
+`record_result` 仅接受当前有效租约提交的结果,并要求外部版本递增。`process` 遇到传输错误、HTTP 429 或服务端错误时返回 `None`;尚未明确受理的操作进入 `UNKNOWN`,对应工单进入 `RESULT_UNKNOWN`,数量预占继续保持 `HELD`。已经确认 `ACCEPTED` 的操作遇到未知结果时保留已确认状态,等待后续查询。
+
+OMS 返回 `ACCEPTED` 后预占状态为 `CONSUMED`,工单继续保持 `PROCESSING`;返回 `COMPLETED` 后工单完成;明确返回 `FAILED` 时预占状态为 `RELEASED`。完成接口由外部履约方调用,平台工作进程通过后续查询取得完成结果。
+
+### 图 3 源码导航
+
+- [main.executor](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:142)、[execute_due](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:216)、[OmsClient.process](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/client.py:50)。
+- [OMS submit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/oms/api.py:181)、[query](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/oms/api.py:239)、[complete](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/oms/api.py:252)。
+- [record_result](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:245)、[get_ticket](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:202)、[ticket_view](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:157)。
+
+## 关键数据与状态
+
+- `Ticket.version` 控制工单并发修改;`Ticket.proposal_version` 标识当前方案。`ProposalVersion` 保存当时的规则、订单版本和规则依据。
+- `Confirmation` 与 `Approval` 都关联具体方案版本。`revise` 增加方案版本,将待处理旧审批改为 `INVALIDATED`,工单回到 `DRAFT`,需要客户重新确认。
+- `CommandReceipt` 使用租户、操作者、操作路径和 `Idempotency-Key` 组成作用域;相同标识更换参数返回 `IDEMPOTENCY_CONFLICT`。
+- `BusinessEvent.aggregate_version` 用于确认事件仍对应当前工单。`ConsumerReceipt` 以消费者名称和事件标识记录处理结果。
+- `ExternalOperation.id` 作为 OMS 的 `operation_id` 保持不变。操作的执行租约和外部结果版本分别控制当前工作进程提交权、外部状态推进。
+- 主流程为 `DRAFT → WAITING_REVIEW → APPROVED → PROCESSING → COMPLETED`。审批可以进入 `REJECTED`;取消操作受当前状态和外部操作登记情况约束;履约准备可能进入 `NEEDS_REVISION` 或 `FAILED`。
+
+## 阅读检查问题
+
+1. 客户确认与主管审批之间,订单来源版本变化时,哪个函数拒绝继续处理?方案修订后,哪些记录需要重新产生?
+2. RabbitMQ 重复投递同一审批事件时,哪个事务保证只登记一次履约准备?发布租约与消费收据各自负责什么?
+3. OMS 已受理但响应没有到达平台时,为什么数量继续保持预占?后续执行怎样复用 `operation_id`,并拒绝旧工作进程提交的结果?
+
+## 相关测试阅读入口
+
+本路线只阅读测试代码,未执行下列用例。
+
+- [test_confirmation_approval_and_isolation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_after_sales.py:298):客户确认、员工审批和数据访问范围。
+- [test_concurrent_idempotency_and_conflict](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_after_sales.py:333)、[test_revision_invalidates_previous_approval](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_after_sales.py:369):并发重复提交和方案版本变化。
+- [test_broker_delivery_and_fulfillment](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_fulfillment.py:138):消息重复投递、OMS 受理、仓库完成与工单查询。
+- [test_lost_response_keeps_reservation_and_recovers](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_fulfillment.py:255):通过网络响应延迟观察结果未知与后续恢复。
+- [test_revoked_approval_permission_prevents_external_operation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_fulfillment.py:302):审批权限变化后,履约准备重新检查当前权限。

+ 174 - 0
docs/reading-routes/04-knowledge-memory.md

@@ -0,0 +1,174 @@
+# 路线 04:知识检索、引用校验与客户记忆
+
+## 学习场景
+
+商家发布键盘清洁说明,客户询问清洁方法,并授权系统以后使用简洁回复。沿着资料发布、回答引用和沟通习惯保存三个过程,理解知识正文、检索索引、客户授权和模型上下文各自的作用。
+
+图中后端路径相对 `src/zhifu/`。连续箭头表示直接函数调用,箭头文字注明调用条件;间断箭头标明 HTTP、框架调度、异步队列或持久数据关系。框架节点、第三方库节点和存储节点单独标明类型。`execute`、`model` 分别是 Agent 图中的工具节点与模型节点内部函数。返回[路线目录](README.md)。
+
+## 图 1:知识草稿、向量索引与发布版本
+
+```mermaid
+flowchart TD
+    H["HTTP / 商家知识管理<br/>创建或修订、发布、撤回"]
+    A["modules/knowledge/api.py<br/>revise<br/>正文分段、版本检查并保存草稿"]
+    S["第三方库<br/>RecursiveCharacterTextSplitter.split_text<br/>生成带重叠内容的分段"]
+    B["modules/knowledge/api.py<br/>transition<br/>处理 publish 或 revoke"]
+    D[("MySQL<br/>modules/knowledge/models.py<br/>KnowledgeDocument:正文、分段、版本、状态")]
+    I["modules/knowledge/vector.py<br/>RetrievalClient.index_document<br/>分批生成向量并写入索引"]
+    E["modules/knowledge/vector.py<br/>RetrievalClient.embeddings<br/>请求内部向量服务"]
+    F["modules/knowledge/inference.py<br/>embed<br/>检查文本数量和长度"]
+    Q["modules/knowledge/batching.py<br/>Batcher.submit<br/>加入队列并等待 Future"]
+    W["modules/knowledge/batching.py<br/>Batcher.run<br/>合并文本并调用 model.encode"]
+    V[("Milvus<br/>document + version + position<br/>dense、sparse 与检索字段")]
+    H -. "HTTP / FastAPI:PUT 知识资料" .-> A
+    H -. "HTTP / FastAPI:POST publish 或 revoke" .-> B
+    A --> S
+    A -. "事务提交:version 增加,status=DRAFT" .-> D
+    D -. "读取并锁定当前版本" .-> B
+    B -->|publish 且当前为 DRAFT| I
+    I --> E
+    E -. "HTTP POST /embeddings" .-> F
+    F --> Q
+    Q -. "进程内异步队列:等待批处理结果" .-> W
+    I -. "向量返回后:MilvusClient.upsert" .-> V
+    B -. "索引完成后重新检查权限和版本:PUBLISHED" .-> D
+    B -. "revoke:REVOKED,version 增加" .-> D
+```
+
+`revise` 使用 `chunk_size=500`、`chunk_overlap=50` 生成最多 64 个分段,同时保留 `source_text`。当前版本与 `expected_version` 不符时拒绝写入;相同标题、正文及上一版本的紧邻重试可以返回已有结果。
+
+`transition` 在索引写入前后分别检查权限与资料版本。向量生成和 Milvus 写入发生在数据库事务之外,随后重新读取当前资料并设置 `PUBLISHED`。同版本已经发布时直接返回。撤回通过 MySQL 状态和版本立即影响检索可用性,检索流程在图 2 中再次核验这些数据。
+
+向量服务由 `inference.py` 的 `lifespan` 加载 `SentenceTransformer`,并通过 `TaskGroup.create_task` 启动 `Batcher.run`。`Batcher.submit` 等待每个请求自己的 `Future`;`Batcher.run` 通过 `asyncio.to_thread` 调用 `model.encode`,再按请求顺序交付向量。默认使用固定 revision 的 `BAAI/bge-m3`,每个向量为 1024 维。
+
+### 图 1 源码导航
+
+1. [knowledge/api.py:revise](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/api.py:150)、[transition](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/api.py:198):核对修订、发布、撤回各自更新哪些字段。
+2. [knowledge/vector.py:RetrievalClient.index_document](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/vector.py:75)、[embeddings](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/vector.py:66):追踪每批最多 32 个分段、向量维度检查与索引主键。
+3. [knowledge/inference.py:lifespan](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/inference.py:25)、[embed](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/inference.py:79)、[knowledge/config.py:KnowledgeSettings](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/config.py:5):查看独立向量服务的模型加载、接口入口和配置。
+4. [knowledge/batching.py:Batcher.submit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/batching.py:48)、[Batcher.run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/batching.py:80):查看队列容量、等待超时、取消请求与结果顺序处理。
+5. [knowledge/models.py:KnowledgeDocument](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/models.py:10)、[knowledge/vector.py:setup](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/vector.py:22):对照 MySQL 正文记录与 Milvus 字段、中文分析器和索引定义。
+
+## 图 2:混合检索、候选校验与回答引用
+
+```mermaid
+flowchart TD
+    H["HTTP<br/>POST /openapi/v1/stores/{store_id}/knowledge/search"]
+    A["modules/knowledge/api.py<br/>retrieve<br/>检查身份与店铺权限"]
+    G["LangGraph 框架<br/>调度 tools 子图或 model 节点"]
+    T["modules/agent_runtime/runtime.py<br/>execute<br/>处理 search_knowledge 工具"]
+    S["modules/knowledge/vector.py<br/>RetrievalClient.search<br/>店铺过滤、向量与 BM25 混合检索"]
+    E["modules/knowledge/vector.py<br/>RetrievalClient.embeddings<br/>取得查询文本向量,见图 1"]
+    V[("Milvus<br/>dense + sparse<br/>hybrid_search / RRFRanker")]
+    O["modules/knowledge/vector.py<br/>stable_candidates<br/>相同分数按正文摘要等字段排序"]
+    F["modules/knowledge/api.py<br/>verified_results<br/>核对当前发布版本并读取正文"]
+    D[("MySQL<br/>modules/knowledge/models.py<br/>KnowledgeDocument:当前有效资料")]
+    R[("MySQL<br/>modules/agent_runtime/models.py<br/>AgentRun.messages:ToolMessage 引用")]
+    M["modules/agent_runtime/runtime.py<br/>model → validate_citations<br/>模型调用前后核验引用"]
+    H -. "HTTP / FastAPI 路由分发" .-> A
+    G -. "框架调度工具子图" .-> T
+    A --> S
+    T -->|通过 search_knowledge 参数调用| S
+    S --> E
+    S -. "SDK 请求:每种检索 30 个候选,RRF 合并" .-> V
+    S -->|收到候选后| O
+    A -->|检索返回后重新认证| F
+    T -->|检索返回后重新检查执行权| F
+    D -. "读取当前 PUBLISHED 版本和正文" .-> F
+    T -. "保存 F 返回的 document_id、version、position 和正文" .-> R
+    G -. "框架调度 model 节点" .-> M
+    R -. "读取消息中的引用" .-> M
+    D -. "再次检查店铺、发布状态与引用版本" .-> M
+```
+
+应用启动时,`app.py` 将复用的 `RetrievalClient` 保存在 `app.state.knowledge`;Agent 工作进程的 `main` 将同类客户端的 `knowledge.search` 传给 `execute_one`。图中的两条入口因此使用同一检索实现。
+
+`RetrievalClient.search` 同时过滤 `tenant` 和 `store`,分别发起向量相似度与中文 BM25 检索,再由 `RRFRanker` 合并候选。`verified_results` 重新检查访问权限,从 MySQL 读取当前 `PUBLISHED` 文档,仅接受版本一致的候选,返回正文、标题和引用标识。HTTP 接口默认返回最多 5 个分段,允许 `limit` 为 1 至 10;Agent 工具分支调用 `verified_results` 时使用其默认值 5。
+
+`validate_citations` 检查 `ToolMessage` 中的资料引用,分别在生成模型调用前、模型返回后执行。资料已撤回、改版或不属于当前店铺时抛出 `KNOWLEDGE_CHANGED`,工作进程保存失败状态,最终答复不会提交。检索正文和已经产生的模型用量有各自的保存位置。
+
+### 图 2 源码导航
+
+1. [knowledge/api.py:retrieve](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/api.py:289)、[agent_runtime/runtime.py:execute](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:96):分别找到 HTTP 检索入口和 Agent 工具入口。
+2. [knowledge/vector.py:RetrievalClient.search](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/vector.py:98)、[stable_candidates](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/vector.py:117):追踪检索过滤、RRF 排序与相同分数的处理。
+3. [knowledge/api.py:verified_results](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/api.py:243):查看候选标识怎样对应 MySQL 的当前正文与版本。
+4. [agent_runtime/runtime.py:validate_citations](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:221)、[model](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:284):找到模型调用前后的两次引用检查,以及用量独立提交的位置。
+5. [app.py:retrieval_client 初始化](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/app.py:66)、[agent_runtime/worker.py:main](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:210):确认 HTTP 进程与工作进程分别管理自己的检索客户端。
+
+## 图 3:客户授权、记忆提取与后续使用
+
+```mermaid
+flowchart TD
+    A["modules/memory/api.py<br/>consent / change / clear<br/>客户授权、主动修改或清空"]
+    P[("MySQL<br/>modules/memory/models.py<br/>MemoryProfile:consent、epoch、revision、value_key")]
+    C["modules/conversations/api.py<br/>accept_message<br/>接受客户消息并保存事件"]
+    E["modules/memory/service.py<br/>enqueue<br/>已授权时创建提取任务"]
+    J[("MySQL<br/>modules/memory/models.py<br/>MemoryJob:消息位置、epoch、执行租约")]
+    X["modules/memory/worker.py<br/>execute_one → execute_claimed<br/>申请后台额度后处理任务"]
+    K["modules/memory/worker.py<br/>claim<br/>检查授权代次、会话状态和并发额度"]
+    W["modules/memory/worker.py<br/>process<br/>提取沟通习惯并复核版本与租约"]
+    L["LangMem / LangChain / 模型服务<br/>create_memory_manager → manager.ainvoke<br/>按 Communication 提取明确表达的习惯"]
+    S["modules/memory/service.py<br/>write / erase<br/>写入独立内容键或删除指定内容键"]
+    R[("Redis<br/>AsyncRedisStore<br/>租户、客户、epoch 对应的记忆内容")]
+    M["modules/agent_runtime/runtime.py<br/>model<br/>调用前读取,提交答复前复核版本"]
+    U["modules/memory/service.py<br/>snapshot / read<br/>仅返回当前授权且仍有效的内容"]
+    A -. "事务提交:更新授权、epoch、revision 和可读内容键" .-> P
+    A -->|change 写入新内容;三种成功操作均删除旧内容| S
+    C -->|客户消息| E
+    P -. "读取 consent 和 epoch" .-> E
+    E -. "同会话消息事务:创建 QUEUED 任务" .-> J
+    X -->|execute_claimed 调用| K
+    X -->|领取成功后经内部 invoke 调用| W
+    J -. "跨进程:领取任务,增加 lease_token" .-> K
+    P -. "授权关闭或 epoch 不同:任务 CANCELLED" .-> K
+    W -. "LangMem 调用与模型 HTTP 请求" .-> L
+    W --> S
+    S -. "AsyncRedisStore.aput / adelete" .-> R
+    P -. "提交前读取当前状态,与领取时快照核对" .-> W
+    W -. "校验通过:更新 value_key 和 revision" .-> P
+    W -. "校验通过为 COMPLETED;版本变化时取消本次任务" .-> J
+    M --> U
+    P -. "读取授权、有效期及内容键" .-> M
+    R -. "AsyncRedisStore.aget:读取指定内容键" .-> U
+```
+
+记忆接口要求 `CUSTOMER` 身份。`consent`、`change`、`clear` 都以 `expected_revision` 检查并发;成功操作增加 `epoch` 与 `revision`。`consent` 每次成功调用均清空已有内容,包括 `enabled` 与当前值相同的情况;`clear` 清空内容并保留当前授权选择。主动修改通过 `Communication` 校验字段;`source=user` 表明内容由客户主动设置。
+
+`enqueue` 在客户消息事务内创建 `MemoryJob`。`claim` 检查客户当前授权与任务的 `epoch` 是否一致,同一客户存在运行中的提取任务、会话仍有 `active_run_id` 或租户后台额度不足时暂不领取。`process` 读取任务引用的那条客户消息,使用 LangMem 提取明确表达的长期沟通习惯,字段限于 `response_style`、`language`、`contact_channel`。
+
+`process` 写入新的 Redis 内容键后保存模型用量,再核对 `MemoryProfile` 快照与任务租约。校验通过才把新键设为可读;授权变化或客户修改造成版本不一致时,任务取消并删除本次新内容。有效执行权已经被其他工作进程取得时,当前进程不能覆盖其任务状态。
+
+Agent 的 `model` 通过 `snapshot` 与 `read` 取得授权记忆,将 `communication` 添加到系统提示的回复形式说明中;模型返回后再次检查快照。调用前后 `snapshot` 不一致时返回 `MEMORY_CHANGED`,当前答复不能提交。已授权客户修改或清空内容会改变快照;未授权状态的快照为 `None`,在该状态执行 `clear` 后仍为 `None`。
+
+### 图 3 源码导航
+
+1. [memory/api.py:consent](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/api.py:68)、[change](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/api.py:93)、[clear](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/api.py:129):核对每种客户操作的授权与版本变化。
+2. [conversations/api.py:accept_message](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py:205)、[memory/service.py:enqueue](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/service.py:99):找到消息事件与提取任务共同提交的位置。
+3. [memory/worker.py:execute_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/worker.py:205)、[execute_claimed](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/worker.py:222)、[claim](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/worker.py:32):阅读后台容量、客户任务串行、会话等待及过期租约。
+4. [memory/worker.py:process](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/worker.py:94)、[memory/service.py:Communication](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/service.py:16):查看 LangMem 的字段限制、提取提示、用量保存与提交检查。
+5. [memory/service.py:namespace](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/service.py:38)、[snapshot](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/service.py:53)、[read](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/service.py:76)、[write](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/service.py:85)、[erase](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/service.py:93):追踪 Redis 命名空间、内容有效期与删除对象。
+6. [agent_runtime/runtime.py:model](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:284):找到记忆进入系统提示以及返回后的版本复核。
+
+## 关键数据与状态
+
+- [KnowledgeDocument](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/models.py:10):`source_text` 保存编辑原文;`chunks` 保存检索分段;`version`、`status` 决定引用是否有效。状态包括 `DRAFT`、`PUBLISHED`、`REVOKED`。
+- 知识索引主键包含租户、文档、版本和分段位置;每条引用保留 `document_id`、`version`、`position`。知识访问范围为当前租户与店铺。
+- [MemoryProfile](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/models.py:10):`consent` 控制授权;`epoch` 使旧任务失效;`revision` 控制并发操作;`value_key` 指向当前可读内容。客户记忆以租户和客户为范围,可以用于该客户的后续会话。
+- [MemoryJob](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/models.py:26):`conversation_id` 与 `sequence` 指向来源消息,状态包括 `QUEUED`、`RUNNING`、`COMPLETED`、`CANCELLED`、`FAILED`,`lease_token` 防止旧工作进程覆盖新执行结果。
+- 记忆在 MySQL 中的可读有效期为 29 天,Redis 内容 TTL 为 30 天。未授权或已经过期时返回空内容;有效 MySQL 记录对应的 Redis 内容缺失时返回 `MEMORY_UNAVAILABLE`。
+
+## 阅读检查问题
+
+1. Milvus 仍保存某个旧版本向量时,为什么知识检索不能继续返回已撤回正文?模型正在生成时撤回资料,哪一次检查处理这个变化?
+2. 客户在记忆提取过程中清空内容,旧任务已经产生模型费用并写入新 Redis 内容键。代码分别怎样处理用量、内容键和任务状态?
+3. 店铺知识与客户记忆分别使用哪些字段隔离访问范围?客户沟通习惯通过什么位置进入模型请求,订单事实又通过什么入口查询?
+
+## 测试文件阅读入口
+
+本路线仅阅读以下用例及其断言,没有执行测试、向量计算或生成模型调用。
+
+- [test_knowledge_memory.py:test_publication_and_revocation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_knowledge_memory.py:52):阅读发布前后、撤回后、跨租户访问及旧向量清理的断言。
+- [test_knowledge_memory.py:test_memory_consent_isolation_and_clear](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_knowledge_memory.py:84)、[test_memory_clear_invalidates_queued_extraction](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_knowledge_memory.py:107):阅读授权、客户隔离、并发版本和旧提取任务取消。
+- [knowledge_memory_model_acceptance.py:test_actual_model_retrieves_published_document](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/knowledge_memory_model_acceptance.py:17)、[test_actual_langmem_extraction_and_cross_conversation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/knowledge_memory_model_acceptance.py:52):阅读真实模型验收文件中工具检索和跨会话记忆的检查方法。
+- [knowledge_memory_model_acceptance.py:test_clear_during_actual_extraction](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/knowledge_memory_model_acceptance.py:109)、[test_revoke_during_actual_answer](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/knowledge_memory_model_acceptance.py:149):阅读模型调用期间清空记忆、撤回资料的并发场景。

+ 162 - 0
docs/reading-routes/05-identity-permissions.md

@@ -0,0 +1,162 @@
+# 路线 05:从登录追踪到租户隔离、权限检查与后台复验
+
+## 学习场景
+
+客户登录后查询自己的工单;同一身份提交 Agent 任务,工作进程在稍后执行时重新检查会话和权限;平台员工通过独立入口访问平台功能。沿三个场景观察凭据如何建立、身份如何验证、资源范围如何限制。
+
+图中的后端路径均相对 `src/zhifu/`。连续箭头表示直接函数调用;标注“框架调用”的箭头表示 FastAPI 或 LangGraph 调用已注册函数。间断箭头表示 HTTP、持久数据或框架注册连接,箭头说明具体含义。数据节点使用类名,函数节点使用源码中的函数名。返回[路线目录](README.md)。
+
+## 图 1:登录与一次工单查询
+
+登录成功产生有效期为 3600 秒的 opaque Bearer token,数据库保存 token 的 SHA-256 摘要。客户查询工单时,服务端重新验证会话、租户与账户状态,然后检查店铺权限及工单客户归属。
+
+```mermaid
+flowchart TB
+    CLIENT["客户端<br/>保存登录响应中的 token"]
+    LOGIN["modules/identity/auth.py<br/>login<br/>限流、密码验证与会话创建"]
+    SESSION[("modules/identity/access_models.py<br/>AccessSession<br/>token_hash、有效期与撤销状态")]
+    GET["modules/after_sales/api.py<br/>get_ticket<br/>建立查询事务"]
+    AUTH["modules/identity/auth.py<br/>authenticate<br/>读取 Bearer token 并计算摘要"]
+    HASH["modules/identity/auth.py<br/>authenticate_hash<br/>复查会话、租户和账户"]
+    TENANT[("modules/identity/models.py<br/>Tenant<br/>租户状态")]
+    PRINCIPAL[("modules/identity/access_models.py<br/>Principal<br/>账户归属、类型与状态")]
+    FIND["modules/after_sales/service.py<br/>AfterSalesService.find<br/>按 tenant_id 与资源 id 查询"]
+    ACCESS["modules/after_sales/service.py<br/>AfterSalesService.access<br/>检查客户本人资源"]
+    STORE["modules/identity/auth.py<br/>authorize_store<br/>检查店铺状态与指定权限"]
+    GRANT[("modules/identity/access_models.py<br/>StoreGrant<br/>租户、账户、店铺、permission")]
+    VIEW["modules/after_sales/service.py<br/>AfterSalesService.ticket_view<br/>返回授权范围内工单"]
+    CLIENT -.->|HTTP POST /openapi/v1/auth/sessions;FastAPI 分发| LOGIN
+    LOGIN -.->|事务保存 token 摘要| SESSION
+    LOGIN -.->|HTTP 响应返回 token| CLIENT
+    CLIENT -.->|"HTTP GET /openapi/v1/tickets/{ticket_id};FastAPI 分发"| GET
+    GET --> AUTH
+    AUTH --> HASH
+    SESSION -.->|读取与重新刷新| HASH
+    TENANT -.->|锁定并检查 ACTIVE| HASH
+    PRINCIPAL -.->|锁定并检查 ACTIVE| HASH
+    GET -->|认证成功后| FIND
+    GET -->|取得工单后| ACCESS
+    ACCESS --> STORE
+    GRANT -.->|按四个字段读取权限记录| STORE
+    GET -->|全部检查通过后| VIEW
+```
+
+`login` 按账户和客户端地址分别限制每 60 秒 10 次、60 次,失败请求也计数。密码验证通过后,它重新读取账户状态和密码摘要,再登记会话。`authenticate_hash` 对账户记录加锁并重新刷新会话状态,使当前操作与权限撤销具有明确的事务顺序。
+
+`find` 将 `auth.tenant_id` 加入查询;`authorize_store` 检查店铺属于当前租户、处于 `ACTIVE`,且存在指定 `StoreGrant`;`access` 对 `CUSTOMER` 继续检查 `subject_id == actor_id`。无权访问业务资源时返回 `RESOURCE_NOT_FOUND`。身份或会话失效时返回 `UNAUTHENTICATED`。
+
+### 图 1 源码导航
+
+- [login](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/auth.py:104)、[AccessSession](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/access_models.py:28):核对 token 返回值与数据库存储字段。
+- [get_ticket](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/api.py:202)、[authenticate](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/auth.py:50)、[authenticate_hash](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/auth.py:66):追踪请求身份进入 `AuthContext` 的过程。
+- [Tenant](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/models.py:10)、[Principal](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/access_models.py:10)、[StoreGrant](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/access_models.py:42):核对租户外键、账户类型和权限作用域。
+- [find](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:74)、[access](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:82)、[authorize_store](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/auth.py:90)、[ticket_view](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:157)。
+- [logout](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/identity/auth.py:196):同样调用 `authenticate`,然后将当前 `AccessSession.revoked` 设为 `True`。
+
+## 图 2:后台 Agent 运行重新验证身份
+
+业务 API 在 `queue_run` 中保存请求身份的 `token_hash`。后台工作进程领取数据库中的任务,再调用编译后的 LangGraph。`build_graph` 注册节点,`graph.ainvoke` 触发节点执行;图中分别标记注册关系和框架调用关系。
+
+```mermaid
+flowchart TB
+    API["modules/agent_runtime/api.py<br/>enqueue<br/>认证、店铺授权与幂等检查"]
+    QUEUE["modules/agent_runtime/service.py<br/>queue_run<br/>保存身份与运行配置"]
+    RUN[("modules/agent_runtime/models.py<br/>AgentRun<br/>身份摘要、状态与租约")]
+    WORKER["modules/agent_runtime/worker.py<br/>execute_one<br/>领取并执行一次运行"]
+    CLAIM["modules/agent_runtime/worker.py<br/>claim<br/>领取任务并增加 lease_token"]
+    BUILD["modules/agent_runtime/runtime.py<br/>build_graph<br/>注册节点并 compile"]
+    INVOKE["modules/agent_runtime/worker.py<br/>execute_one.invoke<br/>调用 graph.ainvoke"]
+    LOAD["modules/agent_runtime/runtime.py<br/>build_graph.load<br/>读取持久 next_node"]
+    MODEL["modules/agent_runtime/runtime.py<br/>build_graph.model<br/>模型请求前后复验"]
+    LOCKED["modules/agent_runtime/runtime.py<br/>locked_run<br/>身份、控制权、租约与期限检查"]
+    AUTH["modules/identity/auth.py<br/>authenticate_hash<br/>重新验证持久会话"]
+    STORE["modules/identity/auth.py<br/>authorize_store<br/>重新检查 orders:read"]
+    ACCESS[("modules/identity/access_models.py<br/>AccessSession / StoreGrant<br/>当前会话与权限记录")]
+    API --> QUEUE
+    QUEUE -.->|API 事务提交| RUN
+    WORKER --> CLAIM
+    RUN -.->|跨进程数据库领取| CLAIM
+    WORKER -->|领取成功后| BUILD
+    WORKER --> INVOKE
+    BUILD -.->|注册 load 节点| LOAD
+    BUILD -.->|注册 model 节点| MODEL
+    INVOKE -->|LangGraph 框架调用 START 后的 load| LOAD
+    INVOKE -->|LangGraph 框架调用;条件路由 next_node=model| MODEL
+    LOAD --> LOCKED
+    MODEL -->|模型请求前及保存响应前| LOCKED
+    RUN -.->|读取身份摘要并锁定当前运行| LOCKED
+    LOCKED --> AUTH
+    LOCKED -->|核对运行身份、控制权和租约后| STORE
+    ACCESS -.->|读取当前状态| AUTH
+    ACCESS -.->|读取当前权限| STORE
+```
+
+图中选择 `load` 和 `model` 展示复验时机。工具子图的 `execute` 与 `followup` 节点也调用 `locked_run`。`locked_run` 检查 token 对应身份与 `AgentRun` 一致;有关联会话时检查 `control_owner`、`control_version` 和 `active_run_id`;随后确认 `RUNNING` 状态、有效租约、执行期限、图版本及店铺授权。
+
+模型请求结束后先独立保存已经发生的用量,再复验身份与控制权并提交响应。身份失效等异常由 `execute_one` 记录到当前运行;它只在自己仍持有对应租约时修改运行状态。
+
+### 图 2 源码导航
+
+- [enqueue](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/api.py:60)、[queue_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/service.py:20)、[AgentRun](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/models.py:10):核对 `tenant_id`、`actor_id`、`store_id`、`token_hash` 的保存来源。
+- [execute_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:137)、[claim](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:28)、[execute_one.invoke](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:165)。
+- [build_graph](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:244)、[load](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:247)、[model](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:284)、[节点注册与条件路由](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:412)。
+- [locked_run](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:47)、[工具子图 execute](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:96)、[followup](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:252)。
+- [履约 prepare](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:85) 直接读取 `Tenant`、`Store`、`Principal`、`StoreGrant`,重新检查审批员工的当前权限。该业务事件路径见[路线 03 的图 2](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/docs/reading-routes/03-after-sales.md)。
+
+## 图 3:平台员工的独立身份
+
+平台登录入口使用 `PlatformAccount` 和 `PlatformSession`。平台请求的 `authenticate` 在平台会话表中寻找凭据,并检查账户启用状态及 `platform:manage`。
+
+```mermaid
+flowchart TB
+    HTTP["FastAPI<br/>/openapi/v1/platform/auth"]
+    LOGIN["modules/platform/auth.py<br/>login<br/>平台账户密码验证与限流"]
+    ACCOUNT[("modules/platform/models.py<br/>PlatformAccount<br/>平台账户与 permissions")]
+    SESSION[("modules/platform/models.py<br/>PlatformSession<br/>平台 token 摘要与有效期")]
+    ME["modules/platform/auth.py<br/>me<br/>查询当前平台身份"]
+    LOGOUT["modules/platform/auth.py<br/>logout<br/>撤销当前平台会话"]
+    AUTH["modules/platform/auth.py<br/>authenticate<br/>检查平台会话与 platform:manage"]
+    HTTP -->|框架调用 POST /sessions| LOGIN
+    HTTP -->|框架调用 GET /me| ME
+    HTTP -->|框架调用 DELETE /sessions/current| LOGOUT
+    ACCOUNT -.->|查询密码摘要并重新核对状态和权限| LOGIN
+    LOGIN -.->|事务保存独立会话| SESSION
+    ME --> AUTH
+    LOGOUT --> AUTH
+    SESSION -.->|读取并重新刷新| AUTH
+    ACCOUNT -.->|锁定账户并校验权限| AUTH
+    LOGOUT -.->|认证成功后设置 revoked=True| SESSION
+```
+
+平台登录成功后同样返回 3600 秒 token。`authenticate` 通过 `PlatformSession.account_id` 找到平台账户,锁定账户后再次刷新会话;随后检查当前 `permissions` 是否包含 `platform:manage`。`me` 返回 `kind=PLATFORM`,退出操作仅撤销本次平台会话。
+
+### 图 3 源码导航
+
+- [平台 login](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/auth.py:44)、[平台 authenticate](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/auth.py:20)。
+- [PlatformAccount](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/models.py:10)、[PlatformSession](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/models.py:23)。
+- [平台 me](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/auth.py:91)、[平台 logout](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/auth.py:106)。
+
+## 关键数据与状态
+
+- `Principal.kind` 包含 `CUSTOMER`、`STAFF`、`SERVICE`;客服、主管和商家管理员的具体职责通过店铺权限表达。可阅读 [ROLE_PERMISSIONS 与 management_access](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/merchant/service.py:22) 理解管理入口的身份检查。
+- `StoreGrant` 的四个主键字段共同限定权限。资源读取同时受 `tenant_id`、店铺权限和客户归属约束。
+- `AccessSession` 保存 `token_hash`、`tenant_id`、`principal_id`、`expires_at`、`revoked`。`authenticate_hash` 从当前记录构建 `AuthContext`。
+- 租户和账户需要处于 `ACTIVE`。有效 token 对应的账户或租户停用后,后续身份检查返回 401;业务资源授权不足时,相关入口返回 404。
+- `AgentRun` 保存原始身份摘要和执行控制信息。`lease_token` 与 `lease_until` 限制工作进程提交权;关联会话的控制版本限制会话接管后的继续执行。
+- `PlatformAccount` 和 `PlatformSession` 管理平台员工身份;平台权限通过 `PlatformAccount.permissions` 检查。
+
+## 阅读检查问题
+
+1. 查询工单时,租户范围、店铺权限和客户本人归属分别在哪个函数检查?这些检查需要哪些字段?
+2. Agent 任务已经进入队列之后,客户退出登录或客服接管会话,哪些检查会阻止工作进程继续保存响应?模型用量为什么另行保存?
+3. 平台员工 token 在哪张表中验证?撤销平台会话与撤销客户会话分别修改什么记录?
+
+## 相关测试阅读入口
+
+本路线只阅读测试代码,未执行下列用例。
+
+- [test_confirmation_approval_and_isolation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_after_sales.py:298):客户确认、审批身份与租户访问范围。
+- [test_permission_revocation_and_logout](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_after_sales.py:392):撤销店铺权限与退出当前会话后的响应。
+- [test_tool_cannot_read_another_customer_order](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_agent_runtime.py:93)、[test_revoked_session_stops_tool_execution](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_agent_runtime.py:106):后台工具访问客户订单及会话撤销检查。
+- [test_revoked_approval_permission_prevents_external_operation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_fulfillment.py:302):履约准备阶段读取当前审批权限。
+- [test_platform_identity_quota_and_audit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_platform_management.py:92):平台独立会话检查及租户停用后的身份验证。

+ 141 - 0
docs/reading-routes/06-capacity-billing.md

@@ -0,0 +1,141 @@
+# 路线 6:模型资源与费用
+
+代码依据:2026-09-30 当前工作区。学习场景:多个商家的咨询和记忆提取同时等待模型,平台需要分配并发额度,并保存每次调用的 token 与人民币费用。
+
+图中的后端路径相对 `src/zhifu/`。连续箭头表示当前进程内的调用或标明条件的执行步骤;间断箭头标注框架回调、网络请求或持久记录之间的联系。框架负责调用 `BudgetCallback`,阅读时注意调用来源。返回[路线目录](README.md)。
+
+## 图 6A:工作进程申请并发额度
+
+```mermaid
+flowchart TD
+    A1["6A-1 modules/agent_runtime/worker.py<br/>main → execute_one<br/>开始一次任务处理"]
+    A2["6A-2 modules/agent_runtime/business_wait.py<br/>reconcile<br/>检查业务等待事件"]
+    A3["6A-3 modules/agent_runtime/worker.py<br/>claim<br/>开始领取事务"]
+    A4["6A-4 modules/agent_runtime/capacity.py<br/>reservations<br/>锁定并发策略并读取商家预留"]
+    A5["6A-5 modules/agent_runtime/worker.py<br/>claim 内部<br/>选择租户、重新统计并发、锁定运行"]
+    A6["6A-6 modules/agent_runtime/capacity.py<br/>acquire<br/>检查全局、交互、后台与租户预留"]
+    A7["Redis state<br/>CAPACITY_SCRIPT 原子更新有效额度"]
+    A8["6A-8 modules/agent_runtime/worker.py<br/>claim 内部<br/>RUNNING、lease_token、lease_until"]
+    A9["6A-9 modules/agent_runtime/runtime.py<br/>build_graph<br/>由 execute_one 调用 graph.ainvoke"]
+    A10["6A-10 modules/agent_runtime/capacity.py<br/>release<br/>execute_one 的 finally 释放额度"]
+    A11["本次没有取得任务<br/>execute_one 返回 False"]
+    A1 --> A2
+    A1 -->|reconcile 返回后| A3
+    A3 --> A4
+    A4 -->|返回预留配置,继续 claim| A5
+    A5 -->|存在有效候选| A6
+    A5 -->|没有候选或租户已满| A11
+    A6 -.->|Redis eval| A7
+    A7 -.->|取得额度| A8
+    A7 -.->|额度不足| A11
+    A8 -->|领取事务提交后返回运行与租约| A9
+    A9 -->|成功、失败或取消后的 finally| A10
+    A11 -->|finally| A10
+```
+
+源码导航:
+
+- 6A-1、6A-3、6A-5、6A-8:[worker.main](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:210)、[execute_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:137)、[claim](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:28)。关注 `TenantDispatch.selected_at`、`TenantQuota` 与 `AgentRun.lease_token`。
+- 6A-2:[business_wait.reconcile](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/business_wait.py),具体业务恢复见[路线 2](02-agent-runtime.md)。
+- 6A-4、6A-6、6A-10:[reservations](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/capacity.py:62)、[acquire](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/capacity.py:75)、[release](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/capacity.py:110)。沿 `state.eval` 阅读同文件的 `CAPACITY_SCRIPT` 与 `RELEASE_SCRIPT`。
+- 6A-9:[build_graph](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:244)。额度取得之后才进入图执行。
+
+租户调度优先照顾尚未用完预留额度的商家,随后参考上次领取时间;租户内部按照创建时间与运行标识选择任务。`claim` 取得租户锁后重新检查并发数量。Redis 将全局、交互、后台和租户预留放在一次 Lua 调用内判断。
+
+记忆任务从 [memory.worker.execute_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/memory/worker.py:205) 进入,同样调用 `reservations` 和 `acquire`,使用 `background` 分类;只有成功取得额度才进入 `execute_claimed`。取得的额度在 `finally` 释放。Redis 额度还有有效期限,工作进程中断后的记录由后续申请清理。
+
+## 图 6B:一次模型调用的预占与结算
+
+```mermaid
+flowchart TD
+    B1["6B-1 modules/agent_runtime/runtime.py<br/>build_graph 内部 model<br/>保存 ModelAttempt,调用 ainvoke"]
+    B2["6B-2 modules/agent_runtime/budget_callback.py<br/>BudgetCallback.on_chat_model_start<br/>估算输入与最大输出"]
+    B3["6B-3 modules/agent_runtime/budget.py<br/>reserve 内部依次调用 lock_tenant、account<br/>保存 token 预占"]
+    B4["6B-4 modules/billing/ledger.py<br/>reserve_money<br/>按已批准价格保存金额预占"]
+    B5["供应商模型服务<br/>实际请求与 usage"]
+    B6["6B-6 modules/agent_runtime/budget_callback.py<br/>BudgetCallback.on_llm_end<br/>读取 usage_metadata"]
+    B7["6B-7 modules/agent_runtime/budget_callback.py<br/>BudgetCallback.on_llm_error<br/>记录未知用量"]
+    B8["6B-8 modules/agent_runtime/budget.py<br/>change_entry<br/>结算 token 或保留 UNKNOWN"]
+    B9["6B-9 modules/billing/ledger.py<br/>settle_money → charge<br/>结算金额或保留 UNKNOWN"]
+    B10["6B-10 modules/agent_runtime/runtime.py<br/>model 返回处理<br/>独立保存 ModelAttempt.usage,继续业务检查"]
+    B11["6B-11 modules/agent_runtime/budget.py<br/>mark_pending_unknown<br/>取消或恢复时检查未结算尝试"]
+    B12["预占事务终止<br/>预算错误向上传播"]
+    B1 -.->|LangChain 请求前回调| B2
+    B2 --> B3 --> B4
+    B4 -.->|预占事务成功后,SDK 发送 HTTP| B5
+    B3 -->|token 不足| B12
+    B4 -->|金额不足或缺少已批准价格| B12
+    B5 -.->|LangChain 成功回调| B6
+    B5 -.->|LangChain 错误回调| B7
+    B6 --> B8
+    B7 --> B8
+    B8 --> B9
+    B9 -->|成功回调结束,ainvoke 返回| B10
+    B1 -->|外层执行被取消或发生异常| B11
+    B11 --> B8
+```
+
+源码导航:
+
+- 6B-1、6B-10:[build_graph.model](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:284)。寻找 `attempt_id`、`callbacks`、`max_retries=0` 与返回后的独立事务。
+- 6B-2、6B-6、6B-7:[on_chat_model_start](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget_callback.py:49)、[on_llm_end](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget_callback.py:74)、[on_llm_error](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget_callback.py:82)。`raise_error` 与 `run_inline` 让预算检查在发送之前完成。
+- 6B-3、6B-8、6B-11:[reserve](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget.py:70)、[change_entry](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget.py:106)、[mark_pending_unknown](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget.py:168)。
+- 6B-4、6B-9:[reserve_money](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/ledger.py:46)、[settle_money](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/ledger.py:77)、[charge](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/ledger.py:32)。
+
+`reserve` 使用独立事务,在租户锁保护下同时处理 token 与金额。存在 `MoneyPolicy` 时才启用人民币预算;已启用金额预算却没有对应 `BillingRoute` 会拒绝调用。价格保存在每次 `MoneyEntry.price` 中,结算读取该次调用的价格快照。
+
+输入预估使用序列化字节数与协议余量,输出使用配置上限。实际结算使用供应商 `usage_metadata`,缓存 token 单独参与金额计算。金额使用整数微元,每次请求汇总后向上取整。
+
+没有 usage 时保留预占并登记 `UNKNOWN`。`change_entry` 对重复的相同 usage 返回原状态;不同 usage 产生冲突。人工释放后收到有效迟到 usage,仍然记录已经发生的消耗。模型用量的事务先完成,之后的业务权限复验仍可能拒绝答复提交。
+
+## 图 6C:账单差异与费用审批
+
+```mermaid
+flowchart TD
+    C1["6C-1 modules/billing/api.py<br/>import_statement<br/>授权并检查导入幂等标识"]
+    C2["6C-2 modules/billing/statements.py<br/>compare<br/>按费用记录比较金额、token 和模型"]
+    C3["BillingStatement<br/>保存账单,返回比较结果"]
+    C4["6C-4 modules/billing/api.py<br/>submit<br/>员工提交 RECONCILE 申请"]
+    C5["6C-5 modules/billing/statements.py<br/>validate_evidence<br/>核对账单明细与申请"]
+    C6["BillingChange<br/>PENDING,保存申请人与原目标版本"]
+    C7["6C-7 modules/billing/api.py<br/>decide<br/>另一员工审批并复验权限、版本及期限"]
+    C8["6C-8 modules/billing/api.py<br/>apply_change → validate_evidence<br/>再次核对证据"]
+    C9["6C-9 modules/billing/ledger.py<br/>reconcile<br/>更新费用、账户与审计"]
+    C10["BillingChange<br/>APPROVED 或 REJECTED"]
+    C1 --> C2 -->|比较通过后继续 import_statement| C3
+    C3 -.->|员工查看差异,另一次 HTTP 提交| C4
+    C4 --> C5 -->|返回后保存申请| C6
+    C6 -.->|审批员工发起独立 HTTP 请求| C7
+    C7 -->|批准| C8
+    C8 -->|证据校验成功后| C9
+    C9 -->|返回 decide,同一事务保存审批结果| C10
+    C7 -->|拒绝| C10
+```
+
+源码导航:
+
+- 6C-1、6C-4、6C-7、6C-8:[import_statement](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/api.py:371)、[submit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/api.py:74)、[decide](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/api.py:191)、[apply_change](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/api.py:136)。每个入口都经由 [authorize](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/api.py:46) 检查对应权限。
+- 6C-2、6C-5:[compare](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/statements.py:28)、[validate_evidence](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/statements.py:87)。
+- 6C-9:[ledger.reconcile](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/ledger.py:113)。预算和价格申请也通过 `submit`、`decide`,分别进入 `apply_change` 的 `BUDGET` 与 `PRICE` 分支。
+
+账单比较产生 `MATCHED`、`DIFFERENT`、`UNKNOWN`、`UNLINKED` 或 `ROUTE_MISMATCH`,比较操作只返回差异。费用变更由独立审批请求触发,申请人不能审批自己的申请。批准操作、费用调整和审计记录共用事务。
+
+## 关键数据与状态
+
+- [AgentRun、ModelAttempt、TokenAccount、TokenEntry](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/models.py):运行可以包含多次模型尝试。`ModelAttempt.id` 传给回调作为 `reference_id`;LangChain 回调的 `run_id.bytes` 用作 `TokenEntry.id` 和 `MoneyEntry.id`。
+- [MoneyPolicy、BillingRoute、MoneyAccount、MoneyEntry](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/billing/models.py):分别记录金额预算、已批准价格、当日账户和单次调用金额。
+- 并发额度在 Redis 记录,同时数据库保存任务租约;token 与金额账户分别记录 `reserved` 和 `consumed`。并发额度释放与费用结算具有各自的记录和条件。
+
+## 阅读检查
+
+1. 在 `claim` 中找到租户选择、重新统计并发和 Redis 申请的位置,解释不同锁与额度检查分别保护什么。
+2. 从 `ModelAttempt.id` 跟踪到 `TokenEntry.reference_id`,说明一次运行、多次尝试和回调标识之间的关系。
+3. 沿 `UNKNOWN`、重复 usage、迟到 usage 和人工审批四种情况,指出费用记录怎样变化,以及每次变化所属的事务。
+
+## 测试阅读入口
+
+以下文件用于追踪预期行为;本文编写时只阅读用例,没有执行其中的业务请求。
+
+- [test_tenant_dispatch.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_tenant_dispatch.py)、[test_capacity_quotas.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_capacity_quotas.py):租户调度与预留额度。
+- [test_token_budget.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_token_budget.py)、[test_interrupted_model_budget.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_interrupted_model_budget.py):预算、未知用量与中断处理。
+- [test_money_budget.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_money_budget.py)、[test_billing_statements.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_billing_statements.py):人民币费用、账单与双人审批。

+ 168 - 0
docs/reading-routes/07-channels-messaging.md

@@ -0,0 +1,168 @@
+# 路线 7:外部渠道与可靠消息
+
+代码依据:2026-09-30 当前工作区。学习场景:外部渠道发送客户咨询或确认事件;平台保存业务结果,再向 OMS 和原渠道传递事件。
+
+图中的后端路径相对 `src/zhifu/`。连续箭头表示当前进程内的调用或标明条件的执行步骤;间断箭头说明 HTTP、消息队列、数据库记录与后续请求的连接。节点中的 `内部` 表示该函数内的逻辑。返回[路线目录](README.md)。
+
+## 图 7A:签名验证、身份关联与入站去重
+
+```mermaid
+flowchart TD
+    A0["外部渠道<br/>客户消息、补充资料或方案确认"]
+    A1["7A-1 modules/channels/api.py<br/>receive<br/>HMAC 入站入口"]
+    A2["7A-2 modules/channels/auth.py<br/>authenticate_channel<br/>原始正文、绑定、时间窗、签名与 nonce"]
+    A3["7A-3 modules/channels/standard.py<br/>receive_standard → authenticate_standard<br/>由 Standard Webhooks SDK 验签"]
+    A4["7A-4 modules/channels/standard_schemas.py<br/>to_envelope<br/>转换业务事件"]
+    A5["7A-5 modules/channels/service.py<br/>accept_envelope<br/>检查 scope、ChannelIdentity 与当前客户权限"]
+    A6{"同一 external_message_id<br/>已经保存 ChannelInbox?"}
+    A7["accept_envelope 内部<br/>同客户、同内容返回原收据<br/>内容冲突终止请求"]
+    A8["accept_envelope 内部<br/>建立内部 AccessSession,取得或创建渠道会话"]
+    A9["7A-9 modules/conversations/api.py<br/>accept_message<br/>保存消息;AGENT 模式同时创建 AgentRun"]
+    A10["7A-10 modules/agent_runtime/inputs.py<br/>accept_input<br/>将补充资料交给原等待运行"]
+    A11["7A-11 modules/after_sales/service.py<br/>AfterSalesService.execute → confirm<br/>登记客户确认"]
+    A12["accept_envelope 返回<br/>ChannelInbox 与业务变更共用事务<br/>路由提交后返回 HTTP 202"]
+    A13["Agent worker<br/>后续读取已提交的运行"]
+    A14["ConversationEvent<br/>HUMAN 模式消息供客服读取<br/>接管与回复见路线 1"]
+    A15["售后确认记录与 BusinessEvent<br/>业务等待的后续核对见路线 2"]
+    A0 -.->|HMAC HTTP| A1
+    A1 --> A2
+    A2 -->|验签返回后,receive 调用| A5
+    A0 -.->|Standard Webhooks HTTP| A3
+    A3 -->|验签成功| A4
+    A4 -->|receive_standard 调用| A5
+    A5 --> A6
+    A6 -->|已经保存| A7
+    A6 -->|首次受理| A8
+    A8 -->|message| A9
+    A8 -->|clarification| A10
+    A8 -->|proposal_confirmation| A11
+    A9 --> A12
+    A10 --> A12
+    A11 --> A12
+    A12 -.->|AGENT 模式新任务或补充输入恢复任务| A13
+    A12 -.->|HUMAN 模式消息,run_id 为 null| A14
+    A12 -.->|方案确认事件| A15
+```
+
+源码导航:
+
+- 7A-1、7A-2:[receive](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/api.py:26)、[authenticate_channel](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/auth.py:15)。继续阅读 [raw_request](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/auth.py:51) 与 [load_binding](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/auth.py:58)。
+- 7A-3、7A-4:[receive_standard](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/standard.py:75)、[authenticate_standard](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/standard.py:40)、[to_envelope](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/standard_schemas.py:60)。
+- 7A-5 至 7A-8、7A-12:[accept_envelope](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/service.py:20)。寻找权限检查与 `ChannelInbox` 查询的先后顺序,以及内部 `AccessSession` 的用途。
+- 7A-9:[accept_message](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/api.py),会话处理见[路线 1](01-conversation.md)。
+- 7A-10:[accept_input](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/inputs.py),运行恢复见[路线 2](02-agent-runtime.md)。
+- 7A-11:[AfterSalesService.execute](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:199),客户确认与主管审批见[路线 3](03-after-sales.md)。
+
+两种协议都先验证当前绑定,再由已登记的 `ChannelIdentity` 找到客户。HMAC 使用时间窗和 `ChannelNonce` 防止同一签名重复使用;业务重复发送使用原 `external_message_id`、原内容和新 nonce。Standard Webhooks 使用 `webhook-id` 作为业务消息标识,交给同一 Inbox 流程去重。
+
+客户身份、店铺权限、业务状态和幂等收据在当前请求事务中检查。普通消息、补充资料和方案确认分别进入对应业务函数。客户消息处于 `AGENT` 模式时创建运行,`HUMAN` 模式只保存消息并返回 `run_id=null`。HTTP 202 表示已经受理并提交记录;运行与履约由各自后续流程推进。
+
+## 图 7B:业务事件、RabbitMQ 与消费收据
+
+```mermaid
+flowchart TD
+    B1["7B-1 modules/after_sales/service.py<br/>AfterSalesService.decide<br/>批准售后申请"]
+    B2["7B-2 modules/after_sales/service.py<br/>changed → emit<br/>业务修改与 BusinessEvent 共用事务"]
+    B3["MySQL BusinessEvent<br/>event_type=after_sale.approved<br/>delivery_status=PENDING"]
+    B4["7B-4 modules/integrations/worker.py<br/>main 内部 publisher → publish_one<br/>领取 Outbox 事件租约"]
+    B5["7B-5 modules/integrations/worker.py<br/>Broker.publish_event<br/>EventEnvelope 与持久消息"]
+    B6["RabbitMQ<br/>发布确认与持久队列"]
+    B7["publish_one 返回处理<br/>验证原租约后登记 SENT"]
+    B8["7B-8 modules/integrations/worker.py<br/>main 内部 consumer → consume_one<br/>读取消息并校验 schema_version"]
+    B9["7B-9 modules/integrations/fulfillment.py<br/>prepare_event<br/>检查 ConsumerReceipt,按事件准备业务"]
+    B10["7B-10 modules/integrations/fulfillment.py<br/>prepare<br/>复验审批、订单、规则与数量"]
+    B11["同一事务<br/>ConsumerReceipt + ExternalOperation + Reservation<br/>有效批准事件进入履约准备"]
+    B12["consume_one 的 message.process 退出<br/>事务成功后确认消息<br/>异常请求重新投递"]
+    B13["7B-13 modules/integrations/fulfillment.py<br/>execute_due<br/>executor 读取已准备的操作"]
+    B1 -->|批准分支| B2
+    B2 -.->|调用方事务提交| B3
+    B3 -.->|publisher 查询数据库| B4
+    B4 --> B5
+    B5 -.->|AMQP 发布| B6
+    B6 -.->|确认返回| B7
+    B6 -.->|consumer 读取| B8
+    B8 --> B9
+    B9 -->|批准事件且没有消费收据| B10
+    B10 -->|验证成功,返回后保存收据| B11
+    B9 -->|已有收据,结束本次业务处理| B12
+    B11 -->|提交后返回 consume_one| B12
+    B11 -.->|独立 executor 查询数据库| B13
+```
+
+源码导航:
+
+- 7B-1、7B-2:[AfterSalesService.decide](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:504)、[changed](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:448)、[emit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:141)。
+- 7B-4、7B-5、7B-8:[main](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:117)、[publish_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:78)、[Broker.publish_event](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:54)、[consume_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/worker.py:104)。`main` 通过 `TaskGroup` 分别运行 publisher、consumer 和 executor。
+- 7B-9、7B-10、7B-13:[prepare_event](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:46)、[prepare](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:85)、[execute_due](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:216)。OMS 网络调用与结果查询继续阅读[路线 3](03-after-sales.md)。
+
+`BusinessEvent` 同时承担 Outbox 记录。`publish_one` 先领取租约,在网络发布成功后用原租约检查提交 `SENT`。发布成功后进程中断,事件可能再次发送;`ConsumerReceipt` 使重复消费能够读取已经处理的结果。
+
+`consume_one` 按 `event_id` 回到数据库读取业务事实。`prepare_event` 在适用的批准事件中查询 OMS 订单,再进入事务检查收据和最新业务状态。过期事件、已经处理的事件与当前有效事件具有不同分支;图 7B 的 `ExternalOperation` 与数量预占属于有效批准事件。
+
+发布者保存 `SENT` 与消费者保存收据由各自任务调度,没有固定先后关系。运维核对入口为 [message_recovery.reconcile](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/operations/message_recovery.py:15):检查审批事件的发送状态、有效租约与消费收据;提交操作人和原因后,仅对 `SENT_WITHOUT_RECEIPT` 重新登记 `PENDING`,保留原 `event_id`。
+
+## 图 7C:业务结果返回外部渠道
+
+```mermaid
+flowchart TD
+    C1["7C-1 modules/conversations/service.py<br/>emit<br/>webhook 会话保存指定回复与控制事件"]
+    C2["7C-2 modules/after_sales/service.py<br/>emit<br/>保存售后业务事件"]
+    C3["MySQL BusinessEvent<br/>conversation.* 或 after_sale.*"]
+    C4["7C-4 modules/platform/delivery.py<br/>main → enqueue<br/>按渠道 scope 查询事件"]
+    C5["MySQL ChannelDelivery<br/>唯一事件投递记录,QUEUED"]
+    C6["7C-6 modules/platform/delivery.py<br/>execute_one<br/>检查前序投递、当前授权及租约"]
+    C7["execute_one 内部<br/>会话事件复验 ChannelIdentity 与 control_version"]
+    C8["execute_one 内部<br/>提交 SENDING、attempts、lease_token<br/>准备 HMAC 请求"]
+    C9["渠道 callback_url<br/>httpx.AsyncClient.post"]
+    C10["execute_one 返回处理<br/>原租约有效,2xx 登记 SENT"]
+    C11["execute_one 返回处理<br/>错误登记 QUEUED 或 FAILED"]
+    C12["7C-12 modules/platform/api.py<br/>retry<br/>平台员工检查版本并重新排队"]
+    C1 -.->|事务提交| C3
+    C2 -.->|事务提交| C3
+    C3 -.->|数据库扫描| C4
+    C4 -.->|保存投递记录| C5
+    C5 -.->|main 随后调用 execute_one 领取| C6
+    C6 -->|会话事件| C7
+    C6 -->|工单事件| C8
+    C7 -->|身份及控制版本有效| C8
+    C8 -.->|签名 HTTP POST| C9
+    C9 -.->|2xx| C10
+    C9 -.->|网络错误或非 2xx| C11
+    C11 -.->|QUEUED 到达 next_attempt_at 后读取| C6
+    C11 -.->|FAILED 后员工发起独立请求| C12
+    C12 -.->|保存 QUEUED,等待后续领取| C6
+```
+
+源码导航:
+
+- 7C-1:[conversations.service.emit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/conversations/service.py:31)。只有 webhook 会话中的指定事件会同时创建渠道用 `BusinessEvent`。
+- 7C-2:[AfterSalesService.emit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/service.py:141),履约更新还使用 [integrations.fulfillment.emit](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/fulfillment.py:28)。
+- 7C-4、7C-6 至 7C-11:[delivery.main](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/delivery.py:202)、[enqueue](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/delivery.py:28)、[execute_one](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/delivery.py:75)。
+- 7C-12:[platform.api.retry](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/api.py:164)。沿 `service.command` 继续阅读平台操作的幂等控制。
+
+渠道投递工作进程直接扫描数据库的业务事件;它与 RabbitMQ 履约消费各自执行。相同会话或工单存在较早版本的 `QUEUED`、`SENDING` 投递时,后续版本暂时不能取得发送租约。
+
+每组三次自动发送,失败后登记 `FAILED`;平台员工可以提交独立重试请求。每次 HTTP 使用 5 秒客户端超时,外层期限为 6 秒。会话身份映射或控制版本变化、渠道停用、缺少签名凭据也会进入失败处理。图 7C 展示成功授权后的发送主线。
+
+发送受理在取得会话锁时登记;其后发生的接管不能撤销已经受理的 HTTP 请求。返回后仍需核对 `lease_token` 与 `SENDING`,迟到结果只能提交到原租约仍然有效的投递记录。
+
+## 关键数据与状态
+
+- [ChannelKey、ChannelNonce、ChannelInbox](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/channels/models.py):签名密钥、签名重复使用检查、业务去重收据。
+- [BusinessEvent](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/after_sales/models.py):业务版本、Outbox 发送状态与租约。
+- [ConsumerReceipt、ExternalOperation、Reservation](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/integrations/models.py):消息已经处理的证据、外部操作与数量预占。
+- [ChannelDelivery](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/platform/models.py:72):`QUEUED`、`SENDING`、`SENT`、`FAILED`,以及发送次数、版本与租约。
+
+## 阅读检查
+
+1. 找到入站签名标识、业务消息标识与内部命令幂等键,分别说明重复发送时哪些值保持相同、哪些值重新生成。
+2. 从审批事务追踪 `BusinessEvent`、发布确认与 `ConsumerReceipt`,解释在各个位置发生进程中断时,后续任务怎样判断是否继续执行。
+3. 跟踪一条 `conversation.message.assistant` 事件到 `ChannelDelivery`,说明人工接管前后哪些检查能够改变发送结果,以及发送受理之后的边界。
+
+## 测试阅读入口
+
+以下文件用于追踪预期行为;本文编写时只阅读用例,没有执行其中的业务请求。
+
+- [test_channel_inbox.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_channel_inbox.py)、[test_standard_webhooks.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_standard_webhooks.py):入站签名、身份与去重。
+- [test_fulfillment.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_fulfillment.py)、[message_recovery_acceptance.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/message_recovery_acceptance.py):履约收据、消息恢复与重复处理。
+- [test_channel_acceptance.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_channel_acceptance.py):渠道业务与结果投递。

+ 176 - 0
docs/reading-routes/08-operations-evaluation.md

@@ -0,0 +1,176 @@
+# 路线 08:运行监控与评测证据
+
+## 学习场景
+
+部署后的服务需要持续回答三个问题:依赖是否可用,业务任务是否继续推进,已有回答与知识检索是否满足评测条件。本路线沿应用启动、工作进程心跳、指标采集和两个评测脚本,追踪这些结论所使用的代码与数据。
+
+图中后端路径相对 `src/zhifu/`;`frontend/`、`scripts/`、`ops/` 等路径相对仓库根目录。连续箭头表示函数直接调用;间断箭头注明框架调度、HTTP、跨进程或者持久数据传递。配置、依赖框架和数据节点单独标明。返回[路线目录](README.md)。
+
+## 图 1:服务启动、健康检查与任务监控
+
+```mermaid
+flowchart TD
+    BOOT["Dockerfile / compose.runtime.yaml<br/>配置:Uvicorn 与工作进程入口"]
+    APP["app.py<br/>create_app()<br/>注册路由、生命周期和指标"]
+    LIFE["app.py<br/>lifespan()<br/>创建客户端并检查依赖"]
+    READY["app.py<br/>ready() / database_probe()<br/>检查 MySQL 与两套 Redis"]
+    HTTP_METRICS["core/telemetry.py<br/>instrument()<br/>注册 HTTP 计数与耗时采集"]
+    HTTP_HANDLER["依赖 Instrumentator<br/>注册的 /metrics 处理函数<br/>提供当前进程 HTTP 指标"]
+    RUN["operations/worker.py<br/>run(role) → run_monitored()<br/>运行指定工作进程"]
+    HEART["core/telemetry.py<br/>heartbeat(role) / loop()<br/>定期更新本进程心跳"]
+    MAIN["modules/agent_runtime/worker.py<br/>main()<br/>以 Agent 工作进程为例"]
+    REDIS[("Redis 数据<br/>monitor:worker:{role}:*<br/>模型并发额度")]
+    DB[("MySQL 数据<br/>AgentRun、MemoryJob<br/>BusinessEvent、ModelAttempt")]
+    MONITOR["monitoring/api.py<br/>lifespan()<br/>创建 exporter 查询客户端"]
+    METRICS["monitoring/api.py<br/>metrics()<br/>查询任务、用量、心跳与额度"]
+    PROM["ops/prometheus/prometheus.yml<br/>配置:Prometheus 定时采集"]
+    ALERT["ops/prometheus/alerts.yml<br/>配置:告警表达式与持续时间"]
+    BOOT -. "Uvicorn factory 调用" .-> APP
+    APP -. "FastAPI 生命周期调用" .-> LIFE
+    APP --> HTTP_METRICS
+    HTTP_METRICS -. "注册依赖提供的路由" .-> HTTP_HANDLER
+    BOOT -. "healthcheck HTTP GET /health/ready" .-> READY
+    BOOT -. "python -m 启动独立进程" .-> RUN
+    RUN -->|进入上下文| HEART
+    RUN -. "role=agent:asyncio.create_task 调度 main" .-> MAIN
+    HEART -. "每 5 秒写入,TTL 为 20 秒" .-> REDIS
+    BOOT -. "exporter 的 FastAPI 生命周期" .-> MONITOR
+    PROM -. "HTTP GET /metrics" .-> METRICS
+    PROM -. "HTTP GET /metrics" .-> HTTP_HANDLER
+    DB -. "采集时查询任务与 token" .-> METRICS
+    REDIS -. "采集时查询心跳与有效额度" .-> METRICS
+    PROM -. "读取规则并计算告警条件" .-> ALERT
+```
+
+API 的 `create_app()` 创建 FastAPI 应用,并调用 `instrument()` 注册 HTTP 指标。Uvicorn 启动应用时,由 FastAPI 进入 `lifespan()`,初始化 MySQL、两套 Redis 与知识检索客户端。`/health/live` 返回进程存活结果,`/health/ready` 查询 MySQL 和两套 Redis,失败返回 `503`。
+
+统一工作进程入口 `run(role)` 通过 `MODULES` 选择模块 `main()`,`run_monitored()` 进入 `heartbeat()`,并通过 `asyncio.create_task()` 调度业务函数与停止等待。停止信号触发后取消业务任务,等待清理;心跳上下文退出时删除本进程的 Redis key。
+
+`monitoring/api.py` 是独立 exporter。`metrics()` 在 8 秒期限内查询待处理任务、最早等待时间、最近 15 分钟记录、token、工作进程心跳与有效模型额度。Prometheus 的 `operations` 采集超时配置为 4 秒。告警规则进一步检查心跳缺失、等待时间、HTTP 错误比例及 Agent 失败数量。
+
+完整发布检查由 `operations/release_check.py:check()` 单独执行:核对两套数据库迁移版本、Redis、RabbitMQ、Milvus 向量维度、一次 embeddings 请求、API 和 OMS 健康接口,以及生成模型配置完整性。
+
+### 图 1 源码导航
+
+1. [create_app()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/app.py:36)、[lifespan()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/app.py:40)、[live() 与 ready()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/app.py:145):区分应用创建、运行期初始化与健康请求。
+2. [run() 与 run_monitored()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/operations/worker.py:49)、[Agent main()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:210)、[heartbeat()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/core/telemetry.py:31):跟踪工作进程入口、心跳和停止处理。
+3. [instrument()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/core/telemetry.py:12)、[exporter lifespan()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/monitoring/api.py:21)、[metrics()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/monitoring/api.py:43):区分 HTTP 指标和任务指标的数据来源。
+4. [Prometheus 采集配置](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/ops/prometheus/prometheus.yml:9)、[告警规则](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/ops/prometheus/alerts.yml:1)、[release_check.check()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/operations/release_check.py:36):核对采集周期、告警条件与发布检查覆盖范围。
+
+## 图 2:Agent 评测清单、执行额度与结果审核
+
+```mermaid
+flowchart TD
+    CLI["scripts/evaluate_agent_runs.py<br/>__main__ 模块入口<br/>解析参数并选择校验或评测"]
+    INPUT["scripts/evaluate_agent_runs.py<br/>load_inputs()<br/>校验资料、源码与审核证据"]
+    VALIDATE["evaluation.py<br/>Dataset / AgentManifest<br/>Pydantic 校验;Review.verify()"]
+    EVAL["scripts/evaluate_agent_runs.py<br/>evaluate()<br/>锁定清单并逐项处理"]
+    INIT["scripts/evaluate_agent_runs.py<br/>initialize_report()<br/>绑定输出文件、额度及期限"]
+    OBSERVE["scripts/evaluate_agent_runs.py<br/>observe()<br/>读取运行、模型尝试与外部操作"]
+    DB[("MySQL 持久数据<br/>AgentRun、ModelAttempt<br/>ExternalOperation")]
+    EXECUTE["modules/agent_runtime/worker.py<br/>execute_one()<br/>限定 run_ids 领取并执行"]
+    GRAPH["modules/agent_runtime/runtime.py<br/>build_graph() / model()<br/>构建运行图及模型请求"]
+    CALLBACK["modules/agent_runtime/budget_callback.py<br/>BudgetCallback.on_chat_model_start()<br/>模型外呼前执行额度检查"]
+    TAKE["scripts/evaluate_agent_runs.py<br/>PersistentAllowance.take()<br/>原子预留次数并保存报告"]
+    REDIS[("Redis 持久数据<br/>evaluation-budget:{manifest_hash}<br/>calls、deadline、output、mode")]
+    JUDGE["evaluation.py<br/>Judgment.model_validate() / verify()<br/>校验结论与证据摘要"]
+    WRITE["evaluation.py<br/>write_report()<br/>写入完整 JSON 报告"]
+    CLI --> INPUT
+    INPUT --> VALIDATE
+    CLI -. "未指定 validate-only:asyncio.run" .-> EVAL
+    EVAL --> INIT
+    INIT -. "读取或登记持久评测状态" .-> REDIS
+    EVAL -->|执行前及适用的执行后| OBSERVE
+    DB -. "读取并核对清单绑定的业务记录" .-> OBSERVE
+    EVAL -->|execute 且运行状态为 QUEUED| EXECUTE
+    EXECUTE -->|调用 build_graph| GRAPH
+    GRAPH -. "LangGraph 调度 model;LangChain 触发模型开始回调" .-> CALLBACK
+    CALLBACK --> TAKE
+    TAKE -. "Redis Lua 检查期限与次数并增加 calls" .-> REDIS
+    TAKE --> WRITE
+    EVAL -->|提供 judgments 或复用已有审核| JUDGE
+    EVAL -->|汇总类别、尝试次数与用量| WRITE
+```
+
+`load_inputs()` 使用 Pydantic 读取数据集和运行清单,核对数据文件摘要、当前后端源码摘要、数据集合及审核证据。`--validate-only` 在完成这些校验后结束。普通评测进入 `evaluate()`,读取已有业务记录,同时维护 Redis 评测记录和本地报告。
+
+`evaluate()` 使用清单摘要作为互斥锁标识。`initialize_report()` 将同一清单绑定到唯一输出文件,恢复时读取原有次数与期限。启用 `--execute` 时还检查 Agent 和 memory 常驻工作进程的心跳;只有本次观测状态为 `QUEUED` 的指定运行会交给 `execute_one()`。
+
+`execute_one()` 调用 `build_graph()`,随后通过 `graph.ainvoke()` 进入 LangGraph。模型调用使用 `BudgetCallback`;LangChain 在模型请求开始前调用 `on_chat_model_start()`,后者调用本次评测传入的 `PersistentAllowance.take()`。次数及总输出上限共同限制可预留请求数,Redis 时间检查总期限,预留次数随报告保存。Agent 内部节点与业务工具继续阅读 [路线 02](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/docs/reading-routes/02-agent-runtime.md)。
+
+`observe()` 核对租户、运行图版本、模型名、发布参数摘要、单次输出上限和初始问题,再读取模型尝试及关联外部操作。存在模型尝试的记录进入 `REVIEW_REQUIRED`;结论文件必须匹配当前 `observation_sha256`。运行仍在 `QUEUED` 或 `RUNNING` 时不能接受该审核结论。`Judgment` 禁止将存在权限越界或重复外部操作的结果登记为 `PASSED`。
+
+### 图 2 源码导航
+
+1. [CLI 入口](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/scripts/evaluate_agent_runs.py:224)、[load_inputs()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/scripts/evaluate_agent_runs.py:82)、[Dataset.validate_isolation()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/evaluation.py:51)、[AgentManifest](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/evaluation.py:106):理解数据集、运行绑定和固定参数。
+2. [evaluate()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/scripts/evaluate_agent_runs.py:139)、[initialize_report()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/scripts/evaluate_agent_runs.py:46)、[PersistentAllowance.take()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/scripts/evaluate_agent_runs.py:26):追踪互斥、报告恢复和持久额度。
+3. [execute_one()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/worker.py:137)、[build_graph()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/runtime.py:244)、[BudgetCallback.on_chat_model_start()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/agent_runtime/budget_callback.py:49):识别直接调用与框架触发的模型回调。
+4. [observe()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/scripts/evaluate_agent_runs.py:106)、[Review.verify()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/evaluation.py:74)、[Judgment.validate_safety()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/evaluation.py:90)、[write_report()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/evaluation.py:24):核对观测、审核和最终报告之间的数据关系。
+
+## 图 3:知识检索评测与 Recall@10
+
+```mermaid
+flowchart TD
+    CLI["scripts/evaluate_retrieval.py<br/>__main__ 模块入口<br/>加载清单与审核资料"]
+    MANIFEST["evaluation.py<br/>RetrievalManifest.model_validate_json()<br/>读取有类型约束的检索清单"]
+    LABELS["evaluation.py<br/>RetrievalManifest.validate_labels()<br/>检查版本、唯一性和标注范围"]
+    REVIEW["evaluation.py<br/>Review.verify()<br/>核对适用的审核证据"]
+    EVAL["scripts/evaluate_retrieval.py<br/>evaluate()<br/>逐项请求并计算召回结果"]
+    API["modules/knowledge/api.py<br/>retrieve()<br/>认证与店铺读取权限检查"]
+    SEARCH["modules/knowledge/vector.py<br/>RetrievalClient.search()<br/>获取混合检索候选"]
+    EMBED["modules/knowledge/vector.py<br/>RetrievalClient.embeddings()<br/>请求查询向量"]
+    VECTOR[("独立依赖服务<br/>BGE embeddings / Milvus 索引")]
+    VERIFY["modules/knowledge/api.py<br/>verified_results()<br/>按当前发布版本过滤候选"]
+    DB[("MySQL 持久数据<br/>知识文档、发布版本和访问权限")]
+    WRITE["evaluation.py<br/>write_report()<br/>保存每项结果与最终状态"]
+    CLI --> MANIFEST
+    MANIFEST -. "Pydantic model_validator 调用" .-> LABELS
+    CLI -->|清单包含审核资料时| REVIEW
+    CLI -. "asyncio.run" .-> EVAL
+    EVAL -. "HTTP POST knowledge/search;limit=10;FastAPI 路由" .-> API
+    API --> SEARCH
+    SEARCH --> EMBED
+    EMBED -. "HTTP POST /embeddings" .-> VECTOR
+    SEARCH -. "Milvus 客户端 hybrid_search" .-> VECTOR
+    API -->|得到候选后,重新开启事务| VERIFY
+    DB -. "读取当前发布版本与权限" .-> VERIFY
+    VERIFY -. "retrieve 返回 HTTP items" .-> EVAL
+    EVAL -->|逐项及 finally 保存| WRITE
+```
+
+`RetrievalManifest` 固定店铺、文档版本、查询文本、预期文档以及总运行时间。`validate_labels()` 检查文档版本、查询唯一性和预期文档范围;`origin=production_reviewed` 要求人工相关性审核及模型部署证据,入口再调用 `Review.verify()` 核对证据文件摘要。
+
+`evaluate()` 对每条查询请求 `limit=10`,请求由 FastAPI 分派给 `retrieve()`。后端先验证身份和店铺权限,通过 `RetrievalClient.search()` 获取候选,再开启事务重新认证并调用 `verified_results()`,保留当前有效发布资料。向量和知识权限内部过程可结合知识路线阅读。
+
+脚本检查返回资料是否在清单内,且版本相同,然后按 `document_id` 去重。单条 `recall_at_10` 为命中的预期文档数除以预期文档数;报告同时计算类别平均值和全部查询平均值。全部平均值达到 `0.9` 时状态为 `PASSED`,否则为 `FAILED`。执行中断时 `finally` 将尚未完成的 `RUNNING` 改为 `INCOMPLETE` 并保存已有记录。报告固定登记 `generation_attempts=0`;检索过程仍调用 embeddings 与 Milvus。
+
+### 图 3 源码导航
+
+1. [检索评测 CLI](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/scripts/evaluate_retrieval.py:68)、[RetrievalManifest.validate_labels()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/evaluation.py:147)、[Review.verify()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/evaluation.py:74):核对清单标注与审核条件。
+2. [evaluate()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/scripts/evaluate_retrieval.py:15)、[retrieve()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/api.py:289):跟踪脚本请求和后端入口。
+3. [RetrievalClient.search()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/vector.py:98)、[embeddings()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/vector.py:66)、[verified_results()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/modules/knowledge/api.py:243):理解候选获取和发布资料校验。
+4. [write_report()](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/src/zhifu/evaluation.py:24):检查完整报告如何经 `.pending` 文件替换目标文件。
+
+## 关键数据与状态
+
+- `monitor:worker:{role}:*` 表示单个工作进程心跳;TTL 使异常退出后的记录自动失效。`zhifu_workers_alive` 在采集时统计有效记录。
+- `zhifu_pending_jobs` 与 `zhifu_oldest_pending_seconds` 反映任务等待;`zhifu_jobs_15m` 按记录创建时间筛选后统计当前状态;`zhifu_model_tokens_15m` 汇总对应时间范围内记录的用量。
+- `manifest_sha256` 关联评测互斥锁、持久额度与报告。`source_sha256` 固定当前后端 Python 源码,`release_sha256` 固定运行发布参数。
+- `reserved_calls` 记录评测预留次数;`recorded_attempts` 统计观测到的模型尝试;`attempts_without_usage` 保留用量未知记录。这些字段分别保存不同阶段的证据。
+- Agent 案例状态包含 `NOT_RUN`、`REVIEW_REQUIRED`、`PASSED`、`FAILED`。数据库运行状态和评测审核状态分别维护。
+- 检索报告状态包含 `RUNNING`、`PASSED`、`FAILED`、`INCOMPLETE`;`recall_at_10` 使用去重后的文档标识计算。
+
+## 阅读检查问题
+
+1. `/health/ready` 正常时,怎样从心跳、待处理数量、等待时间和模型额度判断后台任务是否持续推进?发布检查还覆盖哪些依赖?
+2. 同一 Agent 评测进程中断并重新运行时,哪些字段阻止扩大调用次数?哪些摘要保证审核结果对应当前源码、运行参数和观测证据?
+3. 检索报告的单项召回率、类别平均值和最终状态分别怎样计算?文档版本变化、请求异常与审核资料缺失分别在哪个位置结束处理?
+
+## 测试文件阅读入口
+
+本路线只阅读用例及其断言;编写本文未执行测试、评测脚本或真实模型调用。
+
+- [test_monitoring.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_monitoring.py:20):HTTP 指标标签、身份信息保护、真实数据库指标和工作进程心跳。
+- [test_evaluation_validation.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_evaluation_validation.py:13):数据集合、审核摘要、结果约束、检索标注与报告替换。
+- [test_evaluation_runtime.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/test_evaluation_runtime.py:24):持久额度恢复、总输出上限、指定运行领取和真实工具结果观测。
+- [process_recovery_acceptance.py](G:/leon/miaoa/workspace/shuh-ai/zhifu-platform/tests/process_recovery_acceptance.py:23):自然租约到期、重新领取与旧执行 token 失效,用于连接运行监控和恢复证据。

+ 39 - 0
docs/reading-routes/README.md

@@ -0,0 +1,39 @@
+# 源码阅读路线
+
+代码依据:2026-09-30 当前工作区。每条路线以具体业务场景为入口,提供代码文件、关键函数、调用关系、数据状态和测试阅读位置。
+
+## 路线入口
+
+1. [一次咨询的全过程](01-conversation.md):消费者页面、消息接口、数据库事件、SSE 续传、人工接管与回复。适合作为共同入门路线。
+2. [Agent 如何完成任务](02-agent-runtime.md):运行创建、发布配置、worker 调度、LangGraph、工具调用、补充信息与业务等待恢复。
+3. [一笔换货申请如何完成](03-after-sales.md):业务资格、方案版本、客户确认、主管审批、数量预占与 OMS 履约。
+4. [资料与记忆如何参与回答](04-knowledge-memory.md):资料发布、混合检索、引用复验、客户授权与沟通习惯记忆。
+5. [多个商家如何共用平台](05-identity-permissions.md):租户与店铺、登录会话、资源归属、操作权限、后台复验与平台独立身份。
+6. [模型资源与费用](06-capacity-billing.md):并发预留、租户调度、token 与金额预占、实际用量结算、账单与双人审批。
+7. [外部渠道与可靠消息](07-channels-messaging.md):签名验证、身份关联、Inbox、Outbox、RabbitMQ、消费收据和渠道投递。
+8. [运行管理与效果验证](08-operations-evaluation.md):应用启动、发布检查、心跳、监控、故障恢复入口、Agent 评测与检索评测。
+
+## 跟踪方法
+
+打开一条路线后,从图中的入口函数开始,在源码中寻找图上标注的下一次调用。每到一个节点,记录当前输入、身份、读取的数据、写入的数据和返回结果。遇到数据库任务、HTTP 或消息队列时,沿箭头上的说明进入对应接收端或工作进程。
+
+后端图中以 `modules/`、`core/`、`operations/`、`oms/` 开始的路径相对 `src/zhifu/`;`frontend/`、`scripts/`、`tests/` 与 `compose` 文件从仓库根目录开始。各路线提供节点编号和源码导航,点击带行号的链接可以定位对应定义。
+
+图中的连续箭头表示当前进程内的函数调用、返回后继续执行或带条件的分支;具体含义写在箭头上。间断箭头标注 HTTP、数据库持久记录、消息队列、框架回调或后续独立请求。函数名称和所有状态名称保持源码标识。
+
+函数嵌套定义会注明所属外层函数,例如 `build_graph.model` 表示 `build_graph` 内部的 `model`。同一文件出现多个 `execute_one` 或 `emit` 时,以文件路径和所属类确认定义。`内部` 节点说明当前函数中的关键处理位置。
+
+阅读每幅图后,用源码回答文末的三个检查问题,并查看相应测试用例中的输入、调用和断言。测试文件承担行为说明的作用;实际运行时遵循项目的环境要求、数据范围和模型调用限制。
+
+## 交叉阅读位置
+
+- 路线 1 到达 Agent worker 后,进入路线 2;遇到售后卡片、客户确认和审批时,进入路线 3。
+- 路线 2 中的 `search_knowledge`、记忆读取、`BudgetCallback` 分别连接路线 4 与路线 6。
+- 路线 3 的 `BusinessEvent`、消费收据和 OMS 请求连接路线 7;业务权限检查连接路线 5。
+- 路线 8 根据运行记录、费用记录、检索资料版本和业务断言核对前面各条路线的结果。
+
+已有的[会话消息流程文档](../architecture/conversation-message-flow.md) 提供完整对话时序,可以与路线 1、路线 2 配合阅读。
+
+## 核对范围
+
+本组文档按当前源码核对文件、函数、分支、持久记录与进程边界。文档检查覆盖 Mermaid 语法、本地链接及带行号的源码位置。本文档编写过程没有执行业务验收、外部履约或生成模型请求;测试文件链接供学员阅读。