|
|
@@ -0,0 +1,995 @@
|
|
|
+---
|
|
|
+doc_id: DEV-202607-002
|
|
|
+feature_id: FEAT-202606-001-unified-entry-client
|
|
|
+type: dev-progress
|
|
|
+title: 患者服务台血常规闭环原型实施计划
|
|
|
+status: draft
|
|
|
+owner: 医梦研发团队
|
|
|
+created_at: 2026-07-05
|
|
|
+updated_at: 2026-07-05
|
|
|
+reviewers:
|
|
|
+ - 产品负责人
|
|
|
+ - 终端负责人
|
|
|
+related_docs:
|
|
|
+ - DES-202607-001
|
|
|
+ - ODS-202606-002
|
|
|
+ - ADR-202606-001
|
|
|
+ - TST-202607-001
|
|
|
+related_modules:
|
|
|
+ - emoon-terminal-client
|
|
|
+tags:
|
|
|
+ - 患者服务台
|
|
|
+ - 血常规
|
|
|
+ - 报告闭环
|
|
|
+ - 移动端原型
|
|
|
+ - Open Design
|
|
|
+---
|
|
|
+
|
|
|
+# 患者服务台血常规闭环原型实施计划
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
|
|
+> superpowers:subagent-driven-development (recommended) or
|
|
|
+> superpowers:executing-plans to implement this plan task-by-task.
|
|
|
+> Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 在现有 `patient-service-desk-v3` 中加入患者端血常规深闭环,同时保留
|
|
|
+独立的挂号、签到和候诊演示链。
|
|
|
+
|
|
|
+**Architecture:** 继续使用纯前端事件投影原型。状态模型保存报告来源快照、
|
|
|
+患者事项、授权、解释结果、反馈和失效状态;视图根据投影渲染报告中心、详情、
|
|
|
+授权、处理中、结果和降级页面。报告解读不创建挂号命令,检查报告只增加统一入口
|
|
|
+和扩展框架。
|
|
|
+
|
|
|
+**Tech Stack:** HTML5、CSS3、原生JavaScript ES Modules、Node.js内置
|
|
|
+`node:test`、esbuild经典浏览器包、Lucide图标、Open Design HTML Artifact。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 范围与文件
|
|
|
+
|
|
|
+### 1.1 修改文件
|
|
|
+
|
|
|
+| 文件 | 职责 |
|
|
|
+|---|---|
|
|
|
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs` | 报告来源、状态迁移、反馈和场景fixture |
|
|
|
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js` | 报告中心、详情、结果、降级和控制器渲染 |
|
|
|
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css` | 报告组件、状态提示和双视口布局 |
|
|
|
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs` | 状态、渲染、交互和安全边界测试 |
|
|
|
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js` | 从JS入口重新生成的经典浏览器包 |
|
|
|
+| `docs/prototypes/patient-service-desk-v3/使用说明.md` | 场景、操作和验证方式 |
|
|
|
+| `docs/initiatives/FEAT-202606-001-unified-entry-client/测试计划.md` | 报告闭环验收项 |
|
|
|
+| `docs/initiatives/FEAT-202606-001-unified-entry-client/测试结果.md` | 最终执行记录 |
|
|
|
+
|
|
|
+### 1.2 不修改
|
|
|
+
|
|
|
+- 不修改FEAT-006生产接口和开发代码;
|
|
|
+- 不连接真实LIS、FastGPT或医院接口;
|
|
|
+- 不设计医护OCR审核工作台;
|
|
|
+- 不增加AI推荐科室、用药、治疗或具体复查周期;
|
|
|
+- 不把报告结果串入挂号TaskState;
|
|
|
+- 不修改历史v1/v2原型。
|
|
|
+
|
|
|
+## 2. 目标状态模型
|
|
|
+
|
|
|
+在初始状态中增加报告源投影和反馈集合:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ reportSourceProjections: {},
|
|
|
+ reportFeedback: [],
|
|
|
+ reportFilters: {
|
|
|
+ family: "ALL",
|
|
|
+ status: "ALL"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+血常规报告源fixture使用以下稳定结构:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ reportId: "report-blood-20260702",
|
|
|
+ family: "LAB",
|
|
|
+ reportType: "BLOOD_ROUTINE",
|
|
|
+ title: "血常规",
|
|
|
+ hospitalName: "空海医院",
|
|
|
+ departmentName: "检验科",
|
|
|
+ issuedAt: "2026-07-03 09:30",
|
|
|
+ sourceStatus: "FINAL",
|
|
|
+ interpretationStatus: "NOT_REQUESTED",
|
|
|
+ sourceVersion: 1,
|
|
|
+ sourceHash: "sha256:blood-v1",
|
|
|
+ metrics: [
|
|
|
+ {
|
|
|
+ metricId: "metric-wbc",
|
|
|
+ code: "WBC",
|
|
|
+ name: "白细胞计数",
|
|
|
+ value: "11.2",
|
|
|
+ unit: "10^9/L",
|
|
|
+ referenceRange: "3.5–9.5",
|
|
|
+ sourceFlag: "HIGH"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+原型允许的患者端解释状态:
|
|
|
+
|
|
|
+```text
|
|
|
+NOT_REQUESTED
|
|
|
+CONSENT_REQUIRED
|
|
|
+GENERATING
|
|
|
+READY
|
|
|
+NEED_REVIEW
|
|
|
+CRITICAL_NOTICE
|
|
|
+AI_UNAVAILABLE
|
|
|
+INVALIDATED
|
|
|
+```
|
|
|
+
|
|
|
+## 3. 实施任务
|
|
|
+
|
|
|
+### Task 1:锁定场景分组与报告源契约
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1:写失败测试**
|
|
|
+
|
|
|
+新增测试,要求控制器存在两个场景组,并锁定报告组八个场景:
|
|
|
+
|
|
|
+```js
|
|
|
+assert.deepEqual(demoScenarioGroups.REPORT_CLOSURE, [
|
|
|
+ "REPORT_ARRIVED",
|
|
|
+ "REPORT_DETAIL",
|
|
|
+ "REPORT_CONSENT",
|
|
|
+ "REPORT_GENERATING",
|
|
|
+ "REPORT_READY",
|
|
|
+ "REPORT_NEED_REVIEW",
|
|
|
+ "REPORT_CRITICAL",
|
|
|
+ "REPORT_INVALIDATED",
|
|
|
+]);
|
|
|
+assert.ok(demoScenarioGroups.CARE_CLOSURE.includes("REGISTRATION_QUERY"));
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="scenario groups" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:FAIL,提示`demoScenarioGroups`尚未导出。
|
|
|
+
|
|
|
+- [ ] **Step 3:实现最小场景契约**
|
|
|
+
|
|
|
+在`patient-service-desk-v3.js`导出冻结分组,并让`demoScenarios`由分组扁平化生成:
|
|
|
+
|
|
|
+```js
|
|
|
+export const demoScenarioGroups = Object.freeze({
|
|
|
+ REPORT_CLOSURE: Object.freeze([
|
|
|
+ "REPORT_ARRIVED",
|
|
|
+ "REPORT_DETAIL",
|
|
|
+ "REPORT_CONSENT",
|
|
|
+ "REPORT_GENERATING",
|
|
|
+ "REPORT_READY",
|
|
|
+ "REPORT_NEED_REVIEW",
|
|
|
+ "REPORT_CRITICAL",
|
|
|
+ "REPORT_INVALIDATED",
|
|
|
+ ]),
|
|
|
+ CARE_CLOSURE: Object.freeze([
|
|
|
+ "EMPTY_HOME",
|
|
|
+ "REGISTRATION_QUERY",
|
|
|
+ "COMMAND_PREPARED",
|
|
|
+ "APPOINTMENT_CREATED",
|
|
|
+ "CHECKIN_AVAILABLE",
|
|
|
+ "QUEUE_UPDATED",
|
|
|
+ ]),
|
|
|
+});
|
|
|
+
|
|
|
+export const demoScenarios = Object.freeze(
|
|
|
+ Object.values(demoScenarioGroups).flat(),
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4:运行测试确认通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="scenario groups" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **Step 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/prototypes/patient-service-desk-v3
|
|
|
+git commit -m "test: define patient report closure scenarios"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 2:建立报告源投影与筛选状态
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
|
|
|
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1:写失败测试**
|
|
|
+
|
|
|
+覆盖报告出具、详情读取和筛选:
|
|
|
+
|
|
|
+```js
|
|
|
+const bloodRoutineReportFixture = {
|
|
|
+ reportId: "report-blood-20260702",
|
|
|
+ family: "LAB",
|
|
|
+ reportType: "BLOOD_ROUTINE",
|
|
|
+ title: "血常规",
|
|
|
+ hospitalName: "空海医院",
|
|
|
+ departmentName: "检验科",
|
|
|
+ issuedAt: "2026-07-03 09:30",
|
|
|
+ sourceStatus: "FINAL",
|
|
|
+ interpretationStatus: "NOT_REQUESTED",
|
|
|
+ verificationStatus: "VERIFIED",
|
|
|
+ sourceVersion: 1,
|
|
|
+ sourceHash: "sha256:blood-v1",
|
|
|
+ readStatus: "UNREAD",
|
|
|
+ metrics: [],
|
|
|
+};
|
|
|
+const arrived = applyServerProjectionFixture(createInitialState(), {
|
|
|
+ id: "report-issued-blood-v1",
|
|
|
+ type: "REPORT_ISSUED",
|
|
|
+ occurredAt: "2026-07-03T09:30:00+08:00",
|
|
|
+ payload: bloodRoutineReportFixture,
|
|
|
+});
|
|
|
+
|
|
|
+assert.equal(
|
|
|
+ arrived.reportSourceProjections["report-blood-20260702"].sourceStatus,
|
|
|
+ "FINAL",
|
|
|
+);
|
|
|
+assert.equal(arrived.patientWorkItems.length, 1);
|
|
|
+assert.equal(arrived.activeAgentTaskProjection, null);
|
|
|
+assert.deepEqual(
|
|
|
+ selectReports(arrived, { family: "LAB", status: "UNREAD" })
|
|
|
+ .map((report) => report.reportId),
|
|
|
+ ["report-blood-20260702"],
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="report source|report filters" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:FAIL,报告源集合或`selectReports`不存在。
|
|
|
+
|
|
|
+- [ ] **Step 3:实现报告源投影**
|
|
|
+
|
|
|
+在`createInitialState`增加:
|
|
|
+
|
|
|
+```js
|
|
|
+reportSourceProjections: {},
|
|
|
+reportFeedback: [],
|
|
|
+reportFilters: { family: "ALL", status: "ALL" },
|
|
|
+```
|
|
|
+
|
|
|
+`REPORT_ISSUED`只新增或版本化更新报告源、患者事项和消息,不创建活动Agent任务。
|
|
|
+导出纯函数:
|
|
|
+
|
|
|
+```js
|
|
|
+export function selectReports(
|
|
|
+ state,
|
|
|
+ { family = "ALL", status = "ALL" } = {},
|
|
|
+) {
|
|
|
+ return Object.values(state?.reportSourceProjections ?? {})
|
|
|
+ .filter((report) => family === "ALL" || report.family === family)
|
|
|
+ .filter((report) => status === "ALL" || report.readStatus === status)
|
|
|
+ .sort((a, b) => b.issuedAt.localeCompare(a.issuedAt));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4:运行报告状态测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="report source|report filters" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **Step 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/prototypes/patient-service-desk-v3
|
|
|
+git commit -m "feat: add patient report source projections"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 3:实现报告中心与血常规详情
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
|
|
|
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1:写失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+const reportState = loadScenario(createInitialState(), "REPORT_ARRIVED");
|
|
|
+const centerHtml = renderReportCenter(reportState);
|
|
|
+assert.match(centerHtml, /全部/);
|
|
|
+assert.match(centerHtml, /检验报告/);
|
|
|
+assert.match(centerHtml, /检查报告/);
|
|
|
+assert.match(centerHtml, /血常规/);
|
|
|
+
|
|
|
+const detailHtml = renderReportDetail(
|
|
|
+ reportState.reportSourceProjections["report-blood-20260702"],
|
|
|
+);
|
|
|
+assert.match(detailHtml, /医院原始报告/);
|
|
|
+assert.match(detailHtml, /白细胞计数/);
|
|
|
+assert.match(detailHtml, /11\\.2/);
|
|
|
+assert.match(detailHtml, /10\\^9\\/L/);
|
|
|
+assert.match(detailHtml, /3\\.5–9\\.5/);
|
|
|
+assert.doesNotMatch(detailHtml, /建议挂.*科|建议服用|确诊/);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="report center|blood routine detail" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:FAIL,渲染接口或完整内容不存在。
|
|
|
+
|
|
|
+- [ ] **Step 3:实现页面与交互**
|
|
|
+
|
|
|
+新增并导出:
|
|
|
+
|
|
|
+```js
|
|
|
+export const renderReportCenter = (state) => {
|
|
|
+ const reports = selectReports(state, state?.reportFilters);
|
|
|
+ return `
|
|
|
+ <section class="report-center" aria-labelledby="reportCenterTitle">
|
|
|
+ <h1 id="reportCenterTitle">检验检查报告</h1>
|
|
|
+ <div class="report-filters" aria-label="报告类型筛选">
|
|
|
+ <button data-action="set-report-family-filter" data-filter="ALL">全部</button>
|
|
|
+ <button data-action="set-report-family-filter" data-filter="LAB">检验报告</button>
|
|
|
+ <button data-action="set-report-family-filter" data-filter="EXAM">检查报告</button>
|
|
|
+ </div>
|
|
|
+ <div class="report-list">
|
|
|
+ ${reports.map((report) => `
|
|
|
+ <button
|
|
|
+ class="report-list-item"
|
|
|
+ data-action="view-report"
|
|
|
+ data-report-id="${escapeHtml(report.reportId)}"
|
|
|
+ >
|
|
|
+ <strong>${escapeHtml(report.title)}</strong>
|
|
|
+ <span>${escapeHtml(report.hospitalName)} · ${escapeHtml(report.issuedAt)}</span>
|
|
|
+ </button>
|
|
|
+ `).join("")}
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ `;
|
|
|
+};
|
|
|
+
|
|
|
+export const renderReportDetail = (report, expanded = false) => {
|
|
|
+ const metrics = expanded ? report.metrics : report.metrics.slice(0, 3);
|
|
|
+ return `
|
|
|
+ <section class="report-detail" aria-labelledby="reportDetailTitle">
|
|
|
+ <h1 id="reportDetailTitle">${escapeHtml(report.title)}</h1>
|
|
|
+ <p>${escapeHtml(report.hospitalName)} · ${escapeHtml(report.departmentName)}</p>
|
|
|
+ <h2>医院原始报告</h2>
|
|
|
+ <dl>
|
|
|
+ ${metrics.map((metric) => `
|
|
|
+ <div>
|
|
|
+ <dt>${escapeHtml(metric.name)}</dt>
|
|
|
+ <dd>
|
|
|
+ ${escapeHtml(metric.value)} ${escapeHtml(metric.unit)}
|
|
|
+ · 参考 ${escapeHtml(metric.referenceRange)}
|
|
|
+ </dd>
|
|
|
+ </div>
|
|
|
+ `).join("")}
|
|
|
+ </dl>
|
|
|
+ <button data-action="interpret-report"
|
|
|
+ data-report-id="${escapeHtml(report.reportId)}">AI辅助解读</button>
|
|
|
+ </section>
|
|
|
+ `;
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+增加操作:
|
|
|
+
|
|
|
+```text
|
|
|
+open-report-center
|
|
|
+set-report-family-filter
|
|
|
+set-report-status-filter
|
|
|
+view-report
|
|
|
+toggle-report-metrics
|
|
|
+```
|
|
|
+
|
|
|
+报告详情中的主按钮为“AI辅助解读”,不能出现“推荐复诊”或“预约相关科室”。
|
|
|
+
|
|
|
+- [ ] **Step 4:运行对应测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="report center|blood routine detail" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **Step 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/prototypes/patient-service-desk-v3
|
|
|
+git commit -m "feat: add blood routine report center and detail"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 4:收紧授权、可信门禁与处理中状态
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1:写失败测试**
|
|
|
+
|
|
|
+测试授权绑定和可信门禁:
|
|
|
+
|
|
|
+```js
|
|
|
+const reportId = "report-blood-20260702";
|
|
|
+const reportState = loadScenario(createInitialState(), "REPORT_ARRIVED");
|
|
|
+const requested = requestReportInterpretation(reportState, reportId);
|
|
|
+assert.equal(requested.overlays.consentId, `consent-ai-${reportId}`);
|
|
|
+
|
|
|
+const granted = grantConsent(requested, `consent-ai-${reportId}`);
|
|
|
+assert.equal(granted.reportSourceProjections[reportId].interpretationStatus,
|
|
|
+ "GENERATING");
|
|
|
+
|
|
|
+for (const reason of [
|
|
|
+ "SOURCE_NOT_FINAL",
|
|
|
+ "OCR_REVIEW_REQUIRED",
|
|
|
+ "REFERENCE_CONFLICT",
|
|
|
+]) {
|
|
|
+ const blockedState = {
|
|
|
+ ...reportState,
|
|
|
+ reportSourceProjections: {
|
|
|
+ ...reportState.reportSourceProjections,
|
|
|
+ [reportId]: {
|
|
|
+ ...reportState.reportSourceProjections[reportId],
|
|
|
+ verificationStatus: "NEED_REVIEW",
|
|
|
+ verificationReason: reason,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ };
|
|
|
+ const blocked = evaluateReportInterpretationGate(
|
|
|
+ blockedState,
|
|
|
+ reportId,
|
|
|
+ );
|
|
|
+ assert.equal(blocked.allowed, false);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="report consent boundary|interpretation gate" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:FAIL,门禁函数或绑定结构不存在。
|
|
|
+
|
|
|
+- [ ] **Step 3:实现最小门禁**
|
|
|
+
|
|
|
+导出:
|
|
|
+
|
|
|
+```js
|
|
|
+export function evaluateReportInterpretationGate(state, reportId) {
|
|
|
+ const report = state?.reportSourceProjections?.[reportId];
|
|
|
+ if (!report) return { allowed: false, reason: "REPORT_NOT_FOUND" };
|
|
|
+ if (report.sourceStatus !== "FINAL") {
|
|
|
+ return { allowed: false, reason: "SOURCE_NOT_FINAL" };
|
|
|
+ }
|
|
|
+ if (report.verificationStatus !== "VERIFIED") {
|
|
|
+ return { allowed: false, reason: report.verificationReason };
|
|
|
+ }
|
|
|
+ if (report.criticalNotice === true) {
|
|
|
+ return { allowed: false, reason: "CRITICAL_NOTICE" };
|
|
|
+ }
|
|
|
+ return { allowed: true, reason: null };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+授权只绑定当前患者和当前报告。门禁失败时不创建结果,不启动解释计时器。
|
|
|
+同时将现有`grantConsent`改为接受动态`consent-ai-${reportId}`,并从当前overlay校验
|
|
|
+患者和报告绑定;不新增第二套授权函数。
|
|
|
+
|
|
|
+- [ ] **Step 4:运行授权与门禁测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="report consent boundary|interpretation gate" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **Step 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/prototypes/patient-service-desk-v3
|
|
|
+git commit -m "feat: enforce patient report interpretation gate"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 5:实现安全解读结果与反馈
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
|
|
|
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1:写失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+const reportId = "report-blood-20260702";
|
|
|
+const generating = loadScenario(createInitialState(), "REPORT_GENERATING");
|
|
|
+const ready = applyReportInterpretationProjectionFixture(generating, reportId);
|
|
|
+const html = renderReportInterpretationResult(
|
|
|
+ ready.resultSnapshotProjections["result-report-interpretation-001"],
|
|
|
+);
|
|
|
+
|
|
|
+for (const text of [
|
|
|
+ "结果概括",
|
|
|
+ "需要关注",
|
|
|
+ "指标解释",
|
|
|
+ "常见相关因素",
|
|
|
+ "可以怎么做",
|
|
|
+ "内容依据",
|
|
|
+]) {
|
|
|
+ assert.match(html, new RegExp(text));
|
|
|
+}
|
|
|
+assert.doesNotMatch(html, /推荐科室|复查周期|治疗方案|建议服用/);
|
|
|
+
|
|
|
+const feedback = submitReportFeedback(ready, {
|
|
|
+ reportId,
|
|
|
+ category: "HARD_TO_UNDERSTAND",
|
|
|
+});
|
|
|
+assert.equal(feedback.reportFeedback.at(-1).category, "HARD_TO_UNDERSTAND");
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="safe report result|report feedback" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:FAIL,六层结果或反馈接口不存在。
|
|
|
+
|
|
|
+- [ ] **Step 3:实现结果与反馈**
|
|
|
+
|
|
|
+结果只提供以下操作:
|
|
|
+
|
|
|
+```text
|
|
|
+查看原始报告
|
|
|
+咨询开单医生
|
|
|
+在线咨询
|
|
|
+联系人工服务
|
|
|
+提交反馈
|
|
|
+撤回授权
|
|
|
+```
|
|
|
+
|
|
|
+导出纯函数:
|
|
|
+
|
|
|
+```js
|
|
|
+export function submitReportFeedback(state, { reportId, category }) {
|
|
|
+ const allowed = new Set([
|
|
|
+ "HELPFUL",
|
|
|
+ "METRIC_INACCURATE",
|
|
|
+ "HARD_TO_UNDERSTAND",
|
|
|
+ "DIFFERS_FROM_DOCTOR",
|
|
|
+ "OTHER",
|
|
|
+ ]);
|
|
|
+ if (!allowed.has(category)) return state;
|
|
|
+ return {
|
|
|
+ ...state,
|
|
|
+ reportFeedback: [
|
|
|
+ ...(state.reportFeedback ?? []),
|
|
|
+ { id: `feedback-${reportId}-${category}`, reportId, category },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4:运行结果与反馈测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="safe report result|report feedback" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **Step 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/prototypes/patient-service-desk-v3
|
|
|
+git commit -m "feat: add safe blood report result and feedback"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 6:实现患者端降级、危急值与失效
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
|
|
|
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1:写失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+const reportId = "report-blood-20260702";
|
|
|
+const readyState = loadScenario(createInitialState(), "REPORT_READY");
|
|
|
+const needsReview = loadScenario(createInitialState(), "REPORT_NEED_REVIEW");
|
|
|
+assert.match(renderReportUnavailable({
|
|
|
+ reason: "OCR_REVIEW_REQUIRED",
|
|
|
+}), /暂不适合自动解读/);
|
|
|
+assert.doesNotMatch(renderReportUnavailable({
|
|
|
+ reason: "OCR_REVIEW_REQUIRED",
|
|
|
+}), /继续生成/);
|
|
|
+
|
|
|
+const critical = loadScenario(createInitialState(), "REPORT_CRITICAL");
|
|
|
+assert.match(renderReportUnavailable({
|
|
|
+ reason: "CRITICAL_NOTICE",
|
|
|
+}), /请按医院提示及时联系/);
|
|
|
+assert.doesNotMatch(renderReportUnavailable({
|
|
|
+ reason: "CRITICAL_NOTICE",
|
|
|
+}), /AI解读|推荐科室/);
|
|
|
+
|
|
|
+const invalidated = applyServerProjectionFixture(readyState, {
|
|
|
+ id: "report-corrected-blood-v2",
|
|
|
+ type: "REPORT_CORRECTED",
|
|
|
+ occurredAt: "2026-07-03T11:00:00+08:00",
|
|
|
+ payload: {
|
|
|
+ reportId,
|
|
|
+ sourceVersion: 2,
|
|
|
+ sourceHash: "sha256:blood-v2",
|
|
|
+ },
|
|
|
+});
|
|
|
+assert.equal(
|
|
|
+ invalidated.reportSourceProjections[reportId].interpretationStatus,
|
|
|
+ "INVALIDATED",
|
|
|
+);
|
|
|
+assert.equal(
|
|
|
+ invalidated.resultSnapshotProjections[
|
|
|
+ "result-report-interpretation-001"
|
|
|
+ ].valid,
|
|
|
+ false,
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="report degradation|critical notice|report invalidation" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:FAIL,降级页面或更正事件未实现。
|
|
|
+
|
|
|
+- [ ] **Step 3:实现状态和页面**
|
|
|
+
|
|
|
+统一降级组件接收原因码:
|
|
|
+
|
|
|
+```js
|
|
|
+const REPORT_UNAVAILABLE_PRESENTATION = Object.freeze({
|
|
|
+ OCR_REVIEW_REQUIRED: {
|
|
|
+ title: "当前报告暂不适合自动解读",
|
|
|
+ description: "报告内容需要人工核对,您仍可查看医院原始报告。",
|
|
|
+ },
|
|
|
+ CRITICAL_NOTICE: {
|
|
|
+ title: "请按医院提示及时联系",
|
|
|
+ description: "当前报告需要按医院既有流程处理。",
|
|
|
+ },
|
|
|
+ AI_UNAVAILABLE: {
|
|
|
+ title: "AI辅助解读暂不可用",
|
|
|
+ description: "医院原始报告仍可正常查看,请稍后重试。",
|
|
|
+ },
|
|
|
+ UNSUPPORTED: {
|
|
|
+ title: "暂不支持自动解读",
|
|
|
+ description: "您可以查看原始报告或联系人工服务。",
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+export const renderReportUnavailable = ({ reason }) => {
|
|
|
+ const presentation = REPORT_UNAVAILABLE_PRESENTATION[reason]
|
|
|
+ ?? REPORT_UNAVAILABLE_PRESENTATION.UNSUPPORTED;
|
|
|
+ return `
|
|
|
+ <section class="report-unavailable" role="status">
|
|
|
+ <h1>${escapeHtml(presentation.title)}</h1>
|
|
|
+ <p>${escapeHtml(presentation.description)}</p>
|
|
|
+ <button data-action="open-original-report">查看原始报告</button>
|
|
|
+ <button data-action="contact-human-service">联系人工服务</button>
|
|
|
+ </section>
|
|
|
+ `;
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+`REPORT_CORRECTED`和`REPORT_WITHDRAWN`必须使旧结果失效;旧结果可保留在状态中用于
|
|
|
+演示审计,但不能由当前页面读取为有效结果。
|
|
|
+
|
|
|
+- [ ] **Step 4:运行降级和失效测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="report degradation|critical notice|report invalidation" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **Step 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/prototypes/patient-service-desk-v3
|
|
|
+git commit -m "feat: add patient report safety degradation states"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 7:增加检查报告扩展框架和四入口联动
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1:写失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+const reportState = loadScenario(createInitialState(), "REPORT_ARRIVED");
|
|
|
+const imaging = {
|
|
|
+ reportId: "report-ct-20260702",
|
|
|
+ family: "EXAM",
|
|
|
+ reportType: "IMAGING",
|
|
|
+ title: "胸部CT",
|
|
|
+ bodyPart: "胸部",
|
|
|
+ findings: "双肺纹理清晰。",
|
|
|
+ conclusion: "本次检查未见明确异常。",
|
|
|
+ sourceStatus: "FINAL",
|
|
|
+};
|
|
|
+const html = renderExaminationReportDetail(imaging);
|
|
|
+assert.match(html, /检查所见/);
|
|
|
+assert.match(html, /报告结论/);
|
|
|
+assert.doesNotMatch(html, /参考范围|异常指标列表/);
|
|
|
+assert.match(html, /该类型暂未开放AI辅助解读/);
|
|
|
+
|
|
|
+assert.match(renderHome(reportState), /血常规报告已出/);
|
|
|
+assert.match(renderMessages(reportState), /血常规报告/);
|
|
|
+assert.match(renderRecords({
|
|
|
+ ...reportState,
|
|
|
+ activeTab: "records",
|
|
|
+}), /检验报告/);
|
|
|
+assert.match(renderAssistant({
|
|
|
+ ...reportState,
|
|
|
+ activeTab: "assistant",
|
|
|
+}), /帮我看报告/);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="examination report framework|report entry points" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:FAIL,检查报告详情或入口联动不足。
|
|
|
+
|
|
|
+- [ ] **Step 3:实现扩展框架**
|
|
|
+
|
|
|
+检查报告只渲染:
|
|
|
+
|
|
|
+```text
|
|
|
+检查名称和部位
|
|
|
+检查所见
|
|
|
+报告结论
|
|
|
+报告医生与审核状态
|
|
|
+图像或PDF入口
|
|
|
+暂未开放AI辅助解读
|
|
|
+```
|
|
|
+
|
|
|
+不得复用检验报告的指标、单位和参考区间组件。
|
|
|
+
|
|
|
+- [ ] **Step 4:运行测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test --test-name-pattern="examination report framework|report entry points" \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **Step 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/prototypes/patient-service-desk-v3
|
|
|
+git commit -m "feat: add examination report extension shell"
|
|
|
+```
|
|
|
+
|
|
|
+### Task 8:完成控制器、可访问性、bundle和发布验证
|
|
|
+
|
|
|
+**Files:**
|
|
|
+
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js`
|
|
|
+- Modify: `docs/prototypes/patient-service-desk-v3/使用说明.md`
|
|
|
+- Modify: `docs/initiatives/FEAT-202606-001-unified-entry-client/测试计划.md`
|
|
|
+- Modify: `docs/initiatives/FEAT-202606-001-unified-entry-client/测试结果.md`
|
|
|
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1:写完整故事线失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+for (const scenario of demoScenarioGroups.REPORT_CLOSURE) {
|
|
|
+ const state = loadScenario(createInitialState(), scenario);
|
|
|
+ assert.equal(state.demoScenario, scenario);
|
|
|
+ assert.equal(state.activeAgentTaskProjection, null);
|
|
|
+}
|
|
|
+
|
|
|
+const reportStory = [
|
|
|
+ "REPORT_ARRIVED",
|
|
|
+ "REPORT_DETAIL",
|
|
|
+ "REPORT_CONSENT",
|
|
|
+ "REPORT_GENERATING",
|
|
|
+ "REPORT_READY",
|
|
|
+ "REPORT_INVALIDATED",
|
|
|
+];
|
|
|
+assert.deepEqual(
|
|
|
+ reportStory.map((scenario) => (
|
|
|
+ loadScenario(createInitialState(), scenario).demoScenario
|
|
|
+ )),
|
|
|
+ reportStory,
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+同时断言:
|
|
|
+
|
|
|
+- 弹窗仍在手机画布内部;
|
|
|
+- 390×844和430×932无横向溢出;
|
|
|
+- 控件触控区不小于44px;
|
|
|
+- 对话框焦点进入、Tab循环和焦点恢复有效;
|
|
|
+- 患者视图不出现OCR、RAG、Prompt、Workflow、traceId和L0/L1等术语;
|
|
|
+- HTML继续通过经典bundle支持`file://`直接打开。
|
|
|
+
|
|
|
+- [ ] **Step 2:运行全部测试确认尚有失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:新增完整故事线测试FAIL,既有测试不得出现无关失败。
|
|
|
+
|
|
|
+- [ ] **Step 3:完成控制器与样式**
|
|
|
+
|
|
|
+遥控器分组显示“报告闭环”和“就医闭环”,保持:
|
|
|
+
|
|
|
+```text
|
|
|
+上一步
|
|
|
+下一步
|
|
|
+重置
|
|
|
+390×844
|
|
|
+430×932
|
|
|
+隐藏控制器
|
|
|
+```
|
|
|
+
|
|
|
+所有报告弹窗挂载在`.phone-shell`内部,不得覆盖外部页面。
|
|
|
+
|
|
|
+- [ ] **Step 4:重新生成经典bundle**
|
|
|
+
|
|
|
+```bash
|
|
|
+/Users/destiny/node_modules/.bin/esbuild \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js \
|
|
|
+ --bundle \
|
|
|
+ --format=iife \
|
|
|
+ --platform=browser \
|
|
|
+ --target=es2022 \
|
|
|
+ --banner:js='/* Generated from patient-service-desk-v3.js for direct file:// opening. */' \
|
|
|
+ --outfile=docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js
|
|
|
+```
|
|
|
+
|
|
|
+预期:生成成功,无未解析import。
|
|
|
+
|
|
|
+- [ ] **Step 5:运行完整验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --test \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
|
|
|
+node --check \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs
|
|
|
+node --check \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js
|
|
|
+node --check \
|
|
|
+ docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js
|
|
|
+git diff --check
|
|
|
+```
|
|
|
+
|
|
|
+预期:全部测试通过、语法检查通过、差异检查无输出。
|
|
|
+
|
|
|
+- [ ] **Step 6:同步Open Design发布副本**
|
|
|
+
|
|
|
+```bash
|
|
|
+OPEN_DESIGN="/Users/destiny/Library/Application Support/Open Design/namespaces/release-stable/data/projects/b7a08855-87bd-493c-92a4-31655a384570/web"
|
|
|
+
|
|
|
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.html \
|
|
|
+ "$OPEN_DESIGN/patient-service-desk-v3.html"
|
|
|
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css \
|
|
|
+ "$OPEN_DESIGN/patient-service-desk-v3.css"
|
|
|
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js \
|
|
|
+ "$OPEN_DESIGN/patient-service-desk-v3.js"
|
|
|
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs \
|
|
|
+ "$OPEN_DESIGN/patient-service-desk-v3.state.mjs"
|
|
|
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js \
|
|
|
+ "$OPEN_DESIGN/patient-service-desk-v3.bundle.js"
|
|
|
+```
|
|
|
+
|
|
|
+逐个使用`cmp`确认发布副本与仓库源码一致。
|
|
|
+
|
|
|
+- [ ] **Step 7:更新文档**
|
|
|
+
|
|
|
+更新测试计划、测试结果和使用说明,记录:
|
|
|
+
|
|
|
+- 报告闭环场景;
|
|
|
+- 实际测试数量;
|
|
|
+- 真实视觉复核状态;
|
|
|
+- Open Design发布哈希;
|
|
|
+- 检查报告仅为扩展框架;
|
|
|
+- 不包含真实LIS、FastGPT和医护审核台。
|
|
|
+
|
|
|
+- [ ] **Step 8:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add \
|
|
|
+ docs/prototypes/patient-service-desk-v3 \
|
|
|
+ docs/initiatives/FEAT-202606-001-unified-entry-client/测试计划.md \
|
|
|
+ docs/initiatives/FEAT-202606-001-unified-entry-client/测试结果.md
|
|
|
+git commit -m "docs: complete patient blood report prototype delivery"
|
|
|
+```
|
|
|
+
|
|
|
+## 4. 最终验收
|
|
|
+
|
|
|
+完成后必须满足:
|
|
|
+
|
|
|
+1. 报告闭环和就医闭环在控制器中分组且可独立加载;
|
|
|
+2. `REPORT_ISSUED`只创建患者事项,不自动启动AI解读;
|
|
|
+3. 报告详情先展示医院原始事实;
|
|
|
+4. 授权绑定患者、报告、用途和有效期;
|
|
|
+5. 只有可信门禁通过才进入生成态;
|
|
|
+6. 患者结果不包含诊断、用药、治疗、AI推荐科室和具体复查周期;
|
|
|
+7. OCR待确认、来源冲突和危急值不会硬生成解读;
|
|
|
+8. 报告更正、撤回或作废使旧结果失效;
|
|
|
+9. 报告反馈结构化保存在医梦原型状态;
|
|
|
+10. 检查报告不复用检验指标组件;
|
|
|
+11. 报告闭环不创建挂号Command或活动挂号TaskState;
|
|
|
+12. 原挂号、签到和候诊测试继续通过;
|
|
|
+13. 双视口、弹窗、焦点、44px触控和直接打开能力保持有效;
|
|
|
+14. Open Design发布副本与仓库源码一致。
|
|
|
+
|
|
|
+## 5. 阶段记录
|
|
|
+
|
|
|
+| 日期 | 进展 | 风险 | 下一步 |
|
|
|
+|---|---|---|---|
|
|
|
+| 2026-07-05 | 完成功能策略、边界和逐页设计评审 | 原型仍是fixture,不代表生产接口 | 按Task 1开始TDD实现 |
|
|
|
+
|
|
|
+## 6. 阻塞项
|
|
|
+
|
|
|
+- 内置浏览器安全策略可能继续拒绝本地`file://`地址;自动测试和静态契约不能替代
|
|
|
+ 产品负责人在可访问环境中的真实双视口视觉复核。
|
|
|
+- Open Design属于本地发布目录,不纳入Git;每次修改必须显式同步并使用`cmp`验证。
|