# multi-source-parse **Repository Path**: medical-light-years/multi-source-parse ## Basic Information - **Project Name**: multi-source-parse - **Description**: 面向医院信息科、质控办与医保办的异构监管材料归一系统,把飞检反馈、卫健指标、医保扣款等多源多格式材料按模板解析为结构化工单,经过校验、升级、派发、确认四道关闭环,责任人显式确认才推进状态,并附带事务级审计链与一键导出证据包,全程支持热加载模板与三级覆写路由。 - **Primary Language**: TypeScript - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-23 - **Last Updated**: 2026-08-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 监管多源解析归一系统 > 医院信息科 / 质控办 / 医务科把飞检反馈、卫健指标、医保扣款等异构监管材料丢进同一个口子,自动抽字段、建工单、按责任人派发、按通道落盘,等人确认后再回写。 监管材料来源多、格式杂、模板变化快,单靠人工抄录分派经常漏单、漏时、漏责任人。本系统把这些材料按模板解析成结构化工单,再经过校验、升级、派发、确认四道关,由责任人显式确认后才推进状态,飞检和周复核时可一键导出证据包。 ## 适用场景 / 目标岗位 | 岗位 | 什么时候用 | 得到什么 | |------|------------|----------| | 信息科工程师 | 收到监管材料文件或接口推送,需要按模板入库并派单 | 标准化工单 + 派发清单 + 失败原因 + 重试入口 | | 质控办专干 | 飞检反馈、卫健月报到达,要分发到对应科室并跟踪整改 | 工单分派表 + 整改时限 + 升级提醒 | | 医保办专干 | 收到扣款清单和申诉材料,要走院内申诉流程 | 扣款台账 + 申诉材料清单 + 时点追踪 | | 医务科主任 | 需要看本周监管工单进度和处理率 | 工单状态看板 + 责任人确认率 + 升级工单列表 | | 主管副院长 | 周/月复核或飞检迎检 | 一键导出的证据包(工单 + 审计链 + 派发回执) | > **表格纪律**:本表只写岗位口语 + 业务时刻 + 可核对产出;REST 路径、字段名、状态枚举等放到「命令 / API / 配置说明」。 ## 能力要点 - **多源解析归一**:飞检反馈 PDF、卫健指标 Excel、医保扣款 CSV/Excel、HIS/EMR 导出文件,按模板抽出统一字段,模板变更热加载不重启服务 - **工单状态机闭环**:7 状态(draft → validated → dispatched → escalated → confirmed → reported → closed)严格迁移,并发安全、失败可回放 - **三级覆写路由**:医院级、模板级、责任级 override 按优先级合并,高优先级命中短路,支持信息科远程维护 - **多通道派发**:file / http / 邮件 / OA 等 sink 适配器,失败按策略重试,派发记录全程留痕 - **审计证据包**:每步带 transaction_id、actor、ts、evidence 的完整步骤链,飞检前可一键导出 ZIP 证据包 - **字段脱敏**:邮箱、手机、患者标识、姓名按档位自动脱敏,审计导出前再过一遍 ## 快速开始 ```bash # 1. 安装依赖 npm install # 2. 复制配置 cp .env.example .env # 按需修改 PORT / DB_PATH / INPUT_DIR 等 # 3. 同步模板(GitOps 热加载) npm run sync # 4. 启动服务(含 REST API + 预览页) npm start # 5. 启动输入目录监听(可选,新文件自动 ingest) npm run start:watcher # 6. 手动投递单个文件(可选) npm run ingest -- ./input/feijian_20260811_001.pdf # 7. 验证:跑测试 npm test # 或访问预览页 curl http://127.0.0.1:3000/preview/ ``` ## 命令 / API / 配置说明 ### CLI 子命令 | 命令 | 作用 | 备注 | |------|------|------| | `npm start` | 启动 REST API + 预览页 | 默认 PORT=3000 | | `npm run start:watcher` | 启动 input/ 目录轮询监听 | 防抖 2s | | `npm run sync` | 同步 templates/ 目录到运行引擎 | 不重启服务(详见下方 `sync` 节) | | `npm run ingest -- ` | 手动投递单个文件 | 按文件名前缀识别来源 | | `npm test` | 跑 vitest 测试套件 | 含 parser / workflow / sink / API | | `npm run build` | 编译 TypeScript → dist/ | - | ### `sync` 子命令(GitOps 热加载) 把 `templates/` 下全部 YAML 推入运行引擎,**不重启服务**。信息科改完模板直接 `npm run sync` 即可生效,下次新工单按新 version 解析;旧工单仍按写入时的 version 回放(保证审计可追溯)。 | Flag | 默认 | 说明 | |------|------|------| | `--templates-dir ` | `./templates` | 模板 YAML 目录(递归扫 `*.yaml` / `*.yml`) | | `--db-path ` | `./data/multi-source-parse.db` | SQLite DB 路径(首跑会自动 bootstrap 迁移) | | `--actor ` | `cli:sync` | 审计 actor 标识(落 audit_log 的 `actor` 字段) | 行为说明: - 扫到新模板 → 写入 `version=1`,旧记录保留为 `active=0`(无) - 内容变更(sha256 不同)→ 写入 `version=N+1`,旧 version 软标记为 `active=0` 但仍可查 - 文件从目录中删除 → `active=0` 软删(getActiveTemplate 返回 null,旧 version 仍可按 getTemplateByVersion 回放) - 校验失败(缺字段、类型错、escalate_after > deadline、filename ≠ code)→ 不入 DB,errors 段列出 - 同步完成后写一条 `audit_log`(step=`sync`,evidence 含 added/modified/deleted/errors 摘要 + transaction_id) 退出码: | 退出码 | 含义 | |--------|------| | `0` | 成功(含部分 file 错误——errors 仅作摘要,不视为致命) | | `1` | 未捕获异常 | | `66` | 模板目录不存在(DIRECTORY_NOT_FOUND) | | `73` | audit_log 写入失败(AUDIT_WRITE_FAILED) | 示例输出(修改一份模板后再 sync): ``` [sync] OK tx=6be6c3f6-8206-4d79-90be-e5923b4aee98 dir=./templates [sync] duration=13ms [sync] summary: added=0 modified=1 deleted=0 errors=0 [sync] modified: ~ feijian.hand_hygiene v1 -> v2 ``` ### `ingest` 子命令(手动投递单文件) 把单份监管材料投递到解析归一管线(不依赖 watcher)。**走与 REST `/api/ingest` 完全一致的同进程入口**(`src/cli/ingest.ts → runIngestForFile`),便于 CI / 一次性批跑。 文件名规则:`__.`,例如: ``` feijian_20260811_001.json weijian_20260811_002.csv yibao_20260811_003.json his_export_20260811_004.csv emr_export_20260811_005.xml ``` | Flag | 默认 | 说明 | |------|------|------| | `` | (必填) | 待投递的文件绝对路径 | | `--source ` | 由文件名解析 | 显式 source_kind(`feijian` / `weijian` / `yibao` / `his_export` / `emr_export`) | | `--template-code ` | 由 source_kind 派生 | 显式模板编码(如 `feijian.hand_hygiene`) | | `--hospital-grade ` | 无 | 医院等级 hint(三级甲等 / 三级乙等 / 二级甲等 / 二级乙等 / 其他) | | `--reporting-caliber ` | 无 | 上报口径 hint(`national` / `provincial` / `city` / `hospital`) | | `--no-run-workflow` | `false`(默认跑) | 仅建工单不跑 5 步流水线,便于先解析后人工确认 | | `--actor ` | `cli:ingest` | 审计 actor 标识 | 退出码: | 退出码 | 含义 | |--------|------| | `0` | 成功 | | `64` | 空文件 / 未指定 template_code(EX_USAGE) | | `66` | 文件不存在 / 文件名无法解析 source_kind(EX_NOINPUT) | | `73` | bootstrap DB 失败(EX_CANTCREAT) | | `1` | 未捕获异常 | 示例: ```bash $ npm run ingest -- ./input/feijian_20260811_001.json { "ok": true, "transaction_id": "…", "ticket_id": 42, "ticket_uid": "T-…", "is_new": true, "status": "confirmed", "sha256": "…", "source": "feijian", "template_code": "feijian.hand_hygiene", "filename": "feijian_20260811_001.json", "size_bytes": 1234, "parser_error": null, "run_result": { "final_outcome": "confirmed", ... }, "duration_ms": 87 } ``` ### `start:watcher` 守护进程(持续监听 input/) 把整个 `./input/` 目录当成「自动投递口」:新文件按 `source_yyyyMMdd_seq.ext` 规则被自动解析并入库。适合信息科把卫健直报 / 医保申诉下载目录直接挂载到 `./input/`,由本进程代为巡检。 ```bash $ npm run start:watcher [config] ensured dir …/input …/output …/logs …/templates [watcher] start: input_dir=./input db_path=./data/multi-source-parse.db actor=api:watcher: debounce=2000ms [watcher] ingested: feijian_20260811_001.json → ticket_id=42 status=confirmed is_new=true [watcher] ingested: weijian_20260811_002.csv → ticket_id=43 status=reported is_new=true … ``` | Flag | 默认 | 说明 | |------|------|------| | `--input-dir ` | `./input` | 监听目录(fs.watch + 2s 防抖 + 2 轮稳定性检查) | | `--db-path ` | `./data/multi-source-parse.db` | SQLite DB 路径 | | `--debounce-ms ` | `2000` | 防抖窗口;fs.watch 同写动作在窗口期内合并为一次 | | `--actor-prefix ` | `api:watcher` | 审计 actor 前缀;最终 actor = `:` | | `--no-run-workflow` | `false`(默认跑) | 仅建工单,不跑 5 步流水线 | 行为说明: - 文件名按 `^_\d{8}` 匹配 → `` ∈ `feijian` / `weijian` / `yibao` / `his_export` / `emr_export` - 大小写不敏感;隐藏文件(`.DS_Store`、`.gitkeep` 等)直接跳过 - 同 `sha256 + source_kind` 在进程内幂等:重复投递不再触发 ingest - 写盘未完成(文件还在被另一进程写入)→ 通过 `stability_checks`(连续 N 次 `stat.size` 不变)确认写盘完成 - ingest 失败(parse / DB / workflow 抛错)→ 写 audit `ingest.failed`,**进程不退出**(避免一条坏文件拖死整个 daemon) - `SIGINT` / `SIGTERM` → 关闭 fs.watch + 清 pending timer → `process.exit(0)` > **真实对接层**:watcher 与 ingest 共用 `runIngestForFile()` 同进程入口(不走 HTTP);REST `/api/ingest` 与 watcher 触发走同一份 `defaultRegistry.parse()` + `ensureIdempotent()` + `WorkflowEngine.run()`,业务语义完全一致,actor 字段区分调用来源(`api:user:` / `api:ingest:` / `api:watcher:` / `cli:ingest`)。 (Phase 4 后续 task 补齐:错误码表、Prom metrics 字段说明) ### 内置 sink 适配器 启动期自动 seed 4 个内置 sink(`./src/sinks/registry.ts` 的 `ensureDefaultSinks()`,重复 seed 走 INSERT OR IGNORE 幂等): | sink_id | kind | config_json 默认值 | 说明 | |---------|------|--------------------|------| | `file-default` | `file` | `{description: "./output/____.json"}` | 落盘到 `OUTPUT_DIR`,默认兜底 sink | | `http-default` | `http` | `{url: "", timeout_ms: 5000, description: "..."}` | **需信息科 / 卫健直报平台提供 webhook URL**;本期不配置 → 自动失败不派发 | | `email-default` | `email_mock` | `{description: "..."}` | mock sink:写 `./output/email_outbox.jsonl` + pino 日志 | | `oa-mock` | `oa_mock` | `{description: "..."}` | mock sink:写 `./output/oa_outbox.jsonl` + pino 日志 | #### `http-default` 真实对接 把 webhook URL 写入 `sinks.config_json` 即可启用真实对接(信息科直连院内部署的卫健直报平台 / 医保接口网关): ```sql UPDATE sinks SET config_json = json_set(config_json, '$.url', 'https://weijing.internal/api/feedback') WHERE id = 'http-default'; ``` **协议契约**(spec §closure_dimensions 5:正式对接层): | 元素 | 取值 | |------|------| | `method` | `POST` | | `Content-Type` | `application/json` | | `X-Transaction-Id` | ``(与 audit_log 同源;医院端可关联工单 ↔ 平台回执) | | `X-Attempt` | `<1..max_attempts>`(dispatch 重试序号,便于平台端 idempotency) | | body | `ticket_id / ticket_uid / template_code / template_version / responsible / fields / source / sink_rule / dispatched_at` | **重试分类(http sink 自评,dispatch 步骤按此重试 ≤3 次)**: | HTTP 状态 | `ok` | `retryable` | 含义 | |-----------|------|-------------|------| | 2xx | true | — | 派发成功 | | 4xx | false | false | 业务错 / 模板错 / 配置错 → 终态失败,不重试(避免对端持续告警) | | 5xx | false | true | 上游暂时不可达 → 重试 ≤3 次 | | 网络错误 / 超时 | false | true | 同上 | > **mock 端点仅用于本期验收**。本期任务(Task 25)使用本地 `http.createServer` 验证契约;**真实对接需后续 round 由信息科配置 URL**,未实连范围与 webhook 鉴权签名等留作下轮。 #### `email_mock` / `oa_mock` 替身边界 - 真实邮件 / OA 推送需要外部账号 / 鉴权 / 计费,本期均未接入 - mock sink 写 `./output/{email,oa}_outbox.jsonl`,每条含 `transaction_id / ticket_id / responsible / fields 摘要`,可直接 `jq` 回放 - **fixture ≠ 最终对接**:飞检 / 卫健直报等正式通道需后续 round 接入真实 SMTP / OA SDK ### REST 路径 | 方法 | 路径 | 用途 | |------|------|------| | GET | `/healthz` | 健康检查 | | GET | `/api/ingest/health` | 解析器注册表状态(kind 列表 + max_raw_bytes) | | POST | `/api/ingest` | 接收原始材料(JSON 形式,content_base64 编码) | | GET | `/api/tickets` | 列工单(分页、过滤) | | GET | `/api/tickets/:id` | 工单详情(展开 fields/source/responsible/sinks/parser_error) | | PATCH | `/api/tickets/:id/confirm` | 责任人确认(dispatched → confirmed) | | PATCH | `/api/tickets/:id/revoke` | 撤销(escalated → validated,必带 reason) | | POST | `/api/tickets/:id/replay` | 从指定步重跑 workflow | | POST | `/api/tickets/:id/run` | 跑完整 5 步 workflow | | GET | `/api/templates` | 列出所有 active 模板 | | GET | `/api/templates/:code/active` | 取指定 code 的 active version | | GET | `/api/templates/:code` | 取一个 code 的所有 version | | GET | `/api/templates/:code/versions/:version` | 取指定 (code, version) | | POST | `/api/templates` | 接收单份 YAML 文本并 sync | | POST | `/api/templates/sync` | 触发 loadTemplatesFromDir(GitOps) | | GET / POST / PUT / DELETE | `/api/overrides` | 三级覆写 CRUD | | GET | `/api/audit/:ticket_id` | 单工单完整审计链(默认按 ts ASC) | | GET | `/api/audit/by-transaction/:tx` | 按 transaction_id 跨工单查询 | | GET | `/api/audit/:ticket_id/export` | 导出 ZIP 证据包(Task 31 落地) | | GET | `/preview/:ticket_id` | 极简预览页(解析/派发/审计三视图,Task 29 落地) | | GET | `/metrics` | Prom 格式指标 | **`/api/ingest` body 形式**: ```json { "source": "feijian | weijian | yibao | his_export | emr_export", "content_base64": "<原始字节的 base64 编码>", "hint": { "template_code": "feijian.hand_hygiene", "hospital_grade": "三级甲等", "reporting_caliber": "national", "filename": "feijian_20260811_001.pdf", "extra": { "...": "..." } }, "template_code": "feijian.hand_hygiene", "run_workflow": true } ``` **`/api/tickets` 列表过滤**:`?status=draft|validated|dispatched|escalated|confirmed|reported|closed&template_code=xxx&limit=50&offset=0` **`/metrics` Prom 字段**(3 类计数器): | 指标 | labels | 含义 | |------|--------|------| | `multi_source_parse_tickets_total` | `status` | 工单按 status 分布累计计数 | | `multi_source_parse_sink_dispatch_total` | `sink_kind`, `status` | sink 派发按 sink_kind × status 累计计数 | | `multi_source_parse_parser_failure_total` | `kind`, `code` | 解析失败按 source_kind × error_code 累计计数 | **错误语义**(HTTP 状态码): | 状态 | 含义 | 例子 | |------|------|------| | `400 VALIDATION_ERROR` | zod 入参校验失败 | `content_base64: 不能为空` | | `400 INVALID_ID` | id 非正整数 | `/api/tickets/abc` | | `400 INVALID_BASE64` | content_base64 解码失败 | base64 字符非法 | | `400 MissingReasonError` | revoke 缺 reason | `PATCH /api/tickets/1/revoke` 不带 reason | | `404 TicketNotFoundError` | 工单不存在 | `GET /api/tickets/99999` | | `404 TEMPLATE_NOT_FOUND` | 模板不存在或未激活 | `GET /api/templates/zzz` | | `404 OVERRIDE_NOT_FOUND` | override 不存在 | `GET /api/overrides/999` | | `409 IllegalTransitionError` | 状态机拒绝非法迁移 | draft → confirmed | | `409 WorkflowRunRefusedError` | runWorkflow 时工单状态不允许 | confirmed 工单再 run | | `413 TOO_LARGE` | 原始材料 > 10MB | 上传 12MB PDF | | `422 NOT_IMPLEMENTED` | 未知 source_kind | `source: "unknown_source"` | | `422 DispatchNoSinksError` | 模板无 sink 可派发 | 极端边界 | | `500 INTERNAL_ERROR` | 兜底 | 任何未分类异常 | ### `sync` 状态机与配置 - `POST /api/templates/sync` 与 CLI `npm run sync` 行为完全一致(共用 `loadTemplatesFromDir`) - 同步完成后写一条 `audit_log`(step=`api.templates.sync`,actor 取 ALS 上下文) - `GET /api/templates` 仅返回 active version;旧 version 通过 `GET /api/templates/:code/versions/:version` 按需回放 ### 配置项 通过 `.env` 或环境变量注入,启动时由 envalid 校验;启动前自动创建以下运行时目录:`./data/`、`./input/`、`./output/`、`./logs/`、`./templates/`。 按 `NODE_ENV` 选择 env 文件(参考开源 `envFileMapper` 模式):`production → .env.production`、`ci → .env.ci`、`test → .env.test`、`dev → .env.development`、其余默认 `.env`。生产环境 `.env` 缺失会被视为致命错误,其他环境仅打印警告。 | 键 | 默认 | 说明 | |----|------|------| | `NODE_ENV` | local | dev / test / production / ci / local;决定 env 文件与默认值行为 | | `PORT` | 3000 | HTTP 服务端口 | | `DB_PATH` | ./data/multi-source-parse.db | SQLite 文件路径(自动创建父目录) | | `LOG_LEVEL` | info | fatal / error / warn / info / debug / trace / silent | | `REDACT_LEVEL` | standard | standard / strict;strict 额外脱敏姓名 | | `ESCALATE_POLL_INTERVAL_SEC` | 120 | 升级轮询间隔(秒) | | `INPUT_DIR` | ./input | fileWatcher 监听目录 | | `OUTPUT_DIR` | ./output | file / mock sink 落盘目录 | | `TEMPLATES_DIR` | ./templates | 模板 GitOps 源目录 | ## 典型场景 ### 场景 1:飞检反馈材料派发 1. 信息科收到某地卫健委飞检反馈 PDF,丢进 `input/feijian_20260811_001.pdf` 2. fileWatcher 2 秒内识别并调 ingest;按 `feijian.hand_hygiene.yaml` 模板抽出「检查单位 / 反馈日期 / 扣分项 / 整改要求 / 时限」 3. validate 校验通过 → 自动派发给「院感科主任」邮箱 + 院内 IM + file sink 4. 工单 24 小时未确认 → escalate 到「主管副院长」,deadline 重设 5. 责任人确认后 → reported,飞检前可一键导出证据包 ### 场景 2:卫健指标月报上报 1. 质控办导出当月抗菌药使用月报 Excel 到 `input/weijian_20260811_antibiotic.xlsx` 2. 按 `weijian.antibiotic_use.yaml` 模板抽「指标编码 / 数值 / 单位 / 上报口径 / 时点」 3. 重复指标按时点取最新;单位不一致 → 走 schema 换算或拒绝 4. validate 通过 → 派发给「质控办专干」+ file sink(保留上报底稿) 5. confirmed → reported → closed ### 场景 3:医保扣款申诉材料跟踪 1. 医保办收到医保平台扣款清单 CSV 到 `input/yibao_20260811_deduction.csv` 2. 按 `yibao.drg_dispute.yaml` 模板抽「结算单号 / 扣款金额 / 扣款原因 / 申诉理由 / 时点」 3. 编码自动识别(GBK / UTF-8);金额归一为「1200.00」格式;空原因 → ParserError 落库不丢 4. 派发给「医保办主任」+ OA + file sink 5. 申诉结果回填 → reported ### 场景 4:主管副院长 / 院长 周/月复核 + 飞检迎检「一键导出证据包」 1. 主管副院长打开周复核列表,挑选某条已闭环工单(如飞检反馈手卫生整改) 2. 系统对每条工单一键导出 zip 证据包(`GET /api/audit/:ticket_id/export`): - `ticket.json`:工单行快照 + fields / responsible 已脱敏(邮箱/手机/身份证掩码) - `audit.jsonl`:从 extract → validate → dispatch → ack 全步审计链,附带 transaction_id - `sha256.txt`:原始投递文件的 SHA-256,与原始材料对账确保未被篡改 - `README.txt`:包内文件清单 + 用途说明 3. 主管副院长把 zip 解压后人工核对;zip 自身 SHA-256 在响应头 `X-Evidence-Zip-Sha256` 可一并留痕 4. 飞检前批量导出(按月份拼成一组 zip)→ 飞检组当场核对全链路 5. **不替医务人员确认**:证据包仅供复核与留痕;最终签批仍由责任人人工完成 ### 场景 5:跨工单审计链检索(同一 transaction_id 串联多工单) 1. 复盘某次大批量投递(如省级卫健统一指标上报),同一 transaction_id 跨多张工单 2. `GET /api/audit/by-transaction/:tx` 返回所有关联工单的步骤链(按时间顺序) 3. 用于追责 / 找根因 / 排查派发失败定位 ## 输出样例 ### 工单 JSON(脱敏) ```json { "id": "tk_a1b2c3d4", "template_code": "feijian.hand_hygiene", "status": "confirmed", "fields": { "inspection_unit": "××市卫健委", "feedback_date": "2026-08-05", "deduction_items": [ { "item": "手卫生依从率", "score": -2.0, "requirement": "增设手消液点位" } ], "deadline": "2026-08-20T23:59:59+08:00" }, "responsible": { "role": "院感科主任", "user_id": "u_***", "channels": ["email", "im", "file"] }, "parser_version": "feijian@1.0.0", "created_at": "2026-08-11T13:30:00+08:00" } ``` ### 审计链 JSONL(节选) ```jsonl {"step":"extract","actor":"ingest#filewatcher","ts":"2026-08-11T13:30:00.123+08:00","transaction_id":"tx_4f8e7d","evidence":{"parser":"feijian@1.0.0","sha256":"","fields_count":4}} {"step":"validate","actor":"engine","ts":"2026-08-11T13:30:00.456+08:00","transaction_id":"tx_4f8e7d","evidence":{"errors":[],"template_version":3}} {"step":"dispatch","actor":"engine","ts":"2026-08-11T13:30:00.789+08:00","transaction_id":"tx_4f8e7d","evidence":{"sinks":["file","email_mock","oa_mock"],"message_ids":["m_001","m_002","m_003"]}} {"step":"ack","actor":"u_***","ts":"2026-08-11T13:45:12.000+08:00","transaction_id":"tx_4f8e7d","evidence":{"ack":true,"reason":"已安排整改"}} ``` ### 飞检反馈解析结果(5 字段 + 扣分块 · 脱敏) **输入**:`tests/parsers/fixtures/feijian/normal.json` ```json { "fields": { "inspection_unit": "××市卫健委飞行检查组", "feedback_date": "2026-08-05", "deduction_items": [ { "item": "手卫生依从率", "score": -2.0, "requirement": "增设手消液点位并张贴七步洗手法图示" }, { "item": "抗菌药物使用强度", "score": -1.5, "requirement": "梳理围术期预防用药指征并报药剂科" }, { "item": "危急值登记完整率", "score": -0.5, "requirement": "补全 7 月份漏登条目并双向复核" } ], "rectification_req": "请于 8 月 20 日前完成全部整改并书面反馈院感科与医务科", "deadline": "2026-08-20" }, "window": { "from": "2026-08-05", "to": "2026-08-20" }, "confidence": 1.0, "parser_version": "feijian@1.0.0" } ``` ### 解析失败样例(缺关键字段 · 落库不丢) **输入**:hint.required_fields = `["inspection_unit"]` 但 buffer 没给该字段 → 返回: ```json { "ok": false, "code": "MISSING_FIELD", "message": "飞检材料缺少关键字段:inspection_unit", "raw_evidence": { "source_kind": "feijian", "sha256_prefix": "53bb9b0b", "missing_fields": ["inspection_unit"], "hint_required": ["inspection_unit"] } } ``` 审计日志中此 ticket 状态停在 `draft`,等责任人在预览页的「审计视图」补录或重投。 ### 日期归一样例(错位 → ISO) | 输入 | 归一输出 | |------|----------| | `2026-08-05` | `2026-08-05` | | `2026/8/5` | `2026-08-05` | | `2026.08.25` | `2026-08-25` | | `2026年8月5日` | `2026-08-05` | | `11-Aug-2026` | `2026-08-11` | | `20260811` | `2026-08-11` | 非法日期(`not a date` / 13 月 / 32 日)→ `null`,写入 audit 后不阻塞其他字段抽取。 ### 卫健指标解析结果(去重 + 单位校验 + 缺值落库 · 脱敏) **输入**:`tests/parsers/fixtures/weijian/duplicate_rows.json` ```json { "rows": [ { "code": "BED-UTIL-RATE", "name": "床位使用率", "value": 92.3, "unit": "%", "caliber": "national", "reporting_period": "2026-07" }, { "code": "DDD-ANTIBIOTIC-USE", "name": "抗菌药物使用强度", "value": 38.5, "unit": "人次", "caliber": "national", "reporting_period": "2026-07" } ], "reporting_window": { "from": "2026-07", "to": "2026-07" }, "default_caliber": "national", "confidence": 0.95, "parser_version": "weijian@1.0.0" } ``` `DDD-ANTIBIOTIC-USE` 在原文件出现两次(6 月 35.2 / 7 月 38.5),自动取最新时点 2026-07 的 38.5。 ### 卫健指标单位不一致样例(拒绝整张) **输入**:`tests/parsers/fixtures/weijian/unit_mismatch.json`(第二行 unit=`千克` 不在白名单) ```json { "ok": false, "code": "UNIT_MISMATCH", "message": "卫健指标包含 1 行单位不一致:[row 1] 千克", "raw_evidence": { "source_kind": "weijian", "sha256_prefix": "8b2f9a01", "mismatch_count": 1, "mismatches": [ { "row_index": 1, "raw_unit": "千克", "reason": "未识别的单位:千克" } ] } } ``` 审计日志中此 ticket 停在 `draft`,等信息科或质控办修正单位后重投;不会让单位混用污染工单字段。 ### HIS 导出解析结果(列名差异 + 列序错位 + 冲账 · 脱敏) **输入**:`tests/parsers/fixtures/his_export/english_headers.csv`(英文表头) ```text dept_name,visit_id,fee,category,occur_time "Digestive Internal Medicine","OUT20260810001","320.00","Outpatient","2026-08-10" ``` **输出(envelope · fields)**: ```json { "rows": [ { "dept": "Digestive Internal Medicine", "visit_no": "OUT20260810001", "amount": "320.00", "order_type": "Outpatient", "occurred_at": "2026-08-10", "kind": "charge" } ], "visit_count": 1, "dept_count": 1, "encoding": "utf-8", "duplicates_collapsed": 0, "total_amount": "320.00" } ``` #### 负值金额 / 冲账 → `kind: 'refund'` 同一就诊号 `OUT20260810001` 出现两次,先 08-10 收 320.00、08-11 退 -320.00;按 occurred_at 取最新,最终保留为 `kind: 'refund'`、金额 `-320.00`。 ### HIS 解析失败样例(空科室 → EMPTY_DEPT · 拒绝整份) **输入**:`tests/parsers/fixtures/his_export/empty_dept.json` ```json { "ok": false, "code": "EMPTY_DEPT", "message": "HIS 导出有 2/3 行缺科室,无法分派责任人:[row 1] IN20260810002, [row 2] CHK20260811003", "raw_evidence": { "source_kind": "his_export", "sha256_prefix": "5e7ac9b2", "empty_dept_count": 2, "total_rows": 3, "empty_dept_rows": [ { "row_index": 1, "visit_no": "IN20260810002" }, { "row_index": 2, "visit_no": "CHK20260811003" } ] } } ``` 无科室的行无法确定分派责任人,整份拒绝(与医保缺扣款原因同强度);同时 raw_evidence 含源行号 + 就诊号,信息科工程师可据此在原表里补齐科室列后重投。 ### 预览页三视图(脱敏样例片段) 打开 `http://127.0.0.1:3000/preview/`,按 `?tab=parse|dispatch|audit` 切换: ``` ┌──────────────────────────────────────────────────────────────────────┐ │ 监管工单 #12 [ dispatched ]│ │ 模板 feijian.hand_hygiene v3 · 创建于 2026-08-11 14:32 · 截止 09-25 │ ├──────────────────────────────────────────────────────────────────────┤ │ [ 解析视图 ] [ 派发视图 ] [ 审计视图 ] ← 服务端渲染 · hash 锚点切换 │ ├──────────────────────────────────────────────────────────────────────┤ │ 解析视图(tab=parse) │ │ 解析器版本:feijian@1.0.0 │ │ 模板字段(fields) │ │ inspection_unit 省卫生监督所 │ │ feedback_date 2026-08-11 │ │ rectification_req 两周内整改并提交复核报告 │ │ deadline 2026-08-25 │ │ 原始材料元数据(source) │ │ sha256 78d98605f704c524…(前 8 字符,下同) │ │ parser_version feijian@1 │ │ source_kind feijian │ │ 责任人(responsible) │ │ role 科室主任 │ ├──────────────────────────────────────────────────────────────────────┤ │ 派发视图(tab=dispatch) │ │ sink 派发列表(2) │ │ ┌─ file-default [ ok ] ─────────────────────┐ │ │ │ attempts=1 last_attempt_at=… │ │ │ │ response_json.path = ./output/T-…file.json │ │ │ └─────────────────────────────────────────────┘ │ │ ┌─ oa-mock [ ok ] ─────────────────────┐ │ │ │ attempts=1 last_attempt_at=… │ │ │ └─────────────────────────────────────────────┘ │ ├──────────────────────────────────────────────────────────────────────┤ │ 审计视图(tab=audit) │ │ 共 7 步 │ │ ● extract.mapped tx: 8fa694c3… actor: test:preview │ │ └─ evidence(已脱敏):{field_count:4, confidence:0.8,…} │ │ ● validate.passed tx: 8fa694c3… actor: test:preview │ │ ● transition.validated→dispatched │ │ ● dispatch.attempt tx: 8fa694c3… actor: test:preview │ │ ● dispatch.sent tx: 8fa694c3… actor: test:preview │ │ ● transition.dispatched→confirmed │ └──────────────────────────────────────────────────────────────────────┘ 本预览页不引第三方前端;服务端渲染;脱敏等级 standard [ JSON 视图 ] · [ 审计 JSON ] · [ 证据包(Task 31 实现) ] ``` 预览页要点: - **零前端依赖**:纯服务端渲染 HTML + 一段内联 CSS(`/static/preview.css`),无 React / Vue / 打包步骤;浏览器直开即可 - **三 tab 用 URL hash 切换**:`#parse` / `#dispatch` / `#audit`;可加 `?tab=…` 让链接直接落到指定视图 - **脱敏与 logger 同规则**:`fields / responsible / audit evidence` 经 `redactValue(REDACT_LEVEL)` 兜底;邮箱 `zhang.san@hospital.gov.cn` 渲染为 `***@hospital.gov.cn`,手机 `13800138000` 渲染为 `****8000`,就诊号 `ZY20260811` 渲染为 `ZY********` - **404 明确**:ticket 不存在时返回 404 + not-found 页(不是空数组),便于信息科排查 - **可访问性入口**:`/preview` 列表展示最近 20 条工单;`/healthz/preview` 提供子系统健康检查(解析器 / sink 注册表 + DB 表计数) 完整证据包(含 ticket.json / raw 文件 / audit.jsonl / sink 回执 zip)由 Task 31 提供。 ## 架构与数据流 ``` ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │ input/ │ │ ingest │ │ ParserAdapter │ │ REST API │───▶│ raw │───▶│ (5 类注册) │ │ CLI │ │ envelope │ │ feijian/weijian │ └─────────────┘ └─────────────┘ │ yibao/his/emr │ └────────┬─────────┘ │ ParsedEnvelope ▼ ┌──────────────────────────────┐ │ WorkflowEngine (5 步) │ │ extract → validate → │ │ escalate → dispatch → ack │ └────────┬─────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌──────────┐ │ file │ │ http │ │ mock │ │ sink │ │ sink │ │ sinks │ │(落盘) │ │(webhook) │ │(本期验收)│ └─────────┘ └──────────┘ └──────────┘ │ ▼ ┌──────────────────────────────┐ │ Audit + Evidence Export │ │ (transaction_id 全程串联) │ └──────────────────────────────┘ ``` (Phase 4 后续 task 补齐:状态机迁移图、override 优先级矩阵、Conditions DSL 求值流) ## 安全与合规边界 - **本系统不输出诊断或治疗建议**,所有解析结果仅作为工单字段,责任人必须人工确认后才推进状态 - **本系统不替医务人员确认**:飞检反馈、卫健指标、医保扣款等监管工单的「确认 / 驳回 / 整改认定」必须由具名责任人在工单详情页显式操作;解析归一只生成结构化工单,不替医务人员给出任何业务结论 - **不自动签名 / 代签**,所有 PATCH 操作记录 actor + ts + evidence,可被审计追溯 - **解析归一只生成结构化工单**,不自动回写到 HIS / EMR / 医保平台;回写动作由人工触发的后续 round 单独接入 - **mock sink 仅用于本期验收**:email_mock 与 oa_mock 写本地 `./output/email_outbox.jsonl` / `./output/oa_outbox.jsonl` 作为 fixture,**不**真正连接 SMTP 或院内 OA;真实邮件 / OA 对接需后续 round 由医院 IT 配置 webhook URL 与认证凭据,本期不可作为生产对接面 ## 项目结构 ``` multi-source-parse/ ├── package.json # Node 依赖与脚本(sync/ingest/start) ├── tsconfig.json # ES2022 / NodeNext ├── .env.example # 配置样例 ├── .gitignore # 排除 data/ logs/ dist/ node_modules/ 等 ├── README.md # 本文件(产品说明) ├── README_EN.md # 英文版(Phase 5 补齐) ├── qrcode.jpg # 公众号二维码(关注区块使用) ├── src/ # Phase 4 后续 task 补齐 │ ├── config.ts # envalid 配置加载 │ ├── server.ts # Fastify 启动入口 │ ├── cli/ # sync / ingest / watcher 命令 │ ├── parsers/ # 5 类 ParserAdapter │ ├── workflow/ # 5 步工作流引擎 │ ├── routing/ # Conditions DSL + 三级 override │ ├── sinks/ # file / http / mock 派发器 │ ├── subscribers/ # 责任人与通道 │ ├── tickets/ # 状态机 │ ├── audit/ # transaction_id + audit_log │ ├── infra/ # db / logger / instrument │ ├── api/ # Fastify 路由 + fileWatcher │ └── web/ # 极简预览页 ├── tests/ # vitest 测试 ├── templates/ # GitOps 模板源(3 份 fixture) │ ├── feijian.hand_hygiene.yaml │ ├── weijian.antibiotic_use.yaml │ └── yibao.drg_dispute.yaml └── scripts/ └── demo.sh # 端到端演示脚本 ``` (Phase 4 后续 task 补齐:实际目录树随模块落地) ## License MIT --- ## 关注我们 欢迎扫码关注公众号,获取项目更新与交流加群: ![关注我们](qrcode.jpg)