Commit 78802e02 by luoqi

Merge documentation consolidation from main into test

parents cab71e89 99fb2648
Pipeline #3800 failed in 0 seconds
......@@ -2,8 +2,8 @@
PAC — Patient Analyze Center. Independent service that ingests patient facts from host systems
(Friday SaaS / 5i5y / DW / mock), maintains versioned Persona + Follow-up Plan models, and
exposes API Pool 2 query surfaces back to hosts. See [`docs/architecture.md`](docs/architecture.md)
for the full design rationale.
exposes API Pool 2 query surfaces back to hosts. See the [architecture overview](apps/pac-docs/content/docs/architecture/overview.mdx)
and [documentation index](docs/README.md).
## Layout
......@@ -14,7 +14,8 @@ apps/
packages/
types/ API/model/enum types shared by service and web
utils/ Crypto, date, template helpers
docs/architecture.md
apps/pac-docs/ Formal product and integration documentation
docs/ Internal runbooks and historical archive
docker-compose.yml Local infrastructure (postgres / redis; ai, dw, apps profiles)
```
......
# @pac/docs
PAC 的对外文档站,基于 [Fumadocs](https://fumadocs.dev)(Next.js App Router)。
PAC 与 FridayAI 的正式文档站,基于 Fumadocs(Next.js App Router)。
## 开发
```bash
pnpm --filter @pac/docs dev # http://localhost:3200
pnpm --filter @pac/docs dev # http://localhost:3102
```
## 结构
......@@ -13,17 +13,18 @@ pnpm --filter @pac/docs dev # http://localhost:3200
| 路径 | 说明 |
|---|---|
| `content/docs/**` | 手写文档源(MDX)。`meta.json` 控制侧栏顺序/分组。 |
| `content/docs/decisions/` | ADR(架构决策记录),只追加。 |
| `content/docs/friday-ai/` | FridayAI 产品说明与使用指引。 |
| `content/docs/integration/` | 宿主与数据接入契约;参数在对应契约维护。 |
| `openapi/pac.json` | **生成物**:PAC 的 OpenAPI spec(勿手改,见下)。 |
| `lib/openapi.ts` | fumadocs-openapi server(读 `openapi/pac.json`)。 |
| `lib/source.ts` | 多源 loader:手写 docs + 由 spec 派生的虚拟 API 页(`/docs/api`)。 |
| `lib/shared.ts` | 站点名、路由、GitLab 源链接配置。 |
| `source.config.ts` | 内容集合 + frontmatter schema。 |
## API 参考(自动同步,零漂移)
## API 参考
`/docs/api/**` 下的端点页**不是手写的** —— 由 NestJS 的 OpenAPI spec 渲染。
spec 来自 pac-service 的代码(nestjs-zod DTO),「文档 = 代码」:
spec 来自 pac-service 的接口定义。接口变化后需要重新导出;只改代码不会自动更新已保存的 spec:
```bash
# API 改了之后,重新导出 spec(preview 模式,不连 DB):
......@@ -37,8 +38,10 @@ pnpm docs:openapi # 根目录;= pnpm --filter @pac/service openapi:dump
- 新页面:在 `content/docs/` 下加 `.mdx`,frontmatter 写 `title` / `description`,
在对应 `meta.json` 的 `pages` 里登记顺序。
- 新 ADR:`content/docs/decisions/NNNN-<slug>.mdx`,沿用四段结构(背景/决策/后果/状态)。
- 维护原则见文档首页(`content/docs/index.mdx`)的「文档怎么维护」。
- 产品页先回答用途、条件、处理结果;详细参数只在接口契约维护。
- 功能声明核对当前代码,不用历史设计、销售稿或一次部署结果替代事实。
- 内部排障、供应商资料、开发记录和环境凭据留在 `docs/`,不进入对外页面。
- FridayAI 整理依据见 `docs/qa/2026-10-14-friday-ai-docs-consolidation.md`;新增能力时同步更新相关页面与依据。
## 校验
......@@ -46,3 +49,5 @@ pnpm docs:openapi # 根目录;= pnpm --filter @pac/service openapi:dump
pnpm --filter @pac/docs type-check # fumadocs-mdx + next typegen + tsc
pnpm --filter @pac/docs build # 生产构建
```
同时检查侧栏、页面链接、图示和窄屏阅读。线上文档站独立部署,使用 `deploy/deploy-docs.sh`;不需要重启 PAC 或 FridayAI 业务服务。
import { RootProvider } from 'fumadocs-ui/provider/next';
import './global.css';
import type { Metadata } from 'next';
import { translations } from '@/lib/i18n';
// 不用 next/font/google(Google Fonts 在内网 / 国内不可达,会卡编译)。
// 走系统字体栈(见 global.css 的 --font-sans),离线可用,中英文都正常。
......@@ -14,7 +15,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<RootProvider>{children}</RootProvider>
<RootProvider i18n={{ translations }}>{children}</RootProvider>
</body>
</html>
);
......
---
title: 召回算法
description: 应治未治召回(treatment_initiation_recall)—— 有诊断/医生建议但未启动对应治疗的患者,如何筛选、合并、打分成召回任务。
icon: Filter
icon: Funnel
---
> **场景**:**应治未治召回**(`treatment_initiation_recall`)—— 患者有治疗需求信号(诊断 / 医生建议)却没启动对应治疗。**这是当前唯一在引擎注册运行的召回 scenario**。
......
......@@ -21,17 +21,17 @@ icon: Database
| `patient_return_visits` | L1 | upsert | 诊所回访任务记录(展示用)。**非临床 fact,不进召回信号** |
| `patient_transactions` | L1 | append-only | 操作账本:谁在哪个诊所对哪个主体做了什么。原文留底 |
| `patient_facts` | L1 | 版本流 | 事实单元(版本流),`fact.content` 是下游唯一真理源 |
| `personas` | L2 | 版本流 | 患者画像(每次重算新版本) |
| `personas` | L2 | 版本流 | 患者画像(内容变化升版;重复或水位刷新可不升版) |
| `persona_features` | L2 | 版本流(随父) | 画像特征条目(key + 描述 + 证据) |
| `persona_recompute_logs` | 横切 L2 | append-only | 画像重算执行账本 |
| `persona_recompute_logs` | 横切 L2 | 处理账本 | 画像重算执行账本 |
| `followup_plans` | L3 | 版本流 | 召回计划(patient 级,单一活跃) |
| `plan_reasons` | L3 | 子表(plan 1:N) | 召回理由清单(scenario / sub_key 维度 + 证据 + 打分 breakdown) |
| `plan_scripts` | L3 | 版本流(随父) | 客服参考话术(per-plan 单行,异步生成) |
| `plan_summaries` | L3 | 版本流(随父) | 详情页摘要卡片(per-plan × type) |
| `plan_executions` | L3 | append-only | 执行回写(客服通话结果),驱动 plan 状态机 |
| `plan_generation_logs` | 横切 L3 | append-only | 召回生成执行账本 |
| `agent_invocations` | 横切 | append-only | AI 调用审计账本(跨所有层共享) |
| `sync_logs` | 横切 L1 | append-only | 同步运行账本(cursor / reconcile / 计数) |
| `plan_generation_logs` | 横切 L3 | 处理账本 | 召回生成执行账本 |
| `agent_invocations` | 横切 | 处理账本 | AI 调用审计账本(跨所有层共享) |
| `sync_logs` | 横切 L1 | 处理账本 | 同步运行账本(cursor / reconcile / 计数) |
| `ingest_evidence` | 摄入诊断 | 原文去重 + 有限保留 | `/push/rows` 已验签完整请求,包含成功行与入库前拒收行;不参与事实计算 |
> PAC **不立** tenants / clinics / users 表 —— 这三个 id 都是宿主经换票 token 提供的 `TEXT`,PAC 不维护其主数据(见 [架构总览](/docs/architecture/overview) · 多租户与权限)。
......@@ -40,7 +40,7 @@ icon: Database
## 设计原则
**1 · 按表语义隔离,不是统一三级。** `host_id + tenant_id` 是所有业务表的硬隔离基线;`clinic_id` 只在三种业务语义下立柱,不滥用:
**1 · 按表语义隔离,不是统一三级。** `host_id + tenant_id` 标识宿主数据空间;共享空间还需源组织命名空间和授权范围隔离;`clinic_id` 只在三种业务语义下立柱,不滥用:
| 维度 | 表 | 语义 |
|---|---|---|
......@@ -50,11 +50,12 @@ icon: Database
**2 · 事实 vs 推断分层。** 事实(宿主给的结构化记录)落 `patient_transactions` + `patient_facts`;推断(PAC 算的结论)落 `persona_features` + `followup_plans`。**推断绝不回写事实** —— `plan_executions` 只驱动 plan 状态机,不进 fact/transaction(否则 PAC 自己的动作会被当患者事实参与下一轮重算,证据链就乱)。
**3 · 写入模式按数据所有权三分。**
**3 · 写入模式按职责区分。**
| 模式 | 表 |
|---|---|
| **append-only**(只插不改不删) | `patient_transactions` / `plan_executions` / `agent_invocations` / 3 张 orchestration log |
| **业务记录追加** | `patient_transactions` / `plan_executions` |
| **处理账本**(运行状态与结果可回填,按保留策略清理) | `agent_invocations` / `sync_logs` / `persona_recompute_logs` / `plan_generation_logs` |
| **upsert 当前态** | `patients` / `patient_profiles` / `patient_relations` / `patient_return_visits` / `hosts` |
| **版本流**(append + supersede) | `patient_facts` / `personas`(+features)/ `followup_plans`(+scripts/summaries) |
......@@ -62,7 +63,7 @@ icon: Database
入站诊断表 `ingest_evidence` 按报文哈希合并接收次数,到期删除;它不是业务账本或事实版本流。
**4 · 证据链与版本化。** 每个对外输出都能反查"为什么这么定":
**4 · 证据链与版本化。** 画像与计划保存来源证据,处理日志用于核对执行结果:
```
persona_features.evidence = { factIds, agentInvocationIds, ruleIds }
......@@ -113,7 +114,7 @@ patient_facts.transactionIds → patient_transactions.id → rawPayload
**`patient_return_visits`** — 诊所回访任务记录(展示用)。唯一键 `(host_id, tenant_id, source_unit, external_id)`。**不是临床 fact、不进召回信号**;详情页"回访记录"按 `task_date` 倒序展示,避免客服重复外呼。
**`patient_transactions`** — 操作账本(append-only)。`event_seq` BigInt 单调水位(供 persona / 游标消费);`source_event_id` 幂等键(Push 必带,Pull adapter 合成);`raw_payload`(单条宿主源行,push/pull 共用的 reparse 输入)+ `payload_hash`;`clinic_id` 立柱。`action` × `subject_type` 为 PAC 归一化封闭集 —— **23 个 action × 16 个 subject_type**(`@pac/types` `Action` / `SubjectType`)。
**`patient_transactions`** — 操作账本(按事件追加,保留原文)。`event_seq` BigInt 单调水位(供 persona / 游标消费);`source_event_id` 幂等键(Push 必带,Pull adapter 合成);`raw_payload`(单条宿主源行,push/pull 共用的 reparse 输入)+ `payload_hash`;`clinic_id` 立柱。`action` × `subject_type` 为 PAC 归一化封闭集 —— **23 个 action × 16 个 subject_type**(`@pac/types` `Action` / `SubjectType`)。
**`patient_facts`** — 事实单元(版本流)。`subject_id` 业务身份跨版本稳定,`(…, subject_id, version)` 单调递增;`kind`(`actual`/`planned`,2)× `type`(**16 个 FactType**)× `status`(`active`/`superseded`/`cancelled`/`fulfilled`/`expired`/`invalidated`,6);`content` JSONB(per type zod 校验,规则引擎只读这里);`transaction_ids` 证据链。一个 transaction 可产 0/1/N 个 fact。
......@@ -143,7 +144,7 @@ patient_facts.transactionIds → patient_transactions.id → rawPayload
### 横切支撑
**`agent_invocations`** — AI 调用审计账本(append-only,跨所有层共享)。`kind` / `call_key` / `prompt_version` / `model_provider` / `model_name` / `workflow_run_id` / `input_snapshot` / `output` / token 明细 / `cost_yuan` / `status`。每次 LLM 调用两段式落账(start=running → end 回填),是 audit / replay / 成本核算唯一真理源。AI 解析结果归 `output`,**不回写 fact**。
**`agent_invocations`** — AI 调用审计账本(跨层共享,状态及结果分阶段回填)。`kind` / `call_key` / `prompt_version` / `model_provider` / `model_name` / `workflow_run_id` / `input_snapshot` / `output` / token 明细 / `cost_yuan` / `status`。每次 LLM 调用两段式落账(start=running → end 回填),是 audit / replay / 成本核算唯一真理源。AI 解析结果归 `output`,**不回写 fact**。
**`sync_logs`** — 同步运行账本。`direction`(pull/push)/ `resource` / `status`(running/success/partial/failed)/ `cursor_before`/`after`(pull 游标;push 留空)/ `triggered_by`(含 `reconcile:<date>` 对账)/ `fetched`、`transactions_written`、`duplicates`、`failed`、`facts_emitted` / `metadata`(cohort 进度 + 漂移审计 `suspectFields`)。
......
---
title: 组织与账号管理
description: 管理机构层级、人员职责、数据范围和企业微信托管。
icon: Building2
---
机构管理员维护组织、员工与服务配置;平台人员负责客户开通和托管席位。两类工作台权限分别管理。
## 组织、角色与岗位
| 项目 | 决定什么 |
|---|---|
| 组织架构 | 集团、品牌、区域、门店的管理关系 |
| 角色 | 可执行的功能,如管理员、员工、只读 |
| 组织授权 | 能查看或管理哪些组织的数据 |
| 账号归属与授权 | 能处理哪些企微账号的会话 |
| 所在诊所与岗位 | 服务归属、业务职责与提醒对象 |
岗位名称不自动扩大权限。管理员也受其组织范围限制;从宿主系统进入时,同时受宿主本次授权和应用开放能力限制。
一般业务角色按管理员、员工、只读管理。只读角色不能发送患者消息或办理业务;账号使用人可按授权维护自己账号的服务风格等个人设置。
## 企业微信托管
一个企微号占用一个托管席位。首次扫码需要服务方分配的绑定授权;客户管理员不能自行增加席位。
- **同号重登**:使用「重新托管」,复用已有账号的托管关系。
- **人员交接**:由具备权限的管理员转交账号,账号沿用其历史会话与绑定资料。
- **解除托管**:停止由 FridayAI 处理该账号,换号前与服务方核对席位安排。
- **到期或停用**:停止对外发送,包含人工发送、AI 回复及提醒;保留账号与历史数据。
席位到期后续期,不会自动重放已经作废的发送任务。更换账号与转交同一账号分别办理。
## 日常管理重点
关注账号是否在线、患者是否正确绑定、人工待办是否及时处理,以及门店资料和教材是否仍有效。
具备相应权限的人员可查看操作记录和 AI 调试记录,核对回复依据与未发送原因。记录可能包含患者资料和会话内容,应在授权范围内使用。
提醒对象、营业时间与人工协作规则见[人工协作](/docs/friday-ai/human-collaboration);资料维护见[门店知识](/docs/friday-ai/knowledge)。
---
title: 预约服务
description: 从预约前提醒到患者回复、人工办理与到诊结果核对,持续跟进一条预约。
icon: CalendarCheck
---
「预约确认 Agent」依据接入的有效预约生成服务任务。团队可以查看患者、预约安排、联系进度、待执行事项和处理结果。
## 预约前怎样跟进
| 阶段 | 系统处理 |
|---|---|
| 预约在 6–7 天后 | 对符合条件的预约发送关心消息 |
| 预约前 24 小时内 | 发送确认提醒,核对时间、医生、门店及到店准备 |
| 患者确认 | 记录确认意愿,发送适用的到店信息 |
| 患者迟迟未回复 | 依据距预约时间安排追问或人工电话待办 |
| 未绑定可联系的微信 | 生成电话确认待办,由工作人员联系 |
主动预约扫描在北京时间 09:00–20:00 内执行。发送还受账号在线、托管有效、AI 暂停等条件约束。
## 患者回复后怎样处理
- **确认来诊**:记录微信确认;已启用回写的宿主,可将确认结果提交到业务系统。
- **希望改约或取消**:形成需要人工办理的事项,由工作人员核对并处理。
- **提出症状或医疗问题**:转交专业人员。
- **表达不明确**:保留原话,交由工作人员继续确认。
电话由工作人员拨打,通话后在待办中选择实际结果。患者已在微信中表态时,不再继续原来的无回复电话确认;改约、取消或医疗问题转为相应待办。
## 改约与业务回写
已开通回写且具备权限时,工作人员可在待办中办理确认、取消或改约。系统核对原预约、医生、时间及可用性后提交,并查询业务系统的处理结果。
排队或提交成功不等于预约已修改。结果待核验或失败时,先核对原操作;尚未开通回写的机构,在源业务系统办理后记录处理结果。
读取到明确对应的新预约后,系统可复述更新后的安排;正在人工处理的事项由工作人员继续办理。
## 确认意愿与实际到诊
“微信确认来诊”表示患者意愿。系统在预约过 24 小时后的每日收尾中,依据业务记录标记到诊结果或关闭原因;读取失败时留待下一轮核对。
源记录晚录或遗漏会影响结果。出现时间、医生或到诊状态不一致时,应检查源业务记录与接入数据,见[系统对接](/docs/friday-ai/integration)。
---
title: 患者接待
description: 结合患者身份、会话上下文与机构资料,处理企业微信中的日常咨询。
icon: MessageCircle
---
「AI 咨询」汇集授权账号的单聊与患者群。工作人员可查看消息、患者资料、AI 回复和需要接手的事项。
## 先对上患者
在「通讯录」将微信联系人关联到患者档案,并注明本人、父母、子女、配偶或其他关系。群聊可通过成员的绑定关系关联患者。
绑定后,AI 才能查询对应患者的预约与档案要点。多门店机构还需确定服务门店,才能使用该门店的具体资料。绑定关系应由工作人员核实,避免把家属当作患者本人。
## AI 根据什么回复
| 患者的问题 | 回答依据 |
|---|---|
| 地址、营业时间、医生介绍、到店安排 | 门店信息 |
| 我的预约时间与接诊医生 | 已绑定患者的预约记录 |
| 服务项目的一般介绍、常见疑问 | 机构维护的话术与科普 |
| 刚才说到的事情、之前约定的内容 | 当前会话及按需检索的历史消息 |
| 发来的文件或图片是什么内容 | 可读取的附件与图片内容 |
连续发送的文字与图片结合上下文处理。患者有新消息或工作人员已回复时,系统会检查当前回答是否仍适合发送。
## 图片、视频与文件
- **图片**:可结合患者发来的图片交流,具体医疗判断交由专业人员。
- **视频**:支持读取画面;音轨不在当前理解范围内。
- **文件**:支持读取可解析的 PDF、Word(.docx)、Excel(.xlsx)内容,损坏或加密文件可能无法读取。
- **语音**:工作人员可在工作台播放;AI 语音识别尚未开放。
会话文件与教材导入用途不同。维护机构教材的格式和确认步骤见[知识维护](/docs/friday-ai/knowledge)。
## 哪些问题交给人
症状反馈、个体诊断、用药或治疗方案、费用协商、投诉,以及查不到依据的具体安排,交由工作人员核实处理。AI 不对治疗费用报价;患者提供的单据内容可以复述。
群聊按患者的 @ 对象与账号托管状态决定回应方;指向其他人的消息交由对应人员处理。需要人工介入时,进入[人工协作流程](/docs/friday-ai/human-collaboration)。
---
title: 开通与配置
description: 完成客户开通、组织授权、企业微信托管和服务资料准备。
icon: ListChecks
---
开通由平台人员、机构管理员和账号使用人共同完成。患者数据、登录入口和业务回写分别核对,按机构需要启用。
## 1. 确认客户与数据范围
向服务方提供机构名称、业务系统、客户组织编号和首位管理员信息。服务方核对数据归属并开通客户,管理员通过一次性激活链接设置登录信息。链接 30 分钟有效;未激活时可由平台重新生成,旧链接随即失效。
独立登录使用 FridayAI 登录页;已接入宿主的机构也可从业务系统进入。两种入口的可用功能以各自授权为准。
## 2. 配置组织与员工
在「组织架构」建立集团、品牌、区域和门店;需要使用患者数据的门店,关联对应的患者档案组织编号。
在「员工授权」配置人员角色、可管理的组织范围和所在诊所。岗位用于说明职责;数据范围和操作权限由授权决定。
## 3. 托管企业微信账号
服务方开通托管席位并分配扫码授权后,员工在工作台完成企业微信扫码。首次绑定授权有有效期,过期需重新申请。
检查账号在线状态、归属员工与服务门店。重登同一账号使用「重新托管」;换号、转交或解除托管按[账号管理](/docs/friday-ai/administration)办理。
## 4. 备好回答与交接依据
| 配置位置 | 需要准备的内容 |
|---|---|
| 门店信息 | 地址、电话、营业时间、医生及服务安排 |
| 教材语料 | 服务边界、常见问题话术、项目科普、到店准备 |
| AI 设置 | 提醒账号、门店内部群、责任人员及 AI 启停 |
| 账号设置 | 自称、语气和人工服务时段 |
| 通讯录 | 微信联系人与患者档案的绑定关系 |
分级提醒需要可用的提醒账号和收件对象。未配置的层级无法发送,详见[人工协作](/docs/friday-ai/human-collaboration)。
## 5. 核对一条完整服务流程
- 日常咨询:地址、营业时间、医生介绍与门店资料一致。
- 患者查询:绑定正确患者后,预约与档案能在授权范围内查看。
- 人工协作:转人工后能看到交接信息,责任人可收到提醒并接手。
- 预约服务:核对一条有效预约的时间、医生、门店、提醒和待办结果。
- 业务回写:如已开通,核对工作台操作与源业务系统中的实际结果。
完成上述核对后,按已确认的账号和门店范围启用服务。
---
title: 人工协作
description: 看清待处理事项、接手会话,并通过分级提醒减少遗漏。
icon: Handshake
---
需要人工判断或办理的事项,会在工作台显示交接原因、患者原话及可用的上下文。可供参考的回复草稿由工作人员核对后发送。
## 接手与交还
1. 查看需要回答的会话或待办,确认患者诉求与已经核对的信息。
2. 选择「人工接管」,暂停该会话的 AI 发送;工作人员继续在工作台或企业微信中回应。
3. 处理完本次事项并记录待办结果。需要恢复自动接待时,选择「交还 AI」。
“已接管”与“已回复”分别记录:接管停止提醒向上升级,实际回复表示本次交接已得到回应。
## 无人响应时怎样提醒
提醒通过机构配置的企业微信提醒账号发送。
| 完整提醒路径 | 发送对象 |
|---|---|
| 交接后 | 责任人 |
| 上一级提醒后约 5 分钟 | 再次提醒责任人 |
| 再过约 5 分钟 | 门店内部群 |
| 再过约 10 分钟 | 诊所经理 |
实际节奏受扫描时间和消息投递影响。未配置的层级会跳过;有人接管后停止向上升级,非营业时间暂停升级。患者再次催促时,可触发追加提醒。该路径用于推进处理,不构成真人响应时限承诺。
## 按服务范围暂停 AI
| 方式 | 适用情况 |
|---|---|
| 人工接管会话 | 正在处理当前患者的问题 |
| 账号人工时段 | 固定时段由员工接待 |
| 联系人标签「AI别管」 | 对该联系人的单聊暂停自动接待 |
| AI 设置中的启停 | 暂停机构或账号的 AI 服务 |
AI 暂停开关控制自动发送,人工发送仍需账号在线、托管有效及相应权限。暂停期间被作废的消息,恢复后不会集中补发;工作人员应核对尚待处理的事项。
---
title: 系统对接
description: 分别开通业务数据、宿主登录与预约回写,明确服务所需的接入条件。
icon: Cable
---
FridayAI 通过患者数据中枢使用机构授权的患者与预约信息,在企业微信中完成接待与跟进。业务系统继续负责源记录和业务结果。
## 三项接入分别确认
| 接入项 | 提供的能力 | 核对内容 |
|---|---|---|
| 业务数据 | 查询患者、预约与已接入的档案资料 | 客户与门店编号、主体覆盖、更新速度 |
| 宿主登录 | 从业务系统免登进入 FridayAI | 用户身份、组织范围、角色与开放模块 |
| 业务回写 | 将已授权的预约操作提交到源系统 | 动作范围、办理权限、重复请求与结果核验 |
免登开通不自动启用业务回写。具体模块与操作按接入清单开放。
## 业务数据
患者、门店和预约使用稳定的源系统编号关联。微信绑定应对应同一患者;门店映射应对应实际服务组织。
档案中可展示的病历、收费、诊断或影像资料取决于源系统实际接入情况。预约变更与到诊记录需要持续更新,才能及时跟进和核对结果。
数据接入人员使用[数据对接说明](/docs/integration/overview)核对接入方式;Friday SaaS 推送格式见[主体推送契约](/docs/integration/friday-push-payload)。
## 从宿主进入工作台
宿主后端核验用户身份与当前权限后,申请短期进入码;FridayAI 兑换后打开已授权模块。机构和组织映射需事先开通,用户权限按 FridayAI 与宿主授权共同限制。
参数、进入码、组织授权及退出处理统一见[FridayAI 宿主接入契约](/docs/integration/friday-ai-host)。
## 预约操作的结果
确认、取消、改约按宿主支持的动作逐项启用。患者的微信确认、工作台办理进度和源系统中的预约结果分别记录;需要以查询到的业务结果核对完成状态。
客户使用说明见[预约服务](/docs/friday-ai/appointments)。出现不一致时,提供相关预约编号、发生时间和操作结果,由接入人员核对。
---
title: 门店知识与服务口径
description: 分开维护门店事实、机构教材与账号风格,让回答具体、口径一致。
icon: BookOpen
---
机构维护回答依据,FridayAI 按服务场景使用。资料应明确、简短,并及时更新。
## 门店信息:这家店的事实
在「门店信息」维护地址、电话、营业时间、特殊日期安排、医生介绍和服务条件。各门店分别核对,具体回答以对应门店资料为依据。
未知事项保留未填写;“没有填写”与“明确不提供”含义不同。资料变化后更新原记录,避免把旧事实留在教材中。
## 教材语料:机构怎样服务患者
| 分类 | 维护内容 |
|---|---|
| 边界 | 不能说、不能承诺的事项 |
| 话术 | 常见场景的回应方式 |
| 科普 | 服务项目的一般介绍 |
| 到店准备 | 预约前需要携带或准备的事项 |
| 其他交代 | 需要 AI 知道的补充说明 |
| 固定消息 | 系统在指定场景直接发送的文字 |
语料支持启停与有效期。到店准备和固定消息会原样发送,保存前应核对适用场景及全部文字。
### 多门店怎样共用
集团、品牌、区域、门店可分层维护。边界与其他交代沿组织层级累加;同一位置的话术、科普、到店准备和固定消息使用最近一层的有效内容。
日常接待按服务患者的诊所取用口径;到店准备按该条预约的门店取用。页面可查看继承来源与当前使用的内容。
### 从已有教材导入
支持带可读取文字的 PDF 和 Word(.docx)。上传后预览候选段落,选择分类、修订并保存,确认收录后用于服务。教材导入暂不支持 Excel 或扫描件文字识别。
## 账号风格与会话连续性
企微账号可以设置自称、语气、性格及表情使用偏好。账号风格服从机构服务边界,不改变门店事实或人员权限。
会话历史支持上下文衔接,已绑定患者的资料帮助使用合适的称呼。新增服务规则、稳定口径和项目知识,由机构在教材中明确维护。
{
"title": "FridayAI 患者服务",
"icon": "MessagesSquare",
"pages": [
"overview",
"getting-started",
"conversations",
"human-collaboration",
"appointments",
"knowledge",
"administration",
"integration"
]
}
---
title: FridayAI 产品概览
description: 在企业微信中接待患者、跟进预约,并把需要人工处理的事项交给合适的人。
icon: MessagesSquare
---
FridayAI 为口腔机构提供患者服务工作台。AI 结合门店资料、患者预约和会话上下文处理日常咨询;需要判断、协调或办理业务时,转交工作人员继续处理。
机构可以沿用员工的企业微信账号和患者群,也可以从已接入的业务系统直接进入工作台。多门店共用机构服务口径,人员和数据按授权范围管理。
## 服务如何衔接
```mermaid
flowchart TD
A[患者咨询或预约待跟进] --> B{核对信息,确定处理方式}
B --> C[AI 接待与预约提醒]
B --> D[交接信息与人工办理]
C --> E[跟进回复,记录处理结果]
D --> E
```
## 当前提供的能力
| 能力 | 为团队解决的工作 |
|---|---|
| [患者接待](/docs/friday-ai/conversations) | 结合上下文回答日常问题,查询已绑定患者的预约与档案要点 |
| [人工协作](/docs/friday-ai/human-collaboration) | 保留交接原因与患者原话,提醒责任人,支持接管与交还 |
| [预约服务](/docs/friday-ai/appointments) | 预约前关心、确认、追问、电话待办及结果核对 |
| [门店知识](/docs/friday-ai/knowledge) | 维护门店事实、服务边界、话术、科普与到店准备 |
| [组织管理](/docs/friday-ai/administration) | 组织分层、人员授权、企微账号托管与交接 |
诊后随访、Patient Care、治疗机会和治疗链执行目前为待开放模块,以工作台实际开放入口为准。
## 服务质量怎样保障
- **重要信息有出处**:预约、地址、医生等具体信息依据可查询的业务记录与门店资料。
- **复杂事项有人接手**:医疗问题、费用协商和需要核实的安排进入人工协作。
- **联系过程可查看**:区分已发送、等待患者、需要人工和已有结果。
- **机构口径可维护**:公共教材分层应用,门店保留自己的事实与具体说法。
首次使用从[开通与配置](/docs/friday-ai/getting-started)开始;宿主系统对接见[系统对接](/docs/friday-ai/integration)。
......@@ -63,14 +63,14 @@ iframe 内的 token 落地与刷新由 PAC 前端完成。`refreshToken` 过期(
| `appId` | string | ✅ | PAC 分配给宿主的应用标识 |
| `appSecret` | string | ✅ | 与 `appId` 配对的密钥,**仅宿主服务端持有**,不下发到浏览器 |
| `user.userId` | string | ✅ | 宿主侧登录用户的唯一 id|
| `user.tenantId` | string | ✅ | 集团 id(组织树的根);单集团宿主固定用一个值。宿主侧有集团id就传id,没有传唯一slug如appId |
| `user.tenantId` | string | ✅ | 已登记的 PAC 数据空间 ID;通常对应集团。Friday SaaS 固定为 `friday-market`,不能填源品牌 ID |
| `user.role` | string | ✅ | 功能权限:`staff` / `leader` / `admin`。见下「`role`」 |
| `user.orgScope` | string[] | ✅ | 数据权限:用户可达的组织节点 id 列表;传 `[]` = 全集团不限。见下「`tenantId` 与 `orgScope`」 |
| `user.orgScope` | string[] | ✅ | 数据权限:用户可达的组织节点 ID。允许空范围的宿主,`[]` 表示该数据空间不限;Friday SaaS 禁止空范围。见下文 |
| `user.dictionary` | object | 选填 | `id → 名` 映射,仅用于页面把 id 显示成名字,**不影响权限**。含 `clinics`、`users` 两个子字典。建议提供,否则页面显示原始 id |
**Friday SaaS 换票边界**:服务端 `data/friday/manifest.yaml` 的 `auth` 策略限制最高角色为 `leader`,组织范围不能为空或包含空白项;数据租户必须为 `friday-market`,范围不能包含市场根 `friday-market`。品牌管理员应传本品牌节点,诊所用户传所属诊所节点。拒绝返回业务码 `10107`,不会签发令牌。策略同样用于刷新令牌;Friday 配置缺失或损坏时拒绝签发。未配置策略的其他宿主保持原行为(如 jvs-dw 集团管理员)。
宿主后端仍须核实用户身份、角色及所传品牌/诊所归属;PAC 这道上限校验不替代宿主用户授权。Friday 跨品牌运营账号不能复用普通品牌换票入口。已有 access token 和短期换票 code 不会被本次修改主动撤销,生效前如需立即收回旧授权,须另行安排令牌撤销;超范围 refresh token 无法续期。本次不改变开发用 mock-login 的入口权限,生产应关闭模拟登录。
宿主后端仍须核实用户身份、角色及所传品牌/诊所归属;PAC 这道上限校验不替代宿主用户授权。Friday 跨品牌运营账号不能复用普通品牌换票入口。权限调整时同时核对已签发 access token、进入码的有效期及刷新策略;超范围 refresh token 无法续期。生产应关闭开发用模拟登录。
> `orgScope` 字段本身必传(哪怕是空数组);请求体为严格模式,未定义的字段会被拒绝。
......@@ -82,7 +82,7 @@ iframe 内的 token 落地与刷新由 PAC 前端完成。`refreshToken` 过期(
| `leader` | 主管:在 staff 基础上增加全池查看 / 分配 / 回收 / 统计 |
| `admin` | 后台管理员:全部 |
**`tenantId` 与 `orgScope` —— 宿主侧组织 id**。PAC理解宿主(SaaS)组织是一棵树,根 = 集团(`tenantId`),往下品牌 / 区域 / 诊所等(仅举例);`orgScope` 是用户可达的节点(数据权限):
**`tenantId` 与 `orgScope` —— 宿主侧组织 id**。`tenantId` 指已登记的数据空间,`orgScope` 指该空间内用户可达的组织节点。常规集团空间下可包含品牌、区域和诊所;Friday SaaS 的共享市场空间还须指定实际品牌或诊所范围。下表仅适用于允许空范围的集团宿主:
| 用户 | `orgScope` |
|---|---|
......
---
title: FridayAI 宿主接入契约 v1
description: 与 PAC 同形的应用换票、组织授权、免登与退出契约。
description: 宿主后端换票、组织授权、免登与退出的接口约定。
icon: KeyRound
---
| 项目 | 内容 |
|---|---|
| 版本 | 1.0.0 · 2026-10-08 |
| 状态 | FridayAI 侧本地实现;测试/生产尚未部署,宿主后端尚未对接 |
| 版本 | 1.0.0 |
| 开通 | 按应用与客户配置开放角色、组织范围和模块;实际地址以接入清单为准 |
| 范围 | 内嵌或跳转 AI 自有页面,不开放业务 API,不自动启用宿主回写 |
产品与使用条件见 [FridayAI 系统对接](/docs/friday-ai/integration)。本页维护登录接口,业务数据接入与预约回写分别开通。
## 1. 最简接入
宿主后端核验当前登录、当前权限后调用换票,前端只取得短期 code 并打开 AI 页面。应用密钥、宿主 access/refresh token 不下发到 AI 浏览器。无需 postMessage、同域部署或中转页面。
......@@ -214,4 +216,4 @@ orgRoot 仅为开户时配置的固定 PAC 客户根,不是每次登录参数
每应用实例默认 token/logout 每分钟 120 次;入口另有独立的来源 IP 和全局限流,不与手机号密码登录共用桶。多实例生产网关应叠加总配额。码消费、刷新、退出的原子性由 PostgreSQL 保证,不要求 Redis。
宿主须提供真实后端身份核验、组织授权转换、前端入口及可靠退出通知。部署前完成 migration、应用/集团开通和真实宿主验收;本地成功不表示 Friday SaaS 测试站已调用此接口。当前回写权限账号仍为零,保持人工/链接业务流程。
宿主须提供后端身份核验、组织授权转换、前端入口及可靠退出通知。上线前完成数据库迁移、应用与客户开通,并核对真实宿主的进入、范围限制和退出流程。预约回写另行启用;未启用时在源业务系统办理。
---
title: FRIDAY 推送数据契约
description: FRIDAY SaaS 按形态 A 推送的 10 个 source 及字段定义;宿主推自洽的业务表(自己的引用 inline 进相关行),PAC 做跨宿主临床归一。
icon: FileJson
icon: FileBraces
---
> 本文是 [Push 通道](/docs/integration/channel-push)**形态 A** 在 FRIDAY SaaS 宿主的具体化:每个 `source` 的 JSON 字段清单。
......
{
"title": "PAC 文档",
"title": "Friday 产品文档",
"root": true,
"pages": [
"---了解 PAC---",
"---客户召回---",
"start",
"---设计---",
"---FridayAI---",
"friday-ai",
"---原理与设计---",
"design",
"architecture",
"algorithms",
......
---
title: 核心场景
description: PAC 当前的召回 —— 潜在治疗(应治未治):把「医生说该治、患者却没去治」的确定需求,系统化地捞回。
description: 围绕尚未启动的治疗需求,形成有依据、可执行的客户召回。
icon: Target
---
> 把「医生已经诊断或建议了某项治疗、患者却一直没去做」的患者,系统化地**找全、排序、配好话术**交给客服。
> 这类需求叫**潜在治疗(应治未治)** —— 临床上该做、商业上确定,而且已经发生在你自己诊所里。
---
## 为什么先做这个
牙科生意里,最贵的是拉新客;最便宜、最确定的增长,是把**已经进过门、已经被诊断、却没成交**的需求捞回来。这块「应治未治」:
- **需求是确定的** —— 不是猜患者想不想,是医生白纸黑字诊断 / 建议过。
- **已经发生在内部** —— 患者来过、诊断在系统里,捞回成本远低于拉新。
- **靠人一定会漏** —— 几万患者、跨门诊,靠医生 / 客服记得根本看不过来;一漏就是直接的收入与健康损失。
一次检查发现的问题,可能尚未安排治疗;一次咨询留下的建议,也可能没有继续推进。客户召回从机构已有记录中整理这些线索,帮助团队邀请患者回院,由医生重新核实和评估。
PAC 把这件事,从「靠人记得」变成「系统兜底」。
## 如何形成一个机会
---
系统结合三类信息判断:
## 怎么定义「应治未治」
| 信息 | 核对内容 |
|---|---|
| 需求线索 | 已接入的诊断、医生建议或影像分析是否指向支持的治疗方向 |
| 后续处理 | 相关时间、牙位与治疗类别下,是否已有对应的实际治疗记录 |
| 联系时机 | 近期到诊、未来预约、正在跟进及拒绝联系等记录是否需要暂缓或停止联系 |
一个患者进召回池,要同时满足三个条件:
机会依赖已接入数据。**没有查到治疗记录,并不能单独证明患者确实未治疗。** 联系前应核对来源和后续进展;影像所见还需医生评估。
| 条件 | 含义 |
|---|---|
| **应治** | 有诊断、医生建议、或影像分析,指向某项该做的治疗 |
| **未治** | 之后一直没做对应的治疗(只是「计划要做」不算) |
| **可联系** | 在册、未拒访、在世 |
## 覆盖的治疗方向
> 「应治」减去「未治」,剩下的就是**潜在治疗** —— 该做、却没做的那一块。
召回工作可围绕种植、正畸、儿童早期矫治、根管、牙周、充填、修复和拔牙组织。不同方向按各自规则核对需求、牙位、治疗和时间,并给出召回理由与优先级。
---
当前启用的是**尚未启动治疗**的场景。治疗中断后的续治、治疗完成后的定期复查,未作为当前通用召回场景开放。
患者对一项治疗只有三种状态,PAC 现在只覆盖第一种:
## 一个缺牙机会如何推进
- **现在只做**:**没开始治** —— 潜在治疗(应治未治)。
- **暂未做(留后续)**:
- **治了一半**:治疗做了一部分、后续步骤没跟上的。
- **治完要复查**:治疗完成后、该定期回来复查 / 维护的(如种植年度复查、正畸保持期)。
- 这两类代码里暂未启用。
1. 系统从已有记录中整理缺牙及相关牙位,核对后续治疗、到诊和预约。
2. 主管选择本轮客群,查看人数与团队任务量,确认分配和时限。
3. 客服回看病历背景,核对沟通参考,了解患者当前情况与意愿。
4. 愿意进一步了解时,衔接到院评估;暂时不便时,记录再次联系安排。
5. 主管查看执行结果,决定后续跟进和下一轮工作。
延伸:[召回算法](/docs/algorithms/recall) · [产品概述](/docs/start/what-is-pac) · [业务价值](/docs/start/business-value)
计划完成、客服记录预约成功、源系统实际到诊是不同结果,应分别查看。具体工作方式见[召回分配](/docs/design/batch-assignment),数据核对见[数据准备与结果核对](/docs/start/data-quality)。
---
title: 业务价值
description: 把患者数据从散乱的成本,变成集团可复用的判断力与行动力。
description: 把已有客户的需求整理为可执行的工作,让团队有依据地联系并持续跟进。
icon: TrendingUp
---
价值来自**三个结构性转变**,再被**两个放大器**放大。产品定义见 [产品概述](/docs/start/what-is-pac)。
客户召回的价值,在于让已有诊疗建议有人承接,让有限的客服时间用在有联系理由的客户身上,并让每一步推进都能回顾。
---
## 三个结构性转变
### 1. 数据 → 资产:从「散在各系统」到「统一可信的患者事实」
- **现状**:同一患者的就诊、治疗、缴费散在各门诊、各系统,口径不一、脏数据多,集团看不清一个完整的人。
- **PAC**:把任何宿主的原始数据标准化成统一口径、不可变、可追溯的**事实账本**。
- **价值**:集团第一次能**跨门诊看清同一个患者**;数据可信、可审计、可被多场景复用。
## 六类能力与收益
### 2. 判断 → 自动化:从「靠人拍脑袋」到「可解释的自动评价」
| 产品能力 | 工作方式的改善 | 为机构带来的价值 |
|---|---|---|
| 识别治疗机会 | 对照诊断、建议、牙位与已做治疗,保留联系依据 | 发现被搁置的需求,减少逐份翻查病历 |
| 画像与排序 | 集中呈现就诊、消费、服务关系和需求背景 | 帮助团队确定联系先后,新接手人员更快了解客户 |
| 客群筛选 | 按治疗方向、到诊时间和画像条件组织本轮名单 | 将经营目标落到具体客户,按承接能力逐轮推进 |
| 团队分配 | 兼顾专属客服关系与在手任务量,主管预览后确认 | 明确责任,方便交接、调整和发现执行盲区 |
| 沟通准备 | 提供关键事实、病历快读及电话、企微参考内容 | 缩短准备时间,让沟通围绕患者实际经历展开 |
| 跟进与追踪 | 记录反馈、下一次联系、退回原因和批次进展 | 减少跟进断档,为下一轮安排提供依据 |
- **现状**:该召回谁、为什么,靠客服经验和主管派单 —— 不可复制、不可审计,几万患者根本看不过来。
- **PAC**:一夜评完全量患者,给出价值与流失风险**画像** + **召回清单 + 优先级 + 召回原因**,每条带证据链。
- **价值**:判断**规模化**(几万人一次过)、**一致**(同一口径)、**可解释**(有原因和证据)、**可复制**(不绑定某个老员工)。
## 四个值得关注的差异
### 3. 行动 → 即取即用:从「准备耗时」到「上工即打」
- **现状**:客服翻档案、临场想话术耗时;新人靠老带新,上手慢。
- **PAC**:直接给**一页摘要 + AI 话术 + 客服侧 AI 助手**;执行结果回填,形成闭环。
- **价值**:客服时间从「准备」转向「触达」;新人快速即战力;话术统一、可迭代;每次结果回流让系统更准。
---
**联系理由可以回看。** 机会连接到相关诊断、牙位、时间和后续治疗记录,工作人员能核实为什么联系。
## 两个放大器
**需求核对考虑具体牙位。** 不同牙位可能处于不同状态;结合治疗类别和时间,减少把已处理的问题再次作为邀请重点。
### A. 一次投入,多处复用
**从名单到执行有连续的责任链。** 筛选、分配、资料准备、联系反馈和再次跟进围绕同一机会展开,主管与客服能接续处理。
标准事实层 + 画像层**不只服务召回** —— 它是集团的长期数据底座。后续精准营销、客户分层、风险预警、主管 dashboard 都能直接建在上面,边际成本低。**召回是第一个出口,底座是长期资产。**
**AI 帮助整理,业务决定由人确认。** 查询、拟定分配方案和准备话术减轻重复工作;负责人、福利及业务安排由机构核对确认,医疗判断由医生负责。
### B. 可追溯、可审计、主权清晰
## 各角色获得什么
每条召回都有**原因 + 证据链**;数据**主权清晰**(源数据归宿主,PAC 只持加工副本);集团之间硬隔离。让「为什么动这个患者」可解释、可复盘,降低合规与越界风险。
---
## 怎么衡量
| 角色 | 直接收益 |
|---|---|
| 门诊经营者 | 看清已有客户中值得进一步推进的需求,组织持续的老客户经营 |
| 客服主管 | 更快确定客群与分工,掌握在手量、超期和推进结果 |
| 客服与医生助理 | 集中了解背景,有依据地开口,接着上次沟通继续处理 |
| 连锁运营团队 | 在授权门店推广统一方法,同时保留门店安排和服务关系 |
| 患者与家属 | 得到与自身情况有关的关心,理解再次到院的目的,意愿和已有安排受到尊重 |
价值**机制是确定的**,但具体数字要靠试点实测。盯这几个可观测指标(需业务方提供基线):
## 从一个客群开始验证
| 指标 | 它衡量什么 |
|---|---|
| **召回覆盖率** | 系统识别出的「该回未回」患者数 / 总量 —— 衡量「看不过来」被解决的程度 |
| **客服准备耗时** | 每单从「翻档案 + 想话术」到「上工即打」的下降 |
| **高客单捞回比** | 高价值项目(种植 / 正畸 / 根管)漏召回被重新捞回的比例 |
| **接通后转化 / 沉睡挽回** | 高价值优先 + 个性化话术的实际效果 |
| **新客服熟练时长** | 达到稳定产能所需时间 —— 衡量「可复制性」 |
---
先选择一家门店、一个治疗方向和一组可核实的客户,完成一轮工作后再扩大范围。
## 一句话归纳
- **准备效率**:整理名单、查资料和准备沟通分别需要多少时间。
- **执行覆盖**:已分配机会中,哪些已联系,哪些尚未推进或超期。
- **推进结果**:分别看预约成功、约定下次联系、暂不考虑和停止跟进。
- **后续承接**:结合源业务系统,核对实际到诊和治疗进展。
**PAC 的业务价值 = 把患者数据从「散乱的成本」变成「集团可复用的判断力与行动力」。**
这些指标用于机构试点与复盘;可查询范围取决于实际数据接入。计划“完成”不等于转化成功,具体收益以实际执行结果衡量。
数据成资产、判断自动化、行动即取即用;并且一次投入、多处复用,全程可追溯。
开始前核对[数据准备](/docs/start/data-quality),日常组织方式见[召回分配](/docs/design/batch-assignment)。
---
title: 数据准备与结果核对
description: 核对覆盖、内容、更新与后续处理,让召回机会有可靠的依据。
icon: ClipboardCheck
---
召回结果依赖机构已经接入的数据。上线前和日常使用中,需要确认记录是否齐全、内容是否可用,以及新变化是否已经参与判断。
## 先核对四件事
| 核对项 | 重点 |
|---|---|
| 覆盖 | 患者、门店、病历、诊断、实际治疗、预约和到诊等主体是否覆盖约定范围 |
| 内容 | 诊断与治疗是否有实际内容,牙位、患者归属、时间和状态是否正确 |
| 更新 | 新增、订正、取消和删除是否持续同步,源记录到平台之间是否有延迟 |
| 后续处理 | 已接收的数据是否形成事实,并继续更新画像和召回计划 |
**推送接收成功只说明已完成对应接收步骤。** 还应核对事实、画像与计划,以及失败或待处理情况。某类数据有记录,也不能代替全量覆盖核对。
## 选择代表性患者核对
建议同时选取:有明确诊断且未见后续治疗、已经完成相关治疗、近期到诊、已有未来预约、拒绝联系,以及跨门店就诊的患者。
依次查看源业务系统记录、患者事实、画像和召回理由。已做治疗需要核对实际执行记录;未来治疗计划和病历处置文字不能直接当作已经完成治疗。
影像与影像分析分别核对:有影像文件不代表已有分析结果,上传时填写的影像描述也不等同 AI 分析。分析还需要正确关联患者及对应影像,并保留来源。
## 发现结果不一致时
提供机构与门店、患者业务编号、源记录编号、发生时间,以及具体不一致的字段或召回理由。接入人员据此判断问题发生在源数据、传输、标准化还是后续处理。
- **未曾同步的记录**:需要源系统补推,或按已配置的拉取方式补取。
- **已留存但映射错误的记录**:可核实保存的原文后重新解析;涉及判断结果时,再确认画像和计划更新。
- **更新较晚的记录**:核对增量游标与源端延迟。回看窗口能缓解晚到,不能覆盖所有超出窗口的历史变化。
缺少治疗记录时,先核对完整性再联系患者。系统的召回机会用于运营跟进和到院评估,不能代替医生对当前情况的判断。
技术接入与排查分别见[数据对接](/docs/integration/overview)和[数据核对](/docs/data-reconciliation)。
{
"title": "开始",
"icon": "BookOpen",
"pages": ["what-is-pac", "business-model", "business-value"]
"pages": [
"what-is-pac",
"business-model",
"business-value",
"data-quality"
]
}
---
title: 产品概述
description: PAC(Patient Analysis Center)是一套集团级的患者数据中枢。它把散落在各门诊系统里的患者就诊数据,收敛成一套统一、不可变的患者事实账本,在其上自动派生患者画像(客户价值 / 流失风险 / 沟通偏好等),再转化为客服可执行的回访触达(召回清单 + AI 话术),并把执行结果回流,持续变聪明。
description: 从已有诊疗记录中发现召回机会,帮助团队筛选、分工、沟通和跟进。
icon: Sparkles
---
## 三层模型
客户召回面向口腔门诊与连锁机构。它结合已有病历、治疗、到诊、预约和消费记录,整理值得进一步核实的治疗需求,帮助主管组织工作、客服准备沟通,并记录每次联系的结果。
从下到上三层,上层只依赖下层:
**让团队清楚:该联系谁、为什么联系、谁来负责,以及联系之后如何继续。**
| 层 | 做什么 | 说明 |
|---|---|---|
| **事实** | 把**任何宿主**(5i5ya / Friday / 数仓…)的原始数据,标准化成统一口径的**患者事实时间线**(不可变、可追溯) | **底座** —— 数据脏、口径乱的活全在这里收口 |
| **画像** | 在标准事实上**自动评价每个患者**(价值 / 流失风险 / 怎么沟通) | 集团的**长期数据资产** |
| **触达** | 把画像变成"**该打谁、为什么打、怎么打**"的清单 + AI 话术,执行结果再回流 | 当前的主要出口 |
## 从记录到日常工作
> **当前旗舰场景 = 老客户召回**(把"该回来但还没回来"的患者找出来,排好序、配好话术交给客服)。
> 但召回只是第一个出口 —— 标准化的**事实层 + 画像层可复用**于精准营销、客户分层、风险预警、主管 dashboard 等。
>
> 深入:[架构总览](/docs/architecture/overview) · [三层模型](/docs/architecture/three-layer-model) · [业务价值](/docs/start/business-value)
```mermaid
flowchart TD
A[机构已有业务记录] --> B[统一患者事实]
B --> C[画像与召回机会]
C --> D[主管筛选与分配]
D --> E[客服沟通与跟进]
E --> F[记录反馈与下一步]
```
---
## PAC 的边界
| 能力 | 帮助团队解决的问题 |
|---|---|
| 机会识别 | 从已有诊断或建议中,核对尚未看到对应治疗的需求 |
| 患者画像 | 集中了解就诊、消费、服务关系和需求背景 |
| 批次分配 | 按本轮目标选人,结合专属客服关系和在手任务量安排负责人 |
| 沟通准备 | 查看关键事实、病历快读和 AI 电话、企微沟通参考 |
| 执行跟进 | 记录反馈、再次联系时间及退回原因,查看工作推进情况 |
按维度归纳「确定做」与「确定不做」,划清 PAC 与现有系统的职责。
当前主要场景是[尚未启动治疗的召回](/docs/start/business-model)。使用范围和数据覆盖以机构实际接入与授权为准。
| 维度 | ✅ 确定做 | ❌ 确定不做 |
|---|---|---|
| **数据主权与隔离** | 标准化宿主数据,已写入的事实流水保留原始 payload 备查;集团(租户)**硬隔离**,互不可见 | 不充当源数据的**权威系统** —— 源数据主权归宿主;不跨集团串数据 |
| **数据范围** | 接收并标准化就诊、治疗、缴费、影像 metadata、病历结构化字段等**宿主侧事实** | 不当**病历系统** —— 完整临床病历(EMR)原文留在宿主 / 医生侧 |
| **事实与产物分层** | 画像、计划、AI 产物归 PAC,**独立存放、全链路可追溯** | 不把自身产物**回写成事实** —— 事实只装宿主侧事实,PAC 产物与事实流严格分层 |
| **智能产出** | 自动派生**患者画像**(价值 / 流失风险 / 沟通偏好,带证据链);生成**召回清单 + 优先级 + 召回原因 + AI 话术 + 一页摘要**;**客服侧 AI 助手**(工作台 / 企微问答,MCP 工具) | 不**自做影像 AI 诊断** —— 影像 finding 由宿主 / 三方产出,PAC 只摄入并降权使用 |
| **触达与执行** | 产出清单、话术、摘要供客服使用,并**接收执行结果回填**,驱动召回状态机 | 不负责触达的**具体执行** —— 何时、用什么渠道联系,由使用方决定 |
## 有依据,也有责任人
**协作模式**:PAC 是「大脑」,**host-可插拔**,通过契约跟现有业务系统协作 —— 不取代任何系统。
召回理由可以回看相关诊断、牙位、时间及治疗记录。AI 协助整理资料、查询和拟定沟通内容;主管确认分工,工作人员核对事实并开展联系,医生负责医疗评估。
---
影像分析采用已接入的上游结果,标明其来源,并作为辅助线索使用。系统不自行对影像作临床确诊。
## 一句话归纳
## 与现有业务系统协作
**PAC = 集团级患者数据中枢:事实账本(标准化)→ 画像(看清)→ 触达(行动)。**
源业务系统继续负责病历、收费和预约等原始记录。客户召回保留接入记录和事实版本,生成画像、计划与沟通参考;后续源数据更新和执行反馈分别参与结果更新。
召回是当前的旗舰场景;**事实层 + 画像层是集团的长期资产**。不取代现有系统,通过契约协作。
已开通预约对接的机构,可按授权办理相应业务操作。微信确认、操作提交和源系统实际成功需要分别核对。
延伸阅读:[核心场景](/docs/start/business-model) · [业务价值](/docs/start/business-value)
接入前先查看[数据准备与结果核对](/docs/start/data-quality);产品价值见[业务价值](/docs/start/business-value)。技术文档中使用 PAC 指代客户召回服务与患者数据中枢。
// 与当前 Fumadocs UI 的翻译键保持一致;站点路由继续使用 /docs。
export const translations = {
'Search(search trigger)': '搜索文档',
'Search(search dialog)': '搜索文档',
'Open Search(search trigger)(aria-label)': '打开搜索',
'Close Search(search dialog)(aria-label)': '关闭搜索',
'No results found(search dialog)': '未找到相关内容',
'On this page(table of contents)': '本页内容',
'Table of Contents(inline table of contents)': '本页内容',
'No Headings(table of contents)': '本页没有小标题',
'Copy Markdown(page actions)': '复制文档',
'Open(page actions)': '查看文档',
'View as Markdown(page actions)': '查看 Markdown',
'Open in GitHub(page actions)': '在源码仓库查看',
'Edit on GitHub(edit page)': '在源码仓库编辑',
'Open in ChatGPT(page actions)': '在 ChatGPT 中阅读',
'Open in Claude(page actions)': '在 Claude 中阅读',
'Open in Cursor(page actions)': '在 Cursor 中阅读',
'Open in Scira AI(page actions)': '在 Scira AI 中阅读',
'Read {url}, I want to ask questions about it.(page actions)': '请阅读 {url},我想了解其中的内容。',
'Previous Page(pagination)': '上一篇',
'Next Page(pagination)': '下一篇',
'Last updated on(page footer)': '最近更新',
'Copy Anchor Link(heading anchor)(aria-label)': '复制章节链接',
'Copy Link(accordion)(aria-label)': '复制链接',
'Copy Text(code block)(aria-label)': '复制内容',
'Copied Text(code block)(aria-label)': '已复制',
'Collapse Sidebar(sidebar)(aria-label)': '收起目录',
'Open Sidebar(sidebar)(aria-label)': '打开目录',
'Toggle Menu(mobile menu)(aria-label)': '切换菜单',
'Toggle Theme(theme switcher)(aria-label)': '切换外观',
'Light(theme switcher)(aria-label)': '浅色',
'Dark(theme switcher)(aria-label)': '深色',
'System(theme switcher)(aria-label)': '跟随系统',
'Page Not Found(404 page)': '未找到页面',
'Back to Home(404 page)': '返回首页',
'The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.(404 page)': '页面可能已迁移,请从目录或搜索查找。',
};
export const appName = 'PAC 文档';
export const appName = 'Friday 产品文档';
export const docsRoute = '/docs';
export const docsImageRoute = '/og/docs';
export const docsContentRoute = '/llms.mdx/docs';
......
......@@ -3,7 +3,7 @@
"info": {
"title": "FridayAI 宿主接入契约",
"version": "1.0.0",
"description": "2026-10-08 本地实现,测试/生产尚未部署。与 PAC 同形 JSON 凭据及用户上下文;未开放业务 API。"
"description": "宿主后端换票、组织授权、免登与退出契约。按应用与客户配置开放角色、范围和模块;业务数据与预约回写分别开通。"
},
"servers": [
{
......
......@@ -231,7 +231,7 @@ PAC_RECALL_SUBSCENARIO_CONCURRENCY=4
# gap 计算形态:legacy(默认,逐行相关子查询)/ setbased(集合式)。
# ⚠️ 生产**未启用**。集合式已在测试机验证零差异且再省 22~31%,但未经生产验证。
# 见 docs/design/gap-set-based-rewrite-plan.md。
# 见 docs/_archive/design/gap-set-based-rewrite-plan.md。
PAC_GAP_VARIANT=
# FridayAI 服务级只读密钥;与 AI_PAC_READ_SECRET 一致。openssl rand -hex 32 生成。
......
......@@ -123,7 +123,7 @@ export interface GapCoreInput {
* 计算形态。**只换形态,不换口径** —— 两者必须逐 (患者×信号×牙位) 完全一致。
* 'legacy' 逐 (患者×信号) 行跑相关子查询(2026-08 之前唯一形态)
* 'setbased' 先按 (患者,牙位) 预聚合全部 resolved 证据,再做反连接
* 默认 legacy。切换与验证见 docs/design/gap-set-based-rewrite-plan.md,
* 默认 legacy。切换与验证见 docs/_archive/design/gap-set-based-rewrite-plan.md,
* 对拍工具 src/cli/verify-gap-equivalence.cli.ts —— **改任何一边都要重跑对拍**。
*/
variant?: GapVariant;
......
......@@ -8,7 +8,7 @@ import type { NarrowFacts } from './cohort-attributes.service';
/**
* assignment-signals —— 引导节点的**确定性判定**。
*
* 规格见 docs/design/assignment-agent-flow.md §五。五条设计规则,逐条对应到代码:
* 规格见 docs/_archive/design/assignment-agent-flow.md §五。五条设计规则,逐条对应到代码:
* ① **只陈述事实,不给建议** —— `why` 写「他们的专属客服已排满」,
* ⛔ 不写「建议你铺平」。选项中性并列,给建议就是替主管做决定。
* ② **默认永远是 no-op** —— 每个节点的 `defaultLabel` 描述的都是"不动会怎样",
......
......@@ -513,7 +513,7 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
const sb = gap.setBased;
return sb
? // ══ 集合式:候选 → 患者域 → resolved 预聚合 → 牙位反连接 ══
// 见 docs/design/gap-set-based-rewrite-plan.md §4。gap_cand 必须 MATERIALIZED:
// 见 docs/_archive/design/gap-set-based-rewrite-plan.md §4。gap_cand 必须 MATERIALIZED:
// 它被下游扫三次(gap_scope / gap_rem / 主查询),内联会让 ①②③④ 闸重算三遍。
Prisma.sql`
WITH gap_cand AS MATERIALIZED (
......@@ -592,7 +592,7 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
// (患者×信号×牙位) **零差异**,场景段再省 22~31%。但它是召回与画像共用的
// 单一真理源,1600 行改动 + 一套对拍工具,且**尚未在生产验过**。
// 并发已把窗口解决掉之后,性价比要重新权衡 —— 见
// docs/design/gap-set-based-rewrite-plan.md。
// docs/_archive/design/gap-set-based-rewrite-plan.md。
// ══════════════════════════════════════════════════════════════════
// ⚡ 调试开关:PAC_RECALL_DUMP_SQL=1 时把本子场景的完整 SQL 打出来。
......
......@@ -12,7 +12,7 @@ import {
import { modelFacts } from '../src/modules/plan/assignment-facts';
/**
* 引导节点的判定 —— 规格见 docs/design/assignment-agent-flow.md §五。
* 引导节点的判定 —— 规格见 docs/_archive/design/assignment-agent-flow.md §五。
*
* 这里锁的是**判定本身**(什么情况该出、severity 怎么排、默认是不是 no-op),
* ⛔ 不锁措辞 —— 措辞会随产品走查改,锁住它只会让每次改文案都红一片。
......
......@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
/**
* 引导节点区 —— 确认单上「还有什么要您定的」。
*
* 规格见 docs/design/assignment-agent-flow.md §五。这一层是**纯呈现**:
* 规格见 docs/_archive/design/assignment-agent-flow.md §五。这一层是**纯呈现**:
* ⛔ 不判定该不该亮(那是服务端 `computeSignals` 的事),⛔ 不执行动作(只把 intent 抛上去)。
*
* ── 三条来自教条的硬要求 ────────────────────────────────────────
......
# docs/
# 文档入口
PAC 的文档已迁移到独立文档站 **`apps/pac-docs`**(Fumadocs)。
PAC 客户召回与 FridayAI 的正式产品、原理和接口文档统一维护在 `apps/pac-docs/content/docs/`。本地预览:`pnpm --filter @pac/docs dev`,http://localhost:3102。
- 本地预览:`pnpm --filter @pac/docs dev` → http://localhost:3102
- 文档源:`apps/pac-docs/content/docs/**`(MDX,单一来源)
- API 参考:由 `pnpm docs:openapi` 从 NestJS spec 自动生成
## 正式说明
## `_archive/`
- [客户召回](../apps/pac-docs/content/docs/start/what-is-pac.mdx):产品概述、核心场景、业务价值与数据核对。
- [FridayAI](../apps/pac-docs/content/docs/friday-ai/overview.mdx):开通、接待、人工协作、预约服务、知识与组织管理。
- [系统对接](../apps/pac-docs/content/docs/integration/overview.mdx):按宿主约定的数据、登录与业务回写契约。
历史快照:迁移前的原始 `.md`(含 v1/v0 旧版、周报、已合并的算法/场景文档)。
**只读留档,不再维护**;现行内容以 `apps/pac-docs` 为准。
接口参数以现行契约为准,产品页面链接过去,不另维护销售稿或交付副本。API 参考通过 `pnpm docs:openapi` 从服务 spec 生成。
## 内部资料
| 位置 | 内容 |
|---|---|
| [operations/](operations/README.md) | 日常部署、平台与托管命令、宿主操作配置入口 |
| [development/](development/README.md) | 本地端口、启动和专用企微调试 |
| [qa/](qa/README.md) | 待复核事项、历史约定及本次文档核对依据 |
## 历史资料
临时规划、旧流程、环境快照、验收报告和重复销售稿移入 [_archive/](_archive/README.md),保留原文件名、引用与移动清单。
设计准则、行为埋点和架构决策已全部归档至 [_archive/design/](_archive/design/) 与 [_archive/adr/](_archive/adr/)。
Claude Code 的编号资料见 [FridayAI 开发档案](_archive/friday-ai/docs/README.md)。未结事项见[跟进清单](qa/follow-ups.md),归档不表示解决。环境现状需要重新查询;当前行为以代码和正式文档为准。
{
"date": "2026-10-14",
"branch": "codex/friday-ai-docs-archive",
"summary": {
"total_files": 4,
"groups": {
"_archive": 4
},
"link_edit_files": 10,
"kept_qa": {},
"existing_broken_links": 40
},
"moves": [
{
"from": "docs/adr/0001-tenant-group-model.md",
"to": "docs/_archive/adr/0001-tenant-group-model.md",
"reason": "用户明确要求将 docs/adr 与 docs/design 全部归档,保留原文与相关引用",
"before_sha256": "9b80680157233c078c8bd9f28aaca53a7ac8411d17f60427b2e4cbaa498a6168",
"after_sha256": "48a96aade4776ae0d55a7a50badcaec04076f73b89c9c2539a713f087529e2b1"
},
{
"from": "docs/design/agent-doctrine.md",
"to": "docs/_archive/design/agent-doctrine.md",
"reason": "用户明确要求将 docs/adr 与 docs/design 全部归档,保留原文与相关引用",
"before_sha256": "018cce7224486811c9428152ae7a4c5b905ff5992049f891a3bc799753de59da",
"after_sha256": "15df231e3dbae7779602df1ae808e0617003cd429638f718671bc7e1b772a074"
},
{
"from": "docs/design/assignment-principles.md",
"to": "docs/_archive/design/assignment-principles.md",
"reason": "用户明确要求将 docs/adr 与 docs/design 全部归档,保留原文与相关引用",
"before_sha256": "629309e6eb4ac39f9ee50890f079c9fdb2b64951eadb62c821c7bfdcaa086426",
"after_sha256": "95b9de8a1a2a6278594cdbbdc3d0b152af64d2aac292de1b63834d9a64acfc81"
},
{
"from": "docs/design/user-activities.md",
"to": "docs/_archive/design/user-activities.md",
"reason": "用户明确要求将 docs/adr 与 docs/design 全部归档,保留原文与相关引用",
"before_sha256": "bc4dabdf474ac5fb885ec674a2cc9ce4b84e5ebc62da33af1dc0ec91a3660933",
"after_sha256": "bc4dabdf474ac5fb885ec674a2cc9ce4b84e5ebc62da33af1dc0ec91a3660933"
}
],
"reference_edits": [
"docs/README.md",
"docs/_archive/2026-10-14-consolidation-move-manifest.json",
"docs/_archive/README.md",
"docs/_archive/design/assignment-agent-dev-plan.md",
"docs/_archive/design/assignment-agent-flow.md",
"docs/_archive/design/plan-assignment-doctrine.md",
"docs/_archive/adr/0001-tenant-group-model.md",
"docs/_archive/design/agent-doctrine.md",
"docs/_archive/design/assignment-principles.md",
"docs/development/README.md"
],
"backup": "/var/folders/1g/hvr2ddkn4mzcks4mc2_78hkc0000gn/T/pac-docs-adr-design-20261014-c8zf4wpf",
"verification": {
"all_four_files_sha256_match": true,
"old_directories_removed": true,
"new_broken_links": 0,
"existing_archive_broken_links": 40
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
# 历史文档归档
正式产品说明与接口契约维护在 `apps/pac-docs/content/docs/`;正在使用的内部说明见 [docs 入口](../README.md),未结事项与发布证据入口见 [跟进清单](../qa/follow-ups.md)。
归档保存历史设计、实验、排查与交付证据。文件名与原文保留;移动时修正引用。归档不等于问题关闭,不以历史环境快照判断当前运行状态。
## 2026-10-14 整理
| 分类 | 入口 | 移入文件 | 原位置与归档依据 |
|---|---|---:|---|
| FridayAI 开发档案 | [原文档索引](friday-ai/docs/README.md) | 340 | 旧 `docs/_archive/friday-ai/` 整体快照;含编号文档、原型、供应商资料和旧销售稿,正式产品说明已单独维护 |
| 接入研究与首期规划 | [integration/](integration/) | 4 | 旧宿主接入研究、PAC 免登核对、Friday 临时映射与首期回写计划;现行契约和操作手册已独立维护 |
| 阶段开发与实验 | [design/](design/) | 3 | 两份分配开发规划、集合式重写实验;保留当时的分期、裁决与性能结论 |
| 历史排查与验收 | [qa/](qa/) | 43 | 33 份记录、10 个仅由这些记录引用的附件;其后未结事项已汇总到跟进清单 |
| 旧版交付副本 | [交付索引](deliverables/README.md) | 2 | 旧 `docs/deliverables/`;现行免登契约在正式文档站维护 |
| 合计 | | **392** | 不含本次新增的索引和移动清单 |
完整移动清单:[2026-10-14-move-manifest.json](2026-10-14-move-manifest.json)。记录原路径、新路径、归档原因、移动前后 SHA-256 和引用修正文件,便于追溯。
## 同日第二轮整理
继续核对剩余文档,追加归档 **26 个文件**;另将 2 份有效开发手册移入 `docs/development/`。两轮累计新增归档 **418 个资料文件**。
| 分类 | 文件数 | 现行入口 |
|---|---:|---|
| 首次部署、平台、席位与回写说明 | 7 | [内部运维](../operations/README.md);客户说明已进入正式文档 |
| 普尔测试开通快照 | 1 | 现行宿主接入契约 |
| 设计讨论、旧流程与历史分析 | 5 | [分配原则](design/assignment-principles.md)、正式数据核对页面 |
| 日期化排查、发布记录及专属附件 | 11 | [跟进清单](../qa/follow-ups.md),含 9 份记录与 2 个附件 |
| 客户召回销售稿与 HTML 副本 | 2 | 正式产品概述、核心场景与业务价值 |
[第二轮移动清单](2026-10-14-consolidation-move-manifest.json)保存每个文件的路径、原因和校验值;[核对依据](../qa/2026-10-14-friday-ai-docs-consolidation.md)记录内容收敛与代码核查。旧稿保留原文,只修正移动影响的引用。
## 同日补充:ADR 与设计资料
按用户要求,`docs/adr/` 与 `docs/design/` 剩余 **4 个文件**全部归档,原目录已移除;三轮累计新增归档 **422 个资料文件**。
- 架构决策:[集团租户模型](adr/0001-tenant-group-model.md)。
- 设计资料:[分配原则](design/assignment-principles.md)、[Agent 原则](design/agent-doctrine.md)、[行为埋点](design/user-activities.md)。
原文保留,仅修正移动影响的引用。详见[移动清单](2026-10-14-adr-design-move-manifest.json)。正式产品与使用说明仍维护在文档站,归档中的原则和决策用于追溯。
## 原有归档
本目录原有的 v0/v1/v2 文档、周报、算法、场景及旧运维资料维持原结构。阅读具体条目时,以其记录日期与版本为准。
......@@ -8,6 +8,12 @@
| **日期** | 2026-06 |
| **关联** | 登录授权(`orgScope` / `tenant` 的对外契约):`apps/pac-docs/content/docs/integration/auth-login.mdx` |
## 当前适用约束(2026-10-14)
本文保留 2026-06 的模型决策。当前数据空间不必等同业务集团:Friday SaaS 按 manifest 使用共享 `friday-market`,源品牌进入 `source_unit`,客户隔离还依赖非空 `orgScope` 与固定组织根校验;不能仅凭 `tenant_id` 认定各品牌硬隔离。单命名空间宿主也不必把品牌写入 `source_unit`。
现行定义以 `apps/pac-service/data/friday/manifest.yaml`、组织范围解析及[登录契约](../../../apps/pac-docs/content/docs/integration/auth-login.mdx)为准。末尾迁移与验收说明属于当时结果,不能直接作为当前环境证据。
## 背景(Context)
早期 `tenant_id` 装的是**品牌**(同一集团下的多个品牌),数据权限则让 host 直接传
......
# 文档交付记录
此目录保留历史 Markdown 交付快照,不独立维护现行接口。向对接方提供最新说明时,使用 `apps/pac-docs/content/docs/` 中的正式页面及其 Markdown 导出。
- `friday-ai-host-integration-v1.md`:2026-10-08 的历史交付记录,原文保留。
- 现行页面:`apps/pac-docs/content/docs/integration/friday-ai-host.mdx`。
- 机器契约:`apps/pac-docs/openapi/friday-ai-host-integration-v1.json`,接口变更时同步核对。
<!-- 单一来源:apps/pac-docs/content/docs/integration/friday-ai-host.mdx。 -->
# FridayAI 宿主接入契约 v1
# FridayAI 宿主接入契约 v1(历史交付记录)
本页保留 2026-10-08 的交付快照,环境状态与开通数据不代表当前情况。现行接口统一维护在 [正式文档源](../../../apps/pac-docs/content/docs/integration/friday-ai-host.mdx)(文档站 `/docs/integration/friday-ai-host`);后续更新以正式页面为准。
| 项目 | 内容 |
|---|---|
......
# Agent 设计教条
> 给产品与业务读。**一条一句,不解释。**
> 完整论证与图示见 [agent-architecture.md](./agent-architecture.md)。
> 内部设计评审原则。**一条一句,不解释。**
> 这是设计要求,不表示每条均已由代码强制实现;现行分配规则见 [分配原则](assignment-principles.md)。
> 完整论证与图示见 [agent-architecture.md](agent-architecture.md)。
| | |
|---|---|
......
# 分配 Agent · 改造规划
> 把现有助手改造到 [assignment-agent-flow.md](./assignment-agent-flow.md) 定义的行为,
> 并按 [agent-doctrine.md](./agent-doctrine.md) 的教条整理代码组织。
> 把现有助手改造到 [assignment-agent-flow.md](assignment-agent-flow.md) 定义的行为,
> 并按 [agent-doctrine.md](agent-doctrine.md) 的教条整理代码组织。
| | |
|---|---|
......@@ -119,13 +119,13 @@
**26 的状态要说清楚**:
- ✅ [`tests/golden/assignment-golden.ts`](../../apps/pac-service/tests/golden/assignment-golden.ts)
- ✅ [`tests/golden/assignment-golden.ts`](../../../apps/pac-service/tests/golden/assignment-golden.ts)
—— 9 条用例,每条都来自**真实踩过的坑**且写明由来。
判定标准是「必须调到 / 必须没调」两个集合,⛔ 不锁完整顺序(模型多查一次不算错),
⛔ 不判文字(除了「已撤销」这种说了就会让主管停止补救的词)。
- ✅ `golden-set-integrity.spec.ts` —— 防腐:用例引用的工具是否还存在、每条有没有写由来、
id 有没有重复、`mustNotSay` 有没有被拿去锁措辞偏好。**这条进 CI。**
- ✅ **跑批 runner**(2026-08-15)—— [`tests/golden/run.ts`](../../apps/pac-service/tests/golden/run.ts),
- ✅ **跑批 runner**(2026-08-15)—— [`tests/golden/run.ts`](../../../apps/pac-service/tests/golden/run.ts),
`pnpm --filter @pac/service golden`。打**真 HTTP 端点**,不 mock 任何一层。
⚠️ 之前这条命令**写在用例文件的注释里、但根本不存在** —— 那 9 条用例从来没被跑过
(和 `basisNote` 同一个形状:只写不读)。
......
# 分配 · Agent 行为决策树
> 主管发起一批召回分配时,**人 / 模型 / 程序**各自在哪一步做什么。
> 通用原则见 [agent-doctrine.md](./agent-doctrine.md)(引用其编号);业务教条见 [plan-assignment-doctrine.md](./plan-assignment-doctrine.md)(引用 T 编号)。
> 通用原则见 [agent-doctrine.md](agent-doctrine.md)(引用其编号);业务教条见 [plan-assignment-doctrine.md](./plan-assignment-doctrine.md)(引用 T 编号)。
| | |
|---|---|
......
# 召回分配:现行原则与实现入口
内部设计索引,2026-10-14 对照代码整理。客户使用说明统一在[召回分配](../../../apps/pac-docs/content/docs/design/batch-assignment.mdx)与[主管助手](../../../apps/pac-docs/content/docs/design/assignment-agent.mdx)维护。
## 保留的设计原则
1. 分配围绕一轮运营目标,不以分完整个池子为目标。
2. 人数、名册、在手量和统计由程序取数计算;模型组织查询与表达,不凭空提供结果。
3. 人群与分配方式分别调整;改变人群重算时保留已确认的其他条件。
4. 优先维护已有专属关系;专属超量形成待处理事项,未经主管决定不自动改派。
5. 提示与按钮说明实际后果;不处理的默认行为不得替主管扩大分配。
6. 草稿只保存方案;确认执行后才建立任务。权限、当前归属和幂等由服务端校验。
7. 分配期限用于跟进与超期管理,到期不自动收回;退回、回收与整批撤销分别保留原因和结果。
8. 工作量、处理进度与转化结果分开统计;完成状态不能直接当作召回成功。
## 实现入口
| 责任 | 代码 |
|---|---|
| 客群选择、批量规模、专属优先与待分配 | `apps/pac-service/src/modules/plan/assignment-proposal.service.ts` |
| 草稿事实与引导计算 | `assignment-facts.ts`、`assignment-signals.ts`(同目录) |
| 分配确认、归属、批次查询与撤销 | `plan-assignment.service.ts` |
| 联系结果与计划状态 | `execution.service.ts`、`recall-suppression.ts` |
| 后台到期/暂停等处理 | `apps/pac-service/src/queues/stale-scan.service.ts` |
| 操作参数及显示枚举 | `packages/types/src/schemas/plan-assignment.ts` |
本轮确认:批量默认估算使用在岗人数 × 每日联系量 × 跟进天数;整批撤销窗口为 30 分钟,已打开或已离开本批的任务需按实际结果处理。精确值与并发边界以代码、共享契约和对应测试为准,不在多份文档复制实现细节。
## 历史依据
[业务教条](plan-assignment-doctrine.md)、[Agent 行为树](assignment-agent-flow.md)、[通用 Agent 论证](agent-architecture.md)保留讨论裁决、旧行为与迭代依据。其中草案、旧数字和环境快照不再作为当前行为说明。
[Agent 通用原则](agent-doctrine.md)用于设计评审,不表示每条原则均已由代码强制实现。
......@@ -5,11 +5,11 @@
| **状态** | ⚠️ **本文停在 2026-08-02**(当时 Gate 0 未闭合)。功能此后已落地并跑过真实批次 —— 本文保留为**当时的分期与裁决记录**,⛔ 别拿它判断"现在做到哪了" |
| **现状去哪看** | agent 那一层 → [assignment-agent-dev-plan.md](./assignment-agent-dev-plan.md)(P0–P7);行为规格 → [assignment-agent-flow.md](./assignment-agent-flow.md) |
| **日期** | 2026-08-02 |
| **教条** | [plan-assignment-doctrine.md](./plan-assignment-doctrine.md) —— T1-T20,**冲突处以教条为准** |
| **教条** | [plan-assignment-doctrine.md](plan-assignment-doctrine.md) —— T1-T20,**冲突处以教条为准** |
| **产出方式** | 四层并行方案 → 双路对抗批判 → 汇总(7 agent) |
> 依据:`/Users/luoqi/gitlab/pac/docs/design/plan-assignment-doctrine.md`(T1–T20 + 五之二/三/四 + 六·已定取舍)
> 依据:`/Users/luoqi/gitlab/pac/docs/_archive/design/plan-assignment-doctrine.md`(T1–T20 + 五之二/三/四 + 六·已定取舍)
> 输入:data / service / mcp / web 四份方案 + 挑刺报告 + 排期审查
> 本文是**开发文档**,不是讨论稿。教条冲突处以教条为准;四份方案冲突处由本文第 1 节的契约表裁决。
> 所有 `file:line` 已于 2026-08-02 工作树实测复核。
......@@ -67,7 +67,7 @@ Y 轴 窗口温度 plan_reasons 566,365 条 ← daysSince 现成,读时
→ 但 **25 秒对交互不可接受** → 转为一个**明确的性能任务**(索引 / 物化视图),
属于 S1 内部的工程项,**不是排期依赖**。
阶段划分改以**能力开放**为轴,见 [教条 · 七之二 阶段性开放规划](./plan-assignment-doctrine.md)(S1-S5)。
阶段划分改以**能力开放**为轴,见 [教条 · 七之二 阶段性开放规划](plan-assignment-doctrine.md)(S1-S5)。
---
......
......@@ -10,18 +10,18 @@
| **参与** | 产品(luoqi) + Claude |
| **进入规则** | **教条必须经产品确认才能写入本文**;未确认的一律留在「待确认」区 |
| **关联** | 认领机制现状见 `apps/pac-service/src/modules/plan/claim-guard.ts`;召回打分见 `priority-scorer.ts` |
| **开发规划** | [plan-assignment-dev-plan.md](./plan-assignment-dev-plan.md) —— 分期计划、契约裁决、风险登记(停在 2026-08-02) |
| **开发规划** | [plan-assignment-dev-plan.md](plan-assignment-dev-plan.md) —— 分期计划、契约裁决、风险登记(停在 2026-08-02) |
### 这条线一共六份文档,各写什么(⛔ 别在两份里写同一件事)
| 文档 | 写**什么** | 权威 |
|---|---|---|
| [agent-architecture.md](./agent-architecture.md) | 通用 agent 理论:控制面、上下文工程、引导节点的通用设计 | 与业务无关 |
| [agent-doctrine.md](./agent-doctrine.md) | 上一份的**一条一句摘要**(给产品读,引用编号 A/B/C…) | 同上 |
| [agent-doctrine.md](agent-doctrine.md) | 上一份的**一条一句摘要**(给产品读,引用编号 A/B/C…) | 同上 |
| **本文** | 分配业务的**为什么** —— 教条、取舍、走过的弯路 | 业务判断的最终依据 |
| [assignment-agent-flow.md](./assignment-agent-flow.md) | 分配 agent 的**是什么** —— 状态机、引导节点规格表、事实两段 | **实现按它对齐** |
| [assignment-agent-dev-plan.md](./assignment-agent-dev-plan.md) | 助手改造到 flow 的**分期**(P0–P7),含每期抓到的事故 | 进度真源 |
| [plan-assignment-dev-plan.md](./plan-assignment-dev-plan.md) | 2026-08-02 那次四层并行方案的**裁决记录** | 历史 |
| [assignment-agent-dev-plan.md](assignment-agent-dev-plan.md) | 助手改造到 flow 的**分期**(P0–P7),含每期抓到的事故 | 历史实施记录 |
| [plan-assignment-dev-plan.md](plan-assignment-dev-plan.md) | 2026-08-02 那次四层并行方案的**裁决记录** | 历史 |
> 🔴 **教条写"为什么",规格写"是什么",⛔ 别两边各写一遍。**
> 2026-08-15 吃过这个亏:落人算法在本文和 flow 各写了一遍,8-06 改了第三趟行为,
......@@ -1228,14 +1228,14 @@ patient_transactions 经 patient_id → 客观新预约(canonical_payload.cre
### 开工前必须闭合
见 [开发规划 · 第二节 Gate 0](./plan-assignment-dev-plan.md):
见 [开发规划 · 第二节 Gate 0](plan-assignment-dev-plan.md):
- **G0.1 已实测通过** ✅(见 §4.37)
- **G0.2 契约表评审**(D-1 ~ D-15)—— 未做,**开工前必须过一遍**
- **G0.3 产品决策清点** —— 见本文第七节剩余项
### 第一件事
**不是写新功能,是修前置洞** —— 见 [开发规划 · 第三节](./plan-assignment-dev-plan.md)的 F1-F5。
**不是写新功能,是修前置洞** —— 见 [开发规划 · 第三节](plan-assignment-dev-plan.md)的 F1-F5。
其中 F1(引擎丢归属不记账,**实测 5 处**)和 F4(`assign`/`recycle` 不校验 `scope.clinicIds`,跨诊所越权)
必须在任何分配代码之前落地,否则分配一上线就会出现「主管分的单静默消失」和「跨诊所越权」。
......
......@@ -49,4 +49,4 @@
按天线性增长但基数小(主管几十人量级)。真涨起来按 `created_at` 删旧即可 —— 索引已按
`(host_id, tenant_id, created_at)` 建好。⚠️ 别学 `plan_generation_logs`(每轮每患者一行,
测试服 5,350 万行 / 13 GB)拖到不得不停服才处理。
历史测试环境曾出现大量积压)拖到不得不停服才处理。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="../../../../friday-ai/design/contacts/support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
* { box-sizing: border-box; }
a { color: #002985; } a:hover { color: #01226A; }
</style>
</helmet>
<div style="display: flex; width: 820px; height: 560px; gap: 20px; background: #f8fafc; color: #0f172a; font-size: 12.5px; padding: 20px;">
<!-- ═══ 未绑定:认领搬走之后 ═══ -->
<div style="display: flex; width: 380px; flex-shrink: 0; flex-direction: column; gap: 8px;">
<div style="font-size: 10.5px; font-weight: 600; letter-spacing: .5px; color: #94a3b8;">会话右栏 · 还没绑</div>
<div style="display: flex; flex-grow: 1; flex-direction: column; border-radius: 10px; background: #ffffff; overflow: hidden;">
<div style="display: flex; height: 44px; flex-shrink: 0; align-items: center; border-bottom: 1px solid #f1f5f9; padding: 0 12px;">
<div style="font-size: 13px; font-weight: 600;">患者</div>
</div>
<div style="display: flex; flex-grow: 1; flex-direction: column; gap: 14px; padding: 14px 12px;">
<div style="border-radius: 10px; background: #f8fafc; padding: 14px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: grid; place-items: center; width: 26px; height: 26px; border-radius: 9999px; background: #ffffff;">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6.2a2 2 0 1 1 2.7 1.9c-.5.2-.7.6-.7 1.1v.4"/><path d="M8 12.2h.01"/><circle cx="8" cy="8" r="6.2"/></svg>
</span>
<span style="font-size: 13px; font-weight: 600;">还不知道这是哪位患者</span>
</div>
<p style="margin: 6px 0 0; font-size: 11.5px; line-height: 1.75; color: #475569; text-wrap: pretty;">
绑定之后,这里会显示他的治疗与随访;发出去的消息也会记到他名下。
</p>
</div>
<div>
<div style="margin-bottom: 6px; font-size: 10.5px; font-weight: 600; letter-spacing: .5px; color: #94a3b8;">微信这边知道的</div>
<div style="overflow: hidden; border-radius: 10px; background: #f8fafc;">
<div style="display: grid; grid-template-columns: 88px 1fr; gap: 10px; border-bottom: 1px solid #ffffff; padding: 8px 12px;">
<div style="font-size: 11.5px; color: #64748b;">名称</div>
<div style="min-width: 0; font-size: 12px; font-weight: 600;">小满妈妈</div>
</div>
<div style="display: grid; grid-template-columns: 88px 1fr; gap: 10px; border-bottom: 1px solid #ffffff; padding: 8px 12px;">
<div style="font-size: 11.5px; color: #64748b;">通道</div>
<div style="min-width: 0; font-size: 12px; font-weight: 600;">个微好友</div>
</div>
<div style="display: grid; grid-template-columns: 88px 1fr; gap: 10px; padding: 8px 12px;">
<div style="font-size: 11.5px; color: #64748b;">备注手机号</div>
<div style="min-width: 0; font-size: 12px; font-weight: 600;">138****2570</div>
</div>
</div>
</div>
<div style="margin-top: auto; display: flex; flex-direction: column; align-items: center; gap: 8px;">
<button style="display: inline-flex; height: 36px; align-items: center; justify-content: center; gap: 6px; border-radius: 8px; background: #0032A0; padding: 0 14px; font-size: 12px; font-weight: 500; color: #ffffff;">
去通讯录绑定
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5 10.5 8 6 12.5"/></svg>
</button>
<button style="display: inline-flex; height: 24px; align-items: center; justify-content: center; border-radius: 4px; padding: 0 6px; font-size: 11px; font-weight: 500; color: #002985; text-decoration: underline; text-underline-offset: 2px;">
或在对话里问一句手机号
</button>
</div>
</div>
</div>
</div>
<!-- ═══ 已绑定:这块没变 ═══ -->
<div style="display: flex; width: 380px; flex-shrink: 0; flex-direction: column; gap: 8px;">
<div style="font-size: 10.5px; font-weight: 600; letter-spacing: .5px; color: #94a3b8;">会话右栏 · 已绑(这块没变)</div>
<div style="display: flex; flex-grow: 1; flex-direction: column; border-radius: 10px; background: #ffffff; overflow: hidden;">
<div style="display: flex; height: 44px; flex-shrink: 0; align-items: center; border-bottom: 1px solid #f1f5f9; padding: 0 12px;">
<div style="font-size: 13px; font-weight: 600;">患者</div>
<button style="margin-left: auto; display: inline-flex; height: 24px; align-items: center; justify-content: center; border-radius: 4px; padding: 0 6px; font-size: 11px; font-weight: 500; color: #64748b;">在通讯录里改</button>
</div>
<div style="display: flex; flex-grow: 1; flex-direction: column; gap: 14px; padding: 14px 12px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="display: grid; place-items: center; width: 34px; height: 34px; flex-shrink: 0; border-radius: 9999px; background: #F0F4FF; color: #002985; font-size: 14px; font-weight: 600;">周</span>
<div style="min-width: 0;">
<div style="display: flex; align-items: center; gap: 6px;">
<span style="font-size: 13.5px; font-weight: 600;">周小满</span>
<span style="display: inline-flex; height: 18px; align-items: center; border-radius: 4px; padding: 0 6px; background: #DCE7FE; color: #002985; font-size: 10.5px; line-height: 1;">对方是家长</span>
</div>
<div style="margin-top: 2px; font-size: 11.5px; color: #64748b;">女 · 138****2570 · 2014-03-22</div>
</div>
</div>
<div style="overflow: hidden; border-radius: 10px; background: #f8fafc;">
<div style="display: grid; grid-template-columns: 88px 1fr; gap: 10px; border-bottom: 1px solid #ffffff; padding: 8px 12px;">
<div style="font-size: 11.5px; color: #64748b;">病历号</div>
<div style="min-width: 0; font-size: 12px; font-weight: 600;">1958233</div>
</div>
<div style="display: grid; grid-template-columns: 88px 1fr; gap: 10px; border-bottom: 1px solid #ffffff; padding: 8px 12px;">
<div style="font-size: 11.5px; color: #64748b;">微信</div>
<div style="min-width: 0; font-size: 12px; font-weight: 600;">小满妈妈</div>
</div>
<div style="display: grid; grid-template-columns: 88px 1fr; gap: 10px; padding: 8px 12px;">
<div style="font-size: 11.5px; color: #64748b;">关联方式</div>
<div style="min-width: 0; font-size: 12px; font-weight: 600;">人工确认</div>
</div>
</div>
<div>
<div style="margin-bottom: 6px; font-size: 10.5px; font-weight: 600; letter-spacing: .5px; color: #94a3b8;">治疗与随访</div>
<div style="border-radius: 10px; background: #f8fafc; padding: 14px; text-align: center;">
<div style="font-size: 12px; color: #64748b;">当前治疗、潜在治疗、EMR 时间线</div>
<div style="margin-top: 3px; font-size: 11px; color: #94a3b8;">还没有数据源,等接上再显示</div>
</div>
</div>
<p style="margin: auto 0 0; font-size: 10.5px; line-height: 1.6; color: #94a3b8; text-wrap: pretty;">
往后发出去的消息都会记到周小满名下。说话的是家长。
</p>
</div>
</div>
</div>
</div>
</x-dc>
</body>
</html>
{
"artboards": [
{ "file": "Main.dc.html", "x": 0, "y": 0, "w": 1440, "h": 900, "title": "待绑(主状态)" },
{ "file": "BindDrawer.dc.html", "x": 1520, "y": 0, "w": 1440, "h": 900, "title": "绑定抽屉" },
{ "file": "Bound.dc.html", "x": 0, "y": 1020, "w": 1440, "h": 900, "title": "已绑" },
{ "file": "Colleagues.dc.html", "x": 1520, "y": 1020, "w": 1440, "h": 900, "title": "同事(纯派生)" },
{ "file": "ConvPanel.dc.html", "x": 0, "y": 2040, "w": 820, "h": 560, "title": "会话右栏 · 认领搬走之后" }
],
"annotations": [
{
"id": "rule-uniqueness",
"x": -360,
"y": 0,
"w": 300,
"text": "规则:一个微信绑几位患者,由 tenants.patient_dedup_kind 决定\n\n瑞尔集团现在是 ''(全集团唯一)⇒ 一个微信只绑 1 位。\n\n⚠️ 这个字段划的不是「跨租户」,是「租户内部划到哪一层」。docs/11 §2.4 写着瑞尔的真实情况是「品牌内唯一」—— 一旦改成 'brand',同一个人在瑞尔和瑞泰就是两份档案,一个微信在同一个租户里挂两位。\n\n跨租户反而天然分开:不同租户 = 不同 account_id,contact_links 的键第一位就是它。"
},
{
"id": "three-buckets",
"x": -360,
"y": 460,
"w": 300,
"text": "三档全是算出来的,零新增状态\n\n· 已绑 = 有生效的 contact_links\n· 待绑 = external / 陌生人,且没有 link\n· 同事 = wecom_contacts.kind = 'internal'\n\n⭐ 没有「人工移出」这个动作。wecom_contacts 是 ECom 的镜像 —— 每一列都是它给的,写入口只有 sync 和 reconcile。往里塞我们自己的状态会破坏这个不变式。\n\n代价:供应商、同行这类「外部但不是患者」的人会一直留在待绑里。第一版接受。"
},
{
"id": "gap-key-too-loose",
"x": -360,
"y": 900,
"w": 300,
"text": "⚠️ 唯一键太松\nuq_cl 现在是 (account_id, peer_vid, host_code, host_patient_id) —— 允许一个微信挂任意多位患者。\n\n按上面的规则,正确的键是 (account_id, peer_vid, dedup_scope_ref)。contact_links 里没有 dedup_scope_ref 这一列。\n\n代码目前靠 link() 主动置失效来收紧成一条 —— 行为对,但那是应用层约束,数据库拦不住。"
},
{
"id": "gap-relation",
"x": -360,
"y": 1280,
"w": 300,
"text": "⚠️ 表要加列:relation\ncontact_links 没有「关系」列。\n\n它不决定「消息记给谁」(一个微信只对一位患者),而是记「说话的人是谁」—— 家长代述和患者自述,在病历里不是一回事。\n\n这是要动的唯一一张表,且只加列。"
},
{
"id": "gap-automatch",
"x": -360,
"y": 1600,
"w": 300,
"text": "⚠️ 自动匹配还没实现\ncontact-link.service.ts 里 matchMethod 写死 'manual'。remark_phones 同步了、也显示了,但从没拿去 PAC 匹配过。\n\n这一页的效率全押在「按备注手机号找到 1 位」那条上 —— 不实现,这页就退化成手动一个个搜。"
},
{
"id": "move-claim",
"x": 0,
"y": -190,
"w": 700,
"text": "认领从会话右栏搬到这里(最下方那块画的是搬走之后会话右栏剩什么)。\n⚠️ 代价:客服正跟一个没绑的人聊天时要跳出会话。所以未绑状态给的是一颗「去通讯录绑定」直达按钮,带着这个联系人跳过去,不是让他自己找。\n\n人名、手机号、病历号都是编的;页签上的数字按一个真实前台号的规模估的。当前测试号的实际构成是 67 同事 + 7 企微外部 + 2 个微,其中只有 1 个备注了手机号。"
}
],
"launch": { "view": "canvas" }
}
This source diff could not be displayed because it is too large. You can view the blob instead.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="../../../../friday-ai/design/patient-claim/support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
body { margin: 0; background: #ffffff; color: #0f172a; font-size: 12.5px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
-webkit-font-smoothing: antialiased; }
a { color: #0032A0; } a:hover { color: #002985; }
* { box-sizing: border-box; }
</style>
</helmet>
<div style="display: flex; width: 340px; height: 760px; flex-direction: column; background: #ffffff; border-right: 1px solid #e2e8f0">
<div style="display: flex; height: 56px; flex-shrink: 0; align-items: center; gap: 8px; border-bottom: 1px solid #e2e8f0; padding: 0 16px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.8" stroke-linecap="round"><path d="M15 18l-6-6 6-6"></path></svg>
<div style="font-size: 13px; font-weight: 600">关联患者</div>
</div>
<div style="flex-shrink: 0; padding: 14px 16px 8px">
<div style="display: flex; align-items: center; gap: 8px; border-radius: 8px; border: 1px solid #e2e8f0; padding: 9px 12px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.9" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path></svg>
<span style="font-size: 12.5px">王磊</span>
</div>
</div>
<!-- 这一态的全部意义:坦白说"光靠这些分不出来" -->
<div style="margin: 6px 16px 10px; border-radius: 8px; border: 1px solid #ffe4e6; background: #fff1f2; padding: 10px 12px">
<div style="display: flex; gap: 8px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#be123c" stroke-width="1.9" stroke-linecap="round" style="flex-shrink: 0; margin-top: 1px"><circle cx="12" cy="12" r="9"></circle><path d="M12 8v4.5"></path><path d="M12 16h.01"></path></svg>
<div style="font-size: 11px; line-height: 1.7; color: #be123c">
<b>5 位同名,全是男性,手机号又是掩码</b> —— 界面上只剩<b>出生年份</b>能分。
选错等于把 A 的会话记到 B 的病历上。<b>先问一句再选。</b>
</div>
</div>
</div>
<div style="flex-shrink: 0; padding: 0 16px 8px">
<div style="display: flex; align-items: center; gap: 8px; border-radius: 8px; border: 1px solid #e2e8f0; background: #f8fafc; padding: 9px 12px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
<span style="font-size: 11.5px; color: #475569">发一句「方便说下您的出生年份吗」</span>
</div>
</div>
<div style="display: flex; flex: 1; flex-direction: column; gap: 6px; overflow: hidden; padding: 0 16px">
<div style="display: flex; align-items: baseline; gap: 6px">
<span style="font-size: 11px; font-weight: 600; color: #475569">找到 5 位</span>
<span style="font-size: 10.5px; color: #94a3b8">按出生年份排</span>
</div>
<sc-for list="{{hits}}" as="p" hint-placeholder-count="5">
<div style="display: flex; align-items: center; gap: 10px; border-radius: 10px; border: 1px solid #e2e8f0; padding: 10px 12px">
<div style="display: grid; place-items: center; width: 26px; height: 26px; flex-shrink: 0; border-radius: 13px; background: #f1f5f9; font-size: 11px; font-weight: 600; color: #475569">王</div>
<div style="min-width: 0; flex: 1">
<div style="display: flex; align-items: center; gap: 6px">
<span style="font-size: 12.5px; font-weight: 600">王磊</span>
<span style="border-radius: 4px; background: #f1f5f9; padding: 1px 5px; font-size: 10.5px; color: #475569">男</span>
<span style="border-radius: 4px; background: {{p.yBg}}; padding: 1px 6px; font-size: 11px; font-weight: 600; color: {{p.yFg}}">{{p.year}} 年</span>
</div>
<div style="margin-top: 3px; font-size: 10.5px; color: #94a3b8">{{p.phone}} · 病历号 {{p.mrn}}</div>
</div>
</div>
</sc-for>
</div>
<div style="flex-shrink: 0; border-top: 1px solid #e2e8f0; padding: 12px 16px">
<div style="display: flex; align-items: center; justify-content: center; border-radius: 6px; background: #f1f5f9; padding: 9px 12px; font-size: 12px; font-weight: 500; color: #94a3b8">先选一位</div>
<div style="margin-top: 8px; font-size: 10.5px; line-height: 1.7; color: #94a3b8">
⚠️ 设计问题:PAC 现在只给姓名/性别/生日/病历号/掩码号。要更好分辨,得让 PAC 再给「最近就诊日期」。
</div>
</div>
</div>
</x-dc>
<script data-dc-script data-props='{"$preview":{"width":340,"height":760}}'>
class Component extends DCLogic {
renderVals() {
// 真数据:本地 PAC 库里 source_unit='瑞尔' 恰好有 5 个王磊
return {
hits: [
{ year: '1968', phone: '138****3278', mrn: '1914548', yBg: '#fef3c7', yFg: '#b45309' },
{ year: '1981', phone: '138****3456', mrn: '876106', yBg: '#fef3c7', yFg: '#b45309' },
{ year: '1988', phone: '138****3881', mrn: '1438595', yBg: '#fef3c7', yFg: '#b45309' },
{ year: '1990', phone: '138****4480', mrn: '1886137', yBg: '#fef3c7', yFg: '#b45309' },
{ year: '1993', phone: '138****8019', mrn: '1324139', yBg: '#fef3c7', yFg: '#b45309' },
],
};
}
}
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="../../../../friday-ai/design/patient-claim/support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
body { margin: 0; background: #ffffff; color: #0f172a; font-size: 12.5px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
-webkit-font-smoothing: antialiased; }
a { color: #0032A0; } a:hover { color: #002985; }
* { box-sizing: border-box; }
</style>
</helmet>
<div style="display: flex; width: 340px; height: 700px; flex-direction: column; background: #ffffff; border-right: 1px solid #e2e8f0">
<div style="display: flex; height: 56px; flex-shrink: 0; align-items: center; gap: 8px; border-bottom: 1px solid #e2e8f0; padding: 0 16px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.8" stroke-linecap="round"><path d="M15 18l-6-6 6-6"></path></svg>
<div style="font-size: 13px; font-weight: 600">关联患者</div>
</div>
<div style="flex-shrink: 0; padding: 14px 16px 8px">
<div style="display: flex; align-items: center; gap: 8px; border-radius: 8px; border: 1px solid #e2e8f0; padding: 9px 12px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.9" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path></svg>
<span style="font-size: 12.5px">13812343405</span>
</div>
</div>
<!-- ⚠️ 这条是整个流程里最容易出人命的一条:号对上了不等于人对上了 -->
<div style="margin: 6px 16px 10px; border-radius: 8px; border: 1px solid #fde68a; background: #fffbeb; padding: 10px 12px">
<div style="display: flex; gap: 8px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#b45309" stroke-width="1.9" stroke-linecap="round" style="flex-shrink: 0; margin-top: 1px"><path d="M12 9v4.5"></path><path d="M12 17h.01"></path><path d="M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"></path></svg>
<div style="font-size: 11px; line-height: 1.7; color: #92400e">
这个号在 PAC 里标着<b>未核实</b> —— 测试库的手机号大多是造数的。<b>别只凭号确认</b>,跟患者对一下姓名或生日。
</div>
</div>
</div>
<div style="display: flex; flex: 1; flex-direction: column; gap: 8px; overflow: hidden; padding: 0 16px">
<div style="font-size: 11px; font-weight: 600; color: #475569">找到 2 位</div>
<sc-for list="{{hits}}" as="p" hint-placeholder-count="2">
<div style="border-radius: 10px; border: 1px solid {{p.bd}}; background: {{p.bg}}; padding: 12px; box-shadow: {{p.ring}}">
<div style="display: flex; align-items: center; gap: 10px">
<div style="display: grid; place-items: center; width: 30px; height: 30px; flex-shrink: 0; border-radius: 15px; background: {{p.avBg}}; font-size: 12px; font-weight: 600; color: {{p.avFg}}">{{p.short}}</div>
<div style="min-width: 0; flex: 1">
<div style="display: flex; align-items: center; gap: 6px">
<span style="font-size: 13px; font-weight: 600">{{p.name}}</span>
<span style="border-radius: 4px; background: #f1f5f9; padding: 1px 6px; font-size: 10.5px; color: #475569">{{p.sex}}</span>
<sc-if value="{{p.fake}}" hint-placeholder-val="{{true}}">
<span style="border-radius: 4px; background: #fef3c7; padding: 1px 6px; font-size: 10.5px; color: #b45309">号未核实</span>
</sc-if>
</div>
<div style="margin-top: 3px; font-size: 11px; color: #64748b">{{p.phone}} · {{p.birth}}</div>
</div>
</div>
<div style="margin-top: 9px; display: flex; gap: 10px; font-size: 10.5px; color: #94a3b8">
<span>病历号 {{p.mrn}}</span>
</div>
</div>
</sc-for>
</div>
<div style="flex-shrink: 0; border-top: 1px solid #e2e8f0; padding: 12px 16px">
<div style="display: flex; align-items: center; justify-content: center; border-radius: 6px; background: {{b600}}; padding: 9px 12px; font-size: 12px; font-weight: 500; color: #ffffff">确认关联到 张伟</div>
<div style="margin-top: 8px; text-align: center; font-size: 10.5px; line-height: 1.7; color: #94a3b8">关联后这条会话的消息会记到他名下,可以再改。</div>
</div>
</div>
</x-dc>
<script data-dc-script data-props='{"brand":{"editor":"color","default":"#0032A0","options":["#0032A0","#0d9488","#059669"],"section":"品牌"},"$preview":{"width":340,"height":700}}'>
class Component extends DCLogic {
mix(hex, w) { const n = parseInt(hex.slice(1), 16); const c = [(n>>16)&255,(n>>8)&255,n&255].map((v)=>Math.round(v+(255-v)*w)); return '#'+c.map((v)=>v.toString(16).padStart(2,'0')).join(''); }
dark(hex, k) { const n = parseInt(hex.slice(1), 16); const c = [(n>>16)&255,(n>>8)&255,n&255].map((v)=>Math.round(v*k)); return '#'+c.map((v)=>v.toString(16).padStart(2,'0')).join(''); }
renderVals() {
const b600 = this.props.brand ?? '#0032A0';
const b100 = this.mix(b600, 0.86), b50 = this.mix(b600, 0.95), b700 = this.dark(b600, 0.82);
return {
b600,
// 真数据:来自本地 PAC 库 source_unit='瑞尔'
hits: [
{ short: '张', name: '张伟', sex: '男', phone: '138****2570', birth: '1992-02-06', mrn: '1941351',
fake: true, bg: b50, bd: b600, ring: '0 0 0 1px ' + b600, avBg: b100, avFg: b700 },
{ short: '张', name: '张伟', sex: '男', phone: '138****9014', birth: '1975-08-30', mrn: '1203884',
fake: true, bg: '#ffffff', bd: '#e2e8f0', ring: 'none', avBg: '#f1f5f9', avFg: '#475569' },
],
};
}
}
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="../../../../friday-ai/design/patient-claim/support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
body { margin: 0; background: #ffffff; color: #0f172a; font-size: 12.5px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
-webkit-font-smoothing: antialiased; }
a { color: #0032A0; } a:hover { color: #002985; }
* { box-sizing: border-box; }
</style>
</helmet>
<div style="display: flex; width: 340px; height: 720px; flex-direction: column; background: #ffffff; border-right: 1px solid #e2e8f0">
<div style="display: flex; height: 56px; flex-shrink: 0; align-items: center; border-bottom: 1px solid #e2e8f0; padding: 0 16px">
<div style="font-size: 13px; font-weight: 600">患者</div>
<div style="margin-left: auto; display: flex; align-items: center; gap: 4px; font-size: 11px; color: #94a3b8">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 13v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"></path><path d="M12 3v12"></path><path d="M8 7l4-4 4 4"></path></svg>
改绑
</div>
</div>
<div style="flex-shrink: 0; padding: 16px 16px 12px">
<div style="display: flex; align-items: center; gap: 11px">
<div style="display: grid; place-items: center; width: 40px; height: 40px; flex-shrink: 0; border-radius: 20px; background: {{b100}}; font-size: 15px; font-weight: 600; color: {{b700}}">张</div>
<div style="min-width: 0">
<div style="display: flex; align-items: center; gap: 7px">
<span style="font-size: 15px; font-weight: 600">张伟</span>
<span style="border-radius: 4px; background: #f1f5f9; padding: 1px 6px; font-size: 10.5px; color: #475569">男</span>
</div>
<div style="margin-top: 3px; font-size: 11px; color: #64748b">138****2570 · 1992-02-06</div>
</div>
</div>
<div style="margin-top: 12px; display: flex; flex-direction: column; gap: 1px; border-radius: 8px; border: 1px solid #e2e8f0; overflow: hidden">
<div style="display: flex; gap: 10px; padding: 9px 12px; font-size: 11.5px">
<span style="width: 58px; flex-shrink: 0; color: #94a3b8">病历号</span><span>1941351</span>
</div>
<div style="display: flex; gap: 10px; border-top: 1px solid #f1f5f9; padding: 9px 12px; font-size: 11.5px">
<span style="width: 58px; flex-shrink: 0; color: #94a3b8">诊所</span><span>北京朝阳公园</span>
</div>
<div style="display: flex; gap: 10px; border-top: 1px solid #f1f5f9; padding: 9px 12px; font-size: 11.5px">
<span style="width: 58px; flex-shrink: 0; color: #94a3b8">微信</span><span>五光(企微外部联系人)</span>
</div>
</div>
</div>
<!-- 关联的来历要能查。这是**人推断出来的**关系,不是系统知道的事实 -->
<div style="margin: 0 16px 14px; border-radius: 8px; background: #f8fafc; padding: 10px 12px">
<div style="display: flex; gap: 8px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.9" stroke-linecap="round" style="flex-shrink: 0; margin-top: 1px"><circle cx="12" cy="12" r="9"></circle><path d="M12 11v5"></path><path d="M12 8h.01"></path></svg>
<div style="font-size: 10.5px; line-height: 1.75; color: #64748b">
罗启 于 08-27 15:12 <b>人工确认</b>关联。这是人做的判断,出错时按这条追溯。
</div>
</div>
</div>
<!-- ⛔ 下面这块是**占位**:治疗/随访要等 PAC 读通道和 tasks 有数据。
⛔ 不画假的治疗记录 —— 画了就会有人以为功能已经有了。 -->
<div style="display: flex; flex: 1; flex-direction: column; gap: 10px; padding: 0 16px">
<div style="font-size: 11px; font-weight: 600; color: #475569">治疗与随访</div>
<div style="display: flex; flex: 1; flex-direction: column; align-items: center; justify-content: center; gap: 10px; border-radius: 10px; border: 1px dashed #e2e8f0; padding: 20px 16px">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16"></path><path d="M4 12h10"></path><path d="M4 18h13"></path></svg>
<div style="text-align: center; font-size: 11.5px; line-height: 1.8; color: #94a3b8">
当前治疗、潜在治疗、EMR 时间线<br>还没有数据源,等接上再显示
</div>
</div>
</div>
<div style="flex-shrink: 0; border-top: 1px solid #e2e8f0; padding: 12px 16px">
<div style="font-size: 10.5px; line-height: 1.7; color: #94a3b8">往后发出去的消息都会记到张伟名下。</div>
</div>
</div>
</x-dc>
<script data-dc-script data-props='{"brand":{"editor":"color","default":"#0032A0","options":["#0032A0","#0d9488","#059669"],"section":"品牌"},"$preview":{"width":340,"height":720}}'>
class Component extends DCLogic {
mix(hex, w) { const n = parseInt(hex.slice(1), 16); const c = [(n>>16)&255,(n>>8)&255,n&255].map((v)=>Math.round(v+(255-v)*w)); return '#'+c.map((v)=>v.toString(16).padStart(2,'0')).join(''); }
dark(hex, k) { const n = parseInt(hex.slice(1), 16); const c = [(n>>16)&255,(n>>8)&255,n&255].map((v)=>Math.round(v*k)); return '#'+c.map((v)=>v.toString(16).padStart(2,'0')).join(''); }
renderVals() {
const b600 = this.props.brand ?? '#0032A0';
return { b600, b100: this.mix(b600, 0.86), b700: this.dark(b600, 0.82) };
}
}
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="../../../../friday-ai/design/patient-claim/support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
body { margin: 0; background: #ffffff; color: #0f172a; font-size: 12.5px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
-webkit-font-smoothing: antialiased; }
a { color: #0032A0; } a:hover { color: #002985; }
* { box-sizing: border-box; }
</style>
</helmet>
<div style="display: flex; width: 340px; height: 620px; flex-direction: column; background: #ffffff; border-right: 1px solid #e2e8f0">
<div style="display: flex; height: 56px; flex-shrink: 0; align-items: center; gap: 8px; border-bottom: 1px solid #e2e8f0; padding: 0 16px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.8" stroke-linecap="round"><path d="M15 18l-6-6 6-6"></path></svg>
<div style="font-size: 13px; font-weight: 600">关联患者</div>
</div>
<div style="flex-shrink: 0; padding: 14px 16px 8px">
<div style="display: flex; align-items: center; gap: 8px; border-radius: 8px; border: 1px solid #e2e8f0; padding: 9px 12px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.9" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path></svg>
<span style="font-size: 12.5px">鸿铭</span>
</div>
</div>
<div style="display: flex; flex: 1; flex-direction: column; align-items: center; justify-content: center; gap: 14px; padding: 0 24px">
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="#cbd5e1" stroke-width="1.4" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path><path d="M8.5 11h5"></path></svg>
<div style="text-align: center">
<div style="font-size: 12.5px; font-weight: 600">北京朝阳公园没有叫「鸿铭」的患者</div>
<p style="margin: 8px 0 0; font-size: 11.5px; line-height: 1.8; color: #64748b">
可能是还没建档的新客,也可能他在别家诊所建的档。
</p>
</div>
</div>
<div style="flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; border-top: 1px solid #e2e8f0; padding: 14px 16px">
<!-- ⚠️ 刻意不给「在这里新建患者」—— 患者主档在 PAC/门诊系统,不在这里建。
给了就会有人用,然后两边各有一份档案。 -->
<div style="display: flex; align-items: center; gap: 8px; border-radius: 6px; border: 1px solid #e2e8f0; padding: 9px 12px; font-size: 11.5px; color: #475569">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="1.8" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path></svg>
换个词再搜(手机号最准)
</div>
<div style="display: flex; align-items: center; gap: 8px; border-radius: 6px; border: 1px solid #e2e8f0; padding: 9px 12px; font-size: 11.5px; color: #475569">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v12H7l-3 3z"></path></svg>
标记为「新客,暂不关联」
</div>
<p style="margin: 4px 0 0; font-size: 10.5px; line-height: 1.75; color: #94a3b8">
⛔ 这里<b>不能</b>新建患者档案 —— 档案在门诊系统里建,建完同步过来才搜得到。
</p>
</div>
</div>
</x-dc>
<script data-dc-script data-props='{"$preview":{"width":340,"height":620}}'>
class Component extends DCLogic {}
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="../../../../friday-ai/design/patient-claim/support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
body { margin: 0; background: #ffffff; color: #0f172a; font-size: 12.5px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
-webkit-font-smoothing: antialiased; }
a { color: #0032A0; } a:hover { color: #002985; }
* { box-sizing: border-box; }
</style>
</helmet>
<div style="display: flex; width: 340px; height: 620px; flex-direction: column; background: #ffffff; border-right: 1px solid #e2e8f0">
<div style="display: flex; height: 56px; flex-shrink: 0; align-items: center; gap: 8px; border-bottom: 1px solid #e2e8f0; padding: 0 16px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.8" stroke-linecap="round"><path d="M15 18l-6-6 6-6"></path></svg>
<div style="font-size: 13px; font-weight: 600">改绑患者</div>
</div>
<div style="display: flex; flex-direction: column; gap: 14px; padding: 16px">
<div style="border-radius: 10px; border: 1px solid #ffe4e6; background: #fff1f2; padding: 14px">
<div style="display: flex; gap: 8px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#be123c" stroke-width="1.9" stroke-linecap="round" style="flex-shrink: 0; margin-top: 1px"><circle cx="12" cy="12" r="9"></circle><path d="M12 8v4.5"></path><path d="M12 16h.01"></path></svg>
<div>
<div style="font-size: 12.5px; font-weight: 600; color: #be123c">这条会话已经有 41 条消息记在张伟名下</div>
<p style="margin: 7px 0 0; font-size: 11px; line-height: 1.75; color: #be123c">
改绑之后它们会转到新的患者名下。<b>如果之前是认错了人,那正是要改</b>;
如果只是想看另一个患者,请回列表另开一条会话。
</p>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px">
<div style="display: flex; min-width: 0; flex: 1; align-items: center; gap: 8px; border-radius: 8px; border: 1px solid #e2e8f0; padding: 10px 12px">
<div style="display: grid; place-items: center; width: 24px; height: 24px; flex-shrink: 0; border-radius: 12px; background: #f1f5f9; font-size: 10.5px; font-weight: 600; color: #475569">张</div>
<div style="min-width: 0">
<div style="font-size: 11.5px; font-weight: 600">张伟</div>
<div style="font-size: 10.5px; color: #94a3b8">1941351</div>
</div>
</div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0"><path d="M5 12h14"></path><path d="M13 6l6 6-6 6"></path></svg>
<div style="display: flex; min-width: 0; flex: 1; align-items: center; justify-content: center; border-radius: 8px; border: 1px dashed #cbd5e1; padding: 10px 12px; font-size: 11.5px; color: #94a3b8">
选新的
</div>
</div>
<div>
<div style="margin-bottom: 8px; font-size: 11px; font-weight: 600; color: #475569">为什么要改?<span style="font-weight: 400; color: #94a3b8">(会留档)</span></div>
<div style="display: flex; flex-direction: column; gap: 6px">
<sc-for list="{{reasons}}" as="r" hint-placeholder-count="3">
<div style="display: flex; align-items: center; gap: 9px; border-radius: 8px; border: 1px solid {{r.bd}}; background: {{r.bg}}; padding: 10px 12px; font-size: 11.5px">
<span style="width: 13px; height: 13px; flex-shrink: 0; border-radius: 7px; border: 1px solid {{r.dot}}; background: {{r.fill}}"></span>
{{r.t}}
</div>
</sc-for>
</div>
</div>
</div>
<div style="margin-top: auto; flex-shrink: 0; border-top: 1px solid #e2e8f0; padding: 12px 16px">
<div style="display: flex; align-items: center; justify-content: center; border-radius: 6px; background: #e11d48; padding: 9px 12px; font-size: 12px; font-weight: 500; color: #ffffff">确认改绑</div>
<div style="margin-top: 8px; font-size: 10.5px; line-height: 1.7; color: #94a3b8">
旧关联不删除,标成失效并留档 —— 「这条会话曾经记在谁名下」必须查得到。
</div>
</div>
</div>
</x-dc>
<script data-dc-script data-props='{"$preview":{"width":340,"height":620}}'>
class Component extends DCLogic {
renderVals() {
return {
reasons: [
{ t: '认错人了', bd: '#0032A0', bg: '#F0F4FF', dot: '#0032A0', fill: '#0032A0' },
{ t: '这个微信换人用了', bd: '#e2e8f0', bg: '#ffffff', dot: '#cbd5e1', fill: 'transparent' },
{ t: '患者在 PAC 里有重复档案', bd: '#e2e8f0', bg: '#ffffff', dot: '#cbd5e1', fill: 'transparent' },
],
};
}
}
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="../../../../friday-ai/design/patient-claim/support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
body { margin: 0; background: #ffffff; color: #0f172a; font-size: 12.5px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
-webkit-font-smoothing: antialiased; }
a { color: #0032A0; } a:hover { color: #002985; }
* { box-sizing: border-box; }
</style>
</helmet>
<div style="display: flex; width: 340px; height: 620px; flex-direction: column; background: #ffffff; border-right: 1px solid #e2e8f0">
<div style="display: flex; height: 56px; flex-shrink: 0; align-items: center; gap: 8px; border-bottom: 1px solid #e2e8f0; padding: 0 16px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#475569" stroke-width="1.8" stroke-linecap="round"><path d="M15 18l-6-6 6-6"></path></svg>
<div style="font-size: 13px; font-weight: 600">关联患者</div>
</div>
<div style="flex-shrink: 0; padding: 14px 16px 10px">
<div style="display: flex; align-items: center; gap: 8px; border-radius: 8px; border: 1px solid {{b600}}; box-shadow: 0 0 0 3px {{b50}}; padding: 9px 12px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.9" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path></svg>
<span style="font-size: 12.5px">王磊</span>
<span style="width: 1px; height: 15px; background: #0f172a"></span>
</div>
<div style="margin-top: 8px; font-size: 10.5px; line-height: 1.7; color: #94a3b8">姓名 / 手机号 / 病历号都能搜。只搜<b style="color: #475569">北京朝阳公园</b>的患者。</div>
</div>
<div style="display: flex; flex-direction: column; gap: 10px; padding: 6px 16px">
<sc-for list="{{skel}}" as="s" hint-placeholder-count="3">
<div style="display: flex; align-items: center; gap: 10px; border-radius: 10px; border: 1px solid #e2e8f0; padding: 12px">
<div style="width: 30px; height: 30px; flex-shrink: 0; border-radius: 15px; background: #f1f5f9"></div>
<div style="display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 6px">
<div style="height: 9px; border-radius: 4px; background: #f1f5f9; width: {{s.a}}"></div>
<div style="height: 8px; border-radius: 4px; background: #f8fafc; width: {{s.b}}"></div>
</div>
</div>
</sc-for>
</div>
<div style="margin-top: auto; flex-shrink: 0; border-top: 1px solid #e2e8f0; padding: 12px 16px">
<div style="font-size: 10.5px; line-height: 1.7; color: #94a3b8">正在问 PAC…</div>
</div>
</div>
</x-dc>
<script data-dc-script data-props='{"brand":{"editor":"color","default":"#0032A0","options":["#0032A0","#0d9488","#059669"],"section":"品牌"},"$preview":{"width":340,"height":620}}'>
class Component extends DCLogic {
mix(hex, w) { const n = parseInt(hex.slice(1), 16); const c = [(n>>16)&255,(n>>8)&255,n&255].map((v)=>Math.round(v+(255-v)*w)); return '#'+c.map((v)=>v.toString(16).padStart(2,'0')).join(''); }
renderVals() {
const b600 = this.props.brand ?? '#0032A0';
return { b600, b50: this.mix(b600, 0.95),
skel: [{ a: '58%', b: '76%' }, { a: '44%', b: '66%' }, { a: '52%', b: '70%' }] };
}
}
</script>
</body>
</html>
从代码抠出来的精确值(不许改):
字体 -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif
页底 #f8fafc 文字 #0f172a 根字号 12.5px
侧栏 228px 白 列表 296px #f8fafc 面板 340px 白
头部 h-14(56px) logo size-6(24px) rounded-[7px]
卡片 rounded-[10px] bg-white p-3 选中 ring-2 ring-brand-600
chip rounded(4px) px-1.5 py-px text-[10.5px]
头像 22px 圆 bg-brand-100 text-[11px] font-semibold text-brand-700
按钮 rounded-md(6px) px-3 py-1.5 text-[11.5px] font-medium
字号 10.5 / 11 / 11.5 / 12 / 12.5 / 13
brand 50 #F0F4FF 100 #DCE7FE 600 #0032A0 700 #002985
slate 50 #f8fafc 100 #f1f5f9 200 #e2e8f0 300 #cbd5e1 400 #94a3b8 500 #64748b 600 #475569 900 #0f172a
amber 50 #fffbeb 100 #fef3c7 200 #fde68a 700 #b45309
rose 50 #fff1f2 100 #ffe4e6 600 #e11d48 700 #be123c
{
"artboards": [
{ "file": "Main.dc.html", "x": 0, "y": 0, "w": 1440, "h": 860, "title": "① 未认领 · 全窗上下文" },
{ "file": "Search.dc.html", "x": 0, "y": 1060, "w": 340, "h": 620, "title": "② 搜索中" },
{ "file": "Candidates.dc.html", "x": 420, "y": 1060, "w": 340, "h": 700, "title": "③ 有候选 · 假号警告" },
{ "file": "Ambiguous.dc.html", "x": 840, "y": 1060, "w": 340, "h": 760, "title": "④ 同名歧义 · 5 个王磊" },
{ "file": "NotFound.dc.html", "x": 1260, "y": 1060, "w": 340, "h": 620, "title": "⑤ 搜不到" },
{ "file": "Claimed.dc.html", "x": 1680, "y": 1060, "w": 340, "h": 720, "title": "⑥ 已关联" },
{ "file": "Rebind.dc.html", "x": 2100, "y": 1060, "w": 340, "h": 620, "title": "⑦ 改绑" }
],
"annotations": [
{
"id": "scope",
"x": 1520, "y": 0, "w": 380,
"text": "范围:只画「从不知道这是谁 → 知道了」这一段。\n\n设计稿里的右侧面板(当前治疗/潜在治疗/EMR/时间线)没画 —— 那些还没有数据源,画了就是假数据。⑥ 里留了明确的占位。"
},
{
"id": "flow",
"x": 0, "y": 940, "w": 300,
"text": "② → ⑦ 按护士实际走的顺序排。"
},
{
"id": "gap-fields",
"x": 840, "y": 1860, "w": 340,
"text": "⚠️ 真问题,不是画着玩:\n本地 PAC 库里「瑞尔」有 5 个王磊,全是男性,手机号一掩码就只剩生日能分。\n\n要分得开,得让 PAC 的检索接口再给「最近就诊日期」。这是这次设计发现的接口缺口。"
},
{
"id": "fake-phone",
"x": 420, "y": 1800, "w": 340,
"text": "⚠️ 30000 个患者手机号目前全是 phone_verified=false(造数)。\n\n所以「号对上了」不能当成「人对上了」—— ③ 里那条黄色警告是必须的,不是装饰。"
},
{
"id": "brand",
"x": 2100, "y": 1720, "w": 340,
"text": "品牌色:代码里 brand-600 = #0032A0(PANTONE 286 C,蓝),你给的设计稿是绿的。\n\n我按代码做,并把主色做成可调项 —— 选中画板后在上方色板里点绿色就能整套切过去。"
}
],
"launch": { "view": "canvas" }
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"footnotes": false,
"properties": true,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"bases": true,
"webviewer": false
}
\ No newline at end of file
{
"main": {
"id": "1a19e69d243f8c64",
"type": "split",
"children": [
{
"id": "47823c4543f017fa",
"type": "tabs",
"children": [
{
"id": "95a9bec610e41cef",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "05-企微iPad协议PoC验证计划.md",
"mode": "preview",
"source": false
},
"icon": "lucide-file",
"title": "05-企微iPad协议PoC验证计划"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "b8c8a200ac936ed3",
"type": "split",
"children": [
{
"id": "942fcc7a45400e11",
"type": "tabs",
"children": [
{
"id": "d642bb2198e67ccf",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-folder-closed",
"title": "文件列表"
}
},
{
"id": "aa6b5f0a8c9486d8",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "搜索"
}
},
{
"id": "f67c5c4b785c1736",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-bookmark",
"title": "书签"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "584bd65460877a07",
"type": "split",
"children": [
{
"id": "06f7a9b6fa75b163",
"type": "tabs",
"children": [
{
"id": "2ba774dd51bdf7ed",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "05-企微iPad协议PoC验证计划.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "反向链接"
}
},
{
"id": "021af719bac39c09",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "05-企微iPad协议PoC验证计划.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "出链"
}
},
{
"id": "df2f711cf40b7394",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "标签"
}
},
{
"id": "6db19bb2610eb160",
"type": "leaf",
"state": {
"type": "all-properties",
"state": {
"sortOrder": "frequency",
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-archive",
"title": "添加笔记属性"
}
},
{
"id": "be81e9c5da519d53",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "05-企微iPad协议PoC验证计划.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "大纲"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"switcher:打开快速切换": false,
"graph:查看关系图谱": false,
"canvas:新建白板": false,
"daily-notes:打开/创建今天的日记": false,
"templates:插入模板": false,
"command-palette:打开命令面板": false,
"bases:新建数据库": false
}
},
"active": "95a9bec610e41cef",
"lastOpenFiles": [
"04-企微通道调研.md",
"03-边界与形态.md",
"02-架构决策与PAC映射.md",
"01-与现有系统的关系-PAC与FridaySaaS.md",
"00-需求梳理与产品设想.md"
]
}
\ No newline at end of file
# 05 · 工程约定
# 05 · 工程约定
> 仓库怎么建、有哪些技术基线和纪律。**开工前必读。**
---
## 1. 仓库结构
friday-ai 作为两个 app 进入 PAC 仓库(决策与理由见 [D7](03-决策记录.md#d7)):
```
pac/
├── apps/
│ ├── pac-service/ 现有
│ ├── pac-web/ 现有
│ ├── pac-docs/ 现有
│ ├── ai-service/ 新 —— 独立 DB、独立 prisma、独立部署单元
│ └── ai-web/ 新
├── packages/
│ ├── types/ @pac/types —— 两边共享(这是同仓的全部意义)
│ ├── utils/ 现有
│ └── host-connector/ 新 —— Friday SaaS 写回,只给 ai-service 用
```
`host-connector` 单独成包,是因为 SRS §21.4 要求"同一功能只能有一个确定的写入通道 + 幂等 + 失败补偿"。隔离成包才能在**类型层面**保证没人绕过它直接 fetch Host —— **光靠 code review 守不住**。
---
## 2. 四条 CI 闸(第一天就上)
| # | 闸 | 防什么 |
|---|---|---|
| 1 | **依赖白名单**:`apps/ai-service` 只允许依赖 `@pac/types` / `@pac/utils`,出现 `@pac/service` 即 fail | 绕过 HTTP 直调内部函数 |
| 2 | **独立数据库**:独立 `AI_DATABASE_URL` + 独立 prisma schema + 独立 generator output;CI 断言两个 URL 不相等 | 共用数据库 |
| 3 | **eslint `no-restricted-imports`**:禁止 `apps/*` 之间相对路径 import | 跨 app 偷渡 |
| 4 | **`turbo --filter` 分流**:friday-ai 的失败不染红 PAC 的 pipeline | CI 红灯疲劳 |
---
## 3. 对 PAC 现有环境要做的三处修改
| # | 改什么 | 为什么 |
|---|---|---|
| 1 | `deploy/deploy.sh` 第 5 步的全量 `pnpm build` → 加 `--filter` | 否则会顺带构建 ai-service |
| 2 | ⚠️ **`turbo.json` 的 `globalDependencies: ["**/.env", ...]` 收窄** | 现在 `**/.env` 是全局的,**ai-service 改自己的 `.env` 会让 PAC 的 turbo 缓存全部失效、触发全量重建**。这是同仓唯一一个真正硌到日常开发的点 |
| 3 | 第二个 Prisma generator 配独立 `output` | 否则两个 `@prisma/client` 打架。好消息:PAC 的 db 脚本本来就是 `--filter` 式的 |
**合计约半小时。** 除此之外 PAC 现有的开发、构建、CI、部署流程都不用动。
---
## 4. 技术基线(照抄 PAC,不要另起炉灶)
| 项 | 约定 | 来源 |
|---|---|---|
| **多租户隔离** | `tenantId` 进每张表的唯一键 + 每条查询的 where | PAC ADR-0001 |
| **时间类型** | 统一 `@db.Timestamptz(3)`;无时刻概念的用 `@db.Date` | PAC schema 原则 8 |
| **幂等** | 对外写操作用**服务端铸造**的 `requestId`;**冲突不报错**,按 requestId 回查已有结果 | PAC `plan_assignments` |
| **响应信封** | HTTP 恒 200,业务结果看信封 `code`(非 0 = 错误) | PAC 全局约定 |
| **时间/金额归一化** | 宿主接入时提供 `timezone` + `amount_unit`,由平台侧归一;出站统一 ISO 8601 带时区 + 整数分 | PAC 接入约定 |
| **错误码** | 沿用 PAC 的码表,不另起一套 | PAC `error-codes` |
> 幂等那条的精髓在 PAC 的注释里:**"抛错会让模型以为失败、换个参数再试一次,那才是真正的重复分配。"**
---
## 5. 建表纪律(五条硬约束)
### 1. ⭐ friday-ai 不存任何患者数据的副本
**不存姓名、手机、诊断、预约、病历 —— 只存 `pacPatientId` 一个引用。**
列表要显示患者名时,由 ai-service 调 PAC 批量补齐(照 PAC 的 dictionary 思路)。
> 这条是 [D9](03-决策记录.md#d9)「读全走 PAC」在表层的落地。一旦开了"冗余一下患者姓名方便查询"的口子,半年后就是第二套患者主数据。
### 2. 所有业务表引本地 `users.id`,不引外部 userId
见 [D8](03-决策记录.md#d8)。**这条跟 PAC 不同,别照抄 PAC 存宿主 userId 的做法。**
### 3. 操作主体三态
```
actorType: 'human' | 'agent' | 'system'
actorId
actorVersion? ← agent 时填,对应 NFR-006 版本可追溯
```
**第一天定错要动全表。** PAC 的 `operatorUserId` 假设了操作者都是人,friday-ai 不能这么假设。
### 4. 任务状态用 SRS §10.3 的 9 态,别自创
且 **`AI已执行` 与 `已完成` 必须是两个状态**(前者动作完成,后者业务结果达成)。
### 5. 会话挂通道,不挂患者
`conversation` 挂 `channel_contact`(一对一 or 群),`channel_contact` 再映射到 N 个 `(host, pacPatientId)`。理由见 [04 §6](04-通道方案.md)。
---
## 6. 命名与术语
- 代码里统一用 `Diagnosis` 表示**治疗契约**(= SRS 的 Diagnosis = TreatmentContract)
- `AgentTask` = 一次业务工作;`AgentJob` = Task 内的一个执行步骤。**不要混用**
- PAC 的 `diagnosis_record` 是**临床诊断记录**,与上面的 `Diagnosis` 是不同层次的东西,涉及时显式加前缀区分(如 `pacDiagnosisRecord`)
---
## 7. 分批建表计划
| 批次 | 内容 | 阻塞 |
|---|---|---|
| **① 核心**(约 12 张表) | 身份与组织 · Agent 治理 · Task/Job 状态机 · 人机协同 · 审计 | **无,可立即定稿** |
| **② 会话** | channel_contact · conversation · message · outbound_message | 需先定 [04 §6](04-通道方案.md) 的会话模型(一对一/群、发言人 vs 关联患者、@触发) |
| **③ 写回** | host_action 账本(幂等 / 结果确认 / 补偿) | 等 `docking-gateway` 盘点,才知道幂等键怎么构造 |
### 第一批的四个建模前置问题(已有建议,待确认)
1. **Conversation 挂通道联系人**(见纪律 5)
2. **Task 来源两类要区分**:`source=pac_plan` + planId(治疗机会 Agent 消费 PAC 召回池)vs `source=rule` + 自有规则版本(预约确认 Agent 的"明日预约")
3. **触达账本:friday-ai 记明细,PAC 记计数** —— PAC 有 patient 级 `contactAttempts`/`snoozedUntil`,friday-ai 的 `outbound_message` 是明细(发了什么、通道回执、成功失败),**不要在 friday-ai 再维护一套频控**
4. **Agent 的"定义"与"运行配置"分三张表**:`agent`(业务定义,只读)+ `agent_version`(策略/话术/prompt,可灰度回滚)+ `agent_enablement`(按组织节点启停)。塞进一张表就做不了版本治理(NFR-006)
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment