diff --git a/contracts/booking-agent-m0.openapi.yaml b/contracts/booking-agent-m0.openapi.yaml new file mode 100644 index 0000000..af3d86d --- /dev/null +++ b/contracts/booking-agent-m0.openapi.yaml @@ -0,0 +1,559 @@ +openapi: 3.0.3 +info: + title: Petstore Booking Agent M0 API + version: 0.1.0 + description: | + 智能预约 M0 冻结契约。M0 只生成并回填预约草稿,不注册 confirm endpoint, + 不直接创建 Appointment。除鉴权拦截器的 HTTP 401 外,当前 Petstore 客户端以 + 响应体 code 作为业务结果;实现不得借本功能顺带重构全局响应语义。 +security: + - bearerAuth: [] +x-contract-status: frozen-for-m0 +x-business-error-codes: + - UNAUTHENTICATED + - FORBIDDEN + - AGENT_DISABLED + - STORE_NOT_FOUND + - SESSION_NOT_FOUND + - SESSION_EXPIRED + - SESSION_TERMINAL + - DRAFT_VERSION_CONFLICT + - INVALID_INPUT + - INVALID_AUDIO + - AUDIO_TOO_LARGE + - RATE_LIMITED + - AGENT_UNAVAILABLE + - ASR_UNAVAILABLE +paths: + /api/booking-agent/sessions: + post: + operationId: createBookingAgentSession + summary: 创建 customer 智能预约会话 + description: | + 仅已登录 customer。M0 要求先在普通预约页选定门店;entrySource 由服务端固定为 + appointment_create,不从客户端接收。 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSessionRequest' + responses: + '200': + description: 成功或带业务 code 的可理解失败 + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/SessionEnvelope' + - $ref: '#/components/schemas/ErrorEnvelope' + '401': + $ref: '#/components/responses/Unauthenticated' + + /api/booking-agent/sessions/{sessionId}/messages: + post: + operationId: submitBookingAgentMessage + summary: 提交一轮已确认文字并刷新草稿 + parameters: + - $ref: '#/components/parameters/SessionId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SubmitMessageRequest' + responses: + '200': + description: 成功或带业务 code 的可理解失败 + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/SessionEnvelope' + - $ref: '#/components/schemas/ErrorEnvelope' + '401': + $ref: '#/components/responses/Unauthenticated' + + /api/booking-agent/sessions/{sessionId}/transcriptions: + post: + operationId: transcribeBookingAgentAudio + summary: 转写当前 customer 会话的一段短音频 + description: | + 只做语音转写,不自动提交消息。客户端必须把返回文字放回可编辑输入框,用户确认后 + 再调用 messages。后端从 session 解析门店和 customer 范围,不接受客户端上传词表。 + parameters: + - $ref: '#/components/parameters/SessionId' + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + additionalProperties: false + required: + - audio + properties: + audio: + type: string + format: binary + description: 单段不超过 60 秒、3 MB;MIME 与真实文件头必须一致 + responses: + '200': + description: 成功或带业务 code 的可理解失败 + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/TranscriptionEnvelope' + - $ref: '#/components/schemas/ErrorEnvelope' + '401': + $ref: '#/components/responses/Unauthenticated' + + /api/booking-agent/sessions/{sessionId}/fallback: + post: + operationId: handoffBookingAgentDraft + summary: 结束助手并返回可回填普通表单的已验证草稿 + parameters: + - $ref: '#/components/parameters/SessionId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DraftVersionRequest' + responses: + '200': + description: 成功或带业务 code 的可理解失败 + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/HandoffEnvelope' + - $ref: '#/components/schemas/ErrorEnvelope' + '401': + $ref: '#/components/responses/Unauthenticated' + + /api/booking-agent/sessions/{sessionId}: + delete: + operationId: cancelBookingAgentSession + summary: 结束本人智能预约会话 + parameters: + - $ref: '#/components/parameters/SessionId' + responses: + '200': + description: 成功或带业务 code 的可理解失败 + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/CancelEnvelope' + - $ref: '#/components/schemas/ErrorEnvelope' + '401': + $ref: '#/components/responses/Unauthenticated' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: PetstoreSessionToken + + parameters: + SessionId: + name: sessionId + in: path + required: true + description: 不可预测的 UUID;仍必须和 current.userId 联合查询 + schema: + type: string + format: uuid + + responses: + Unauthenticated: + description: session token 缺失、失效或账号权限已变化 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + example: + code: 401 + message: 登录已失效,请重新登录 + bizCode: UNAUTHENTICATED + + schemas: + CreateSessionRequest: + type: object + additionalProperties: false + required: + - storeId + properties: + storeId: + type: integer + format: int64 + minimum: 1 + + SubmitMessageRequest: + type: object + additionalProperties: false + required: + - inputType + - text + - draftVersion + properties: + inputType: + type: string + enum: + - text + - voice + text: + type: string + minLength: 1 + maxLength: 500 + description: 用户确认后的当轮输入;服务端不得写日志或长期持久化 + draftVersion: + type: integer + minimum: 0 + + DraftVersionRequest: + type: object + additionalProperties: false + required: + - draftVersion + properties: + draftVersion: + type: integer + minimum: 0 + + BookingAgentStatus: + type: string + enum: + - collecting + - proposing + - confirmable + - fallback + - expired + - cancelled + + MissingField: + type: string + enum: + - pet + - service + - date + - time + + BookingDraft: + type: object + additionalProperties: false + required: + - storeId + - storeName + - petId + - petName + - petType + - serviceTypeId + - serviceType + - durationMinutes + - dateConstraint + - timeWindow + - appointmentTime + - appointmentEndTime + - remark + - missingFields + properties: + storeId: + type: integer + format: int64 + storeName: + type: string + maxLength: 128 + petId: + type: integer + format: int64 + nullable: true + petName: + type: string + nullable: true + maxLength: 64 + petType: + type: string + nullable: true + maxLength: 32 + serviceTypeId: + type: integer + format: int64 + nullable: true + serviceType: + type: string + nullable: true + maxLength: 64 + durationMinutes: + type: integer + nullable: true + minimum: 1 + dateConstraint: + type: string + format: date + nullable: true + timeWindow: + $ref: '#/components/schemas/TimeWindow' + appointmentTime: + $ref: '#/components/schemas/NullableLocalDateTime' + appointmentEndTime: + $ref: '#/components/schemas/NullableLocalDateTime' + remark: + type: string + nullable: true + maxLength: 200 + missingFields: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/MissingField' + + TimeWindow: + type: object + nullable: true + additionalProperties: false + required: + - start + - end + properties: + start: + type: string + nullable: true + pattern: '^(?:[01]\d|2[0-3]):[0-5]\d$' + end: + type: string + nullable: true + pattern: '^(?:[01]\d|2[0-3]):[0-5]\d$' + + SlotOption: + type: object + additionalProperties: false + required: + - startTime + - endTime + - label + properties: + startTime: + $ref: '#/components/schemas/LocalDateTime' + endTime: + $ref: '#/components/schemas/LocalDateTime' + label: + type: string + maxLength: 64 + + QuickReply: + type: object + additionalProperties: false + required: + - type + - value + - label + properties: + type: + type: string + enum: + - pet + - service + - slot + - date + - fallback + value: + type: string + maxLength: 128 + label: + type: string + maxLength: 64 + + SessionView: + type: object + additionalProperties: false + required: + - sessionId + - status + - assistantMessage + - draft + - slotOptions + - quickReplies + - confirmable + - draftVersion + - expiresAt + properties: + sessionId: + type: string + format: uuid + status: + $ref: '#/components/schemas/BookingAgentStatus' + assistantMessage: + type: string + maxLength: 300 + description: 服务端模板渲染,不包含模型思考或供应商错误 + draft: + $ref: '#/components/schemas/BookingDraft' + slotOptions: + type: array + maxItems: 3 + items: + $ref: '#/components/schemas/SlotOption' + quickReplies: + type: array + maxItems: 10 + items: + $ref: '#/components/schemas/QuickReply' + confirmable: + type: boolean + draftVersion: + type: integer + minimum: 0 + expiresAt: + $ref: '#/components/schemas/LocalDateTime' + + LocalDateTime: + type: string + pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$' + description: Asia/Shanghai 下的本地日期时间,格式 yyyy-MM-ddTHH:mm:ss + + NullableLocalDateTime: + type: string + nullable: true + pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$' + description: null 或 Asia/Shanghai 下的本地日期时间;非空时格式为 yyyy-MM-ddTHH:mm:ss + + HandoffData: + type: object + additionalProperties: false + required: + - source + - draft + properties: + source: + type: string + enum: + - booking-agent-m0 + draft: + $ref: '#/components/schemas/BookingDraft' + + TranscriptionData: + type: object + additionalProperties: false + required: + - text + properties: + text: + type: string + maxLength: 500 + description: 可编辑转写;不返回供应商原始响应或外部 request ID + + CancelData: + type: object + additionalProperties: false + required: + - sessionId + - status + properties: + sessionId: + type: string + format: uuid + status: + type: string + enum: + - cancelled + + SessionEnvelope: + type: object + additionalProperties: false + required: + - code + - data + properties: + code: + type: integer + enum: + - 200 + data: + $ref: '#/components/schemas/SessionView' + + TranscriptionEnvelope: + type: object + additionalProperties: false + required: + - code + - data + properties: + code: + type: integer + enum: + - 200 + data: + $ref: '#/components/schemas/TranscriptionData' + + HandoffEnvelope: + type: object + additionalProperties: false + required: + - code + - data + properties: + code: + type: integer + enum: + - 200 + data: + $ref: '#/components/schemas/HandoffData' + + CancelEnvelope: + type: object + additionalProperties: false + required: + - code + - data + properties: + code: + type: integer + enum: + - 200 + data: + $ref: '#/components/schemas/CancelData' + + ErrorEnvelope: + type: object + additionalProperties: false + required: + - code + - message + - bizCode + properties: + code: + type: integer + enum: + - 400 + - 401 + - 403 + - 404 + - 409 + - 410 + - 413 + - 429 + - 503 + message: + type: string + maxLength: 200 + bizCode: + type: string + enum: + - UNAUTHENTICATED + - FORBIDDEN + - AGENT_DISABLED + - STORE_NOT_FOUND + - SESSION_NOT_FOUND + - SESSION_EXPIRED + - SESSION_TERMINAL + - DRAFT_VERSION_CONFLICT + - INVALID_INPUT + - INVALID_AUDIO + - AUDIO_TOO_LARGE + - RATE_LIMITED + - AGENT_UNAVAILABLE + - ASR_UNAVAILABLE diff --git a/contracts/booking-intent-v1.schema.json b/contracts/booking-intent-v1.schema.json new file mode 100644 index 0000000..6aaa935 --- /dev/null +++ b/contracts/booking-intent-v1.schema.json @@ -0,0 +1,138 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://petstore.internal/schemas/booking-intent-v1.schema.json", + "title": "BookingIntentV1", + "description": "智能预约 M0 模型输出。所有字段仅是非权威意图提示,业务 ID、服务事实和号源事实由 Petstore 后端重新解析。", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "intent", + "petQuery", + "serviceQuery", + "dateExpression", + "timeWindow", + "remark", + "clearFields", + "ambiguities", + "nextAction" + ], + "properties": { + "schemaVersion": { + "const": "booking-intent-v1" + }, + "intent": { + "type": "string", + "enum": [ + "book", + "modify", + "end", + "fallback" + ] + }, + "petQuery": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 64 + }, + "serviceQuery": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 64 + }, + "dateExpression": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 32 + }, + "timeWindow": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "start", + "end" + ], + "properties": { + "start": { + "type": [ + "string", + "null" + ], + "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$" + }, + "end": { + "type": [ + "string", + "null" + ], + "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$" + } + } + } + ] + }, + "remark": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 200 + }, + "clearFields": { + "type": "array", + "uniqueItems": true, + "maxItems": 5, + "items": { + "type": "string", + "enum": [ + "petQuery", + "serviceQuery", + "dateExpression", + "timeWindow", + "remark" + ] + } + }, + "ambiguities": { + "type": "array", + "uniqueItems": true, + "maxItems": 5, + "items": { + "type": "string", + "enum": [ + "pet", + "service", + "date", + "time", + "remark" + ] + } + }, + "nextAction": { + "type": "string", + "enum": [ + "ask", + "resolve_context", + "search_slots", + "show_draft", + "fallback", + "end" + ] + } + } +} diff --git a/coverage/ontology-coverage-audit.md b/coverage/ontology-coverage-audit.md index 70f3381..0736c9b 100644 --- a/coverage/ontology-coverage-audit.md +++ b/coverage/ontology-coverage-audit.md @@ -3,7 +3,7 @@ > 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。 > 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。 > -> **刷新时间**:2026-08-02(FollowUpTask 回访闭环收口后刷新) +> **刷新时间**:2026-08-02(智能预约 M0 Batch 0 契约冻结后刷新) ## 审计方法 @@ -18,16 +18,16 @@ | 类型 | 数量 | |------|------| -| Entity | 29 | -| Action | 75 | -| Event | 33 | -| Rule | 49 | -| Relation | 102 | -| **合计** | **288** | +| Entity | 30 | +| Action | 80 | +| Event | 36 | +| Rule | 55 | +| Relation | 112 | +| **合计** | **313** | -证据强度:`anchored` 249(86.5%)/ `documented` 28 / `gap` 9 / `implemented` 2。 +证据强度:`anchored` 250(79.9%)/ `documented` 52 / `gap` 9 / `implemented` 2。 -> anchored 占比相对 Batch 2 下降,是因为主动录入了 P1/P2 `gap` 占位与客户端埋点 `documented` 条目;端点漂移仍为 0。 +> anchored 占比相对早期快照下降,是因为主动录入了 P1/P2 `gap`、客户端埋点和智能预约 Batch 0 `documented` 条目;端点漂移仍为 0。 校验: @@ -36,7 +36,7 @@ python3 docs/graph/validate_ontology.py docs python3 docs/graph/audit_drift.py ``` -当前结果:校验通过;HTTP 端点 68/68、JPA Entity 16/16 对齐;非 JPA 领域实体 13 个;代码与本体均无单边漂移。 +当前结果:校验通过;HTTP 端点 68/68、JPA Entity 16/16 对齐;非 JPA/计划领域实体 14 个;代码与本体均无单边漂移。智能预约五个未实现端点以 `planned METHOD /path` 建模,不冒充当前代码端点。 ## 元数据卫生 @@ -72,9 +72,10 @@ python3 docs/graph/audit_drift.py | customer_timeline | ✅ | ✅ read model | StoreCustomerTimelineServiceTest | anchored | StoreCustomer + BusinessEvent 的同店低敏事实投影;jpa:false | | highlight_fail_reason | ✅ | ✅ 枚举 | ReportHighlightVideoServiceTest | anchored | jpa:false | | current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false | +| booking_agent_session | ✅ | —(Batch 0 设计) | — | documented | M0 短期会话;实现批创建 JPA/迁移/测试后才可改 anchored | | membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 | -## 规则覆盖度(49 条) +## 规则覆盖度(55 条) | 规则 | 测试 | 证据 | |------|------|------| @@ -101,6 +102,7 @@ python3 docs/graph/audit_drift.py | BR-AUDIT-001 | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored | | BR-SC-002 客户时间线低敏投影 | StoreCustomerTimelineServiceTest / AdminServiceCustomerControllerTest | anchored | | BR-FU-001/002 回访状态机与真实再次预约 | FollowUpTaskServiceTest / AppointmentServiceTest / MigrationTest | anchored | +| BR-BA-001..005 智能预约 M0 边界 | 实现前;ADR + OpenAPI + JSON Schema 已冻结 | documented | ## 待补 gap 清单 @@ -118,6 +120,7 @@ python3 docs/graph/audit_drift.py | 低 | 成片真 FFmpeg 端到端集成测(本机有 ffmpeg) | 🟡 单测已覆盖状态机与归类 | | 低 | `action:logout` / `event:session_revoked` 服务端吊销 | 🟡 gap | | 低 | 废弃 `register_store` / wechat OAuth demo 代码路径 | 🟡 deprecated 已标 | +| 高 | 智能预约 M0 会话、五个 endpoint、三个事件和五条规则实现证据 | 🟡 documented / Ready;代码+测试同批才转 anchored | | — | 会员/储值/套餐/历史报告/看板 | gap 占位,不展开 | ## P1/P2 方向(jsonl 已占位) @@ -144,4 +147,16 @@ python3 docs/graph/audit_drift.py **页面清单 PRD(已写)**:`docs/门店管理后台-PhaseA-页面清单PRD.md`(A0–A7 全页字段/筛选/API/验收)。 -当前 validate 规模以脚本输出为准(288 条目)。 +当前 validate 规模以脚本输出为准(313 条目)。 + +## 智能预约 M0 Batch 0 状态(2026-08-02) + +| 类型 | 已冻结 ID | 证据 | +|------|------|------| +| Entity | `entity:booking_agent_session` | documented | +| Action | `create/submit/transcribe/handoff/cancel_booking_agent_*` 共 5 条 | documented | +| Event | `booking_agent_started/draft_ready/fallback` | documented | +| Rule | `BR-BA-001..005` | documented | +| Relation | owner/store/session 读写与三条 EMITS 共 10 条 | documented | + +契约评审已通过,尚未实现。下一实现批必须以真实 code/test 路径替换空证据,并将 `BookingAgentSession` 从计划对象切换为真实 JPA 实体;禁止仅修改 `evidence` 文字。 diff --git a/graph/ontology.jsonl b/graph/ontology.jsonl index 6e5ee0f..bf4bb00 100644 --- a/graph/ontology.jsonl +++ b/graph/ontology.jsonl @@ -287,3 +287,27 @@ {"id":"rel:create_appointment_emits_rebook_created","type":"Relation","subject":"action:create_appointment","predicate":"EMITS","object":"event:rebook_created","domain":"lead","evidence":"anchored"} {"id":"rel:br_fu_001_applies_follow_up","type":"Relation","subject":"rule:BR-FU-001","predicate":"APPLIES_TO","object":"action:start_follow_up_task","domain":"lead","evidence":"anchored"} {"id":"rel:br_fu_002_applies_create_appointment","type":"Relation","subject":"rule:BR-FU-002","predicate":"APPLIES_TO","object":"action:create_appointment","domain":"lead","evidence":"anchored"} +{"id":"entity:booking_agent_session","type":"Entity","name":"BookingAgentSession","nameZh":"智能预约短期会话","domain":"booking_agent","jpa":false,"fields":["id","session_id","customer_user_id","store_id","status","draft_json","draft_version","entry_source","input_modality","expires_at","create_time","update_time"],"systems":["Backend Core","Customer Experience FE"],"code":"","doc":"docs/ontology/objects.md#entity:booking_agent_session; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned","note":"Batch 0 冻结设计;尚未创建 JPA 实体,实现批需将 jpa 与证据切换为真实状态"} +{"id":"action:create_booking_agent_session","type":"Action","name":"createBookingAgentSession","nameZh":"创建智能预约会话","domain":"booking_agent","inputs":["storeId","entity:current_user"],"outputs":["entity:booking_agent_session","event:booking_agent_started"],"entrypoints":["planned POST /api/booking-agent/sessions"],"appliesRules":["rule:BR-AUTH-001","rule:BR-BA-001","rule:BR-BA-003","rule:BR-BA-005","rule:BR-BE-001"],"code":"","doc":"docs/ontology/actions.md#action:create_booking_agent_session; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"action:submit_booking_agent_message","type":"Action","name":"submitBookingAgentMessage","nameZh":"提交智能预约消息","domain":"booking_agent","inputs":["entity:booking_agent_session","entity:current_user","draftVersion","inputType","text"],"outputs":["entity:booking_agent_session","event:booking_agent_draft_ready"],"entrypoints":["planned POST /api/booking-agent/sessions/{sessionId}/messages"],"appliesRules":["rule:BR-AUTH-001","rule:BR-BA-001","rule:BR-BA-002","rule:BR-BA-003","rule:BR-BA-004","rule:BR-BA-005","rule:BR-BE-001"],"code":"","doc":"docs/ontology/actions.md#action:submit_booking_agent_message; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"action:transcribe_booking_agent_audio","type":"Action","name":"transcribeBookingAgentAudio","nameZh":"转写智能预约语音","domain":"booking_agent","inputs":["entity:booking_agent_session","entity:current_user","audio"],"outputs":["editableText"],"entrypoints":["planned POST /api/booking-agent/sessions/{sessionId}/transcriptions"],"appliesRules":["rule:BR-AUTH-001","rule:BR-BA-001","rule:BR-BA-004","rule:BR-BA-005"],"code":"","doc":"docs/ontology/actions.md#action:transcribe_booking_agent_audio; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"action:handoff_booking_agent_draft","type":"Action","name":"handoffBookingAgentDraft","nameZh":"交接智能预约草稿","domain":"booking_agent","inputs":["entity:booking_agent_session","entity:current_user","draftVersion"],"outputs":["entity:booking_agent_session","event:booking_agent_fallback"],"entrypoints":["planned POST /api/booking-agent/sessions/{sessionId}/fallback"],"appliesRules":["rule:BR-AUTH-001","rule:BR-BA-001","rule:BR-BA-003","rule:BR-BA-004","rule:BR-BA-005","rule:BR-BE-001"],"code":"","doc":"docs/ontology/actions.md#action:handoff_booking_agent_draft; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"action:cancel_booking_agent_session","type":"Action","name":"cancelBookingAgentSession","nameZh":"结束智能预约会话","domain":"booking_agent","inputs":["entity:booking_agent_session","entity:current_user"],"outputs":["entity:booking_agent_session"],"entrypoints":["planned DELETE /api/booking-agent/sessions/{sessionId}"],"appliesRules":["rule:BR-AUTH-001","rule:BR-BA-001","rule:BR-BA-003"],"code":"","doc":"docs/ontology/actions.md#action:cancel_booking_agent_session; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"event:booking_agent_started","type":"Event","name":"BookingAgentStarted","nameZh":"智能预约会话已启动","domain":"booking_agent","payload":["storeId","aggregateType","aggregateId","actorUserId","actorRole","source","entrySource","occurredAt"],"emittedBy":["action:create_booking_agent_session"],"appliesRules":["rule:BR-BA-001","rule:BR-BA-004","rule:BR-BE-001"],"code":"","doc":"docs/ontology/events.md#event:booking_agent_started; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"event:booking_agent_draft_ready","type":"Event","name":"BookingAgentDraftReady","nameZh":"智能预约草稿已就绪","domain":"booking_agent","payload":["storeId","aggregateType","aggregateId","actorUserId","actorRole","source","inputModality","occurredAt"],"emittedBy":["action:submit_booking_agent_message"],"appliesRules":["rule:BR-BA-002","rule:BR-BA-003","rule:BR-BA-004","rule:BR-BE-001"],"code":"","doc":"docs/ontology/events.md#event:booking_agent_draft_ready; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"event:booking_agent_fallback","type":"Event","name":"BookingAgentFallback","nameZh":"智能预约已降级交接","domain":"booking_agent","payload":["storeId","aggregateType","aggregateId","actorUserId","actorRole","source","reason","occurredAt"],"emittedBy":["action:handoff_booking_agent_draft"],"appliesRules":["rule:BR-BA-003","rule:BR-BA-004","rule:BR-BA-005","rule:BR-BE-001"],"code":"","doc":"docs/ontology/events.md#event:booking_agent_fallback; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"rule:BR-BA-001","type":"Rule","name":"bookingAgentCustomerStoreBoundary","nameZh":"智能预约 customer 与门店边界","domain":"booking_agent","appliesTo":["action:create_booking_agent_session","action:submit_booking_agent_message","action:transcribe_booking_agent_audio","action:handoff_booking_agent_draft","action:cancel_booking_agent_session"],"code":"","doc":"docs/ontology/rules.md#rule:BR-BA-001; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"rule:BR-BA-002","type":"Rule","name":"bookingAgentModelNonAuthority","nameZh":"模型非权威与结构强校验","domain":"booking_agent","appliesTo":["action:submit_booking_agent_message"],"code":"","doc":"docs/ontology/rules.md#rule:BR-BA-002; docs/contracts/booking-intent-v1.schema.json; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"rule:BR-BA-003","type":"Rule","name":"bookingAgentStateVersionTtlNoWrite","nameZh":"会话状态版本 TTL 与无写路径","domain":"booking_agent","appliesTo":["action:create_booking_agent_session","action:submit_booking_agent_message","action:handoff_booking_agent_draft","action:cancel_booking_agent_session"],"code":"","doc":"docs/ontology/rules.md#rule:BR-BA-003; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"rule:BR-BA-004","type":"Rule","name":"bookingAgentPrivacyMinimization","nameZh":"语音原文与日志最小化","domain":"booking_agent","appliesTo":["action:submit_booking_agent_message","action:transcribe_booking_agent_audio","action:handoff_booking_agent_draft"],"code":"","doc":"docs/ontology/rules.md#rule:BR-BA-004; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"rule:BR-BA-005","type":"Rule","name":"bookingAgentDisabledRateLimitedFallback","nameZh":"默认关闭限流与可退化","domain":"booking_agent","appliesTo":["action:create_booking_agent_session","action:submit_booking_agent_message","action:transcribe_booking_agent_audio","action:handoff_booking_agent_draft"],"code":"","doc":"docs/ontology/rules.md#rule:BR-BA-005; docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md","test":"","evidence":"documented","status":"planned"} +{"id":"rel:user_has_booking_agent_session","type":"Relation","subject":"entity:user","predicate":"HAS","object":"entity:booking_agent_session","domain":"booking_agent","evidence":"documented"} +{"id":"rel:store_has_booking_agent_session","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:booking_agent_session","domain":"booking_agent","evidence":"documented"} +{"id":"rel:create_booking_agent_session_creates_session","type":"Relation","subject":"action:create_booking_agent_session","predicate":"CREATES","object":"entity:booking_agent_session","domain":"booking_agent","evidence":"documented"} +{"id":"rel:submit_booking_agent_message_updates_session","type":"Relation","subject":"action:submit_booking_agent_message","predicate":"UPDATES","object":"entity:booking_agent_session","domain":"booking_agent","evidence":"documented"} +{"id":"rel:transcribe_booking_agent_audio_reads_session","type":"Relation","subject":"action:transcribe_booking_agent_audio","predicate":"READS","object":"entity:booking_agent_session","domain":"booking_agent","evidence":"documented"} +{"id":"rel:handoff_booking_agent_draft_updates_session","type":"Relation","subject":"action:handoff_booking_agent_draft","predicate":"UPDATES","object":"entity:booking_agent_session","domain":"booking_agent","evidence":"documented"} +{"id":"rel:cancel_booking_agent_session_updates_session","type":"Relation","subject":"action:cancel_booking_agent_session","predicate":"UPDATES","object":"entity:booking_agent_session","domain":"booking_agent","evidence":"documented"} +{"id":"rel:create_booking_agent_session_emits_started","type":"Relation","subject":"action:create_booking_agent_session","predicate":"EMITS","object":"event:booking_agent_started","domain":"booking_agent","evidence":"documented"} +{"id":"rel:submit_booking_agent_message_emits_draft_ready","type":"Relation","subject":"action:submit_booking_agent_message","predicate":"EMITS","object":"event:booking_agent_draft_ready","domain":"booking_agent","evidence":"documented"} +{"id":"rel:handoff_booking_agent_draft_emits_fallback","type":"Relation","subject":"action:handoff_booking_agent_draft","predicate":"EMITS","object":"event:booking_agent_fallback","domain":"booking_agent","evidence":"documented"} diff --git a/ontology/README.md b/ontology/README.md index abcfaad..be676c4 100644 --- a/ontology/README.md +++ b/ontology/README.md @@ -61,6 +61,8 @@ rel:subject_predicate_object # rel:report_belongs_to_appointment 已覆盖 P0 主闭环实体与动作,并补录:`Pet`、`ServiceType`、`ScheduleBlock`、`HighlightVideo`、`SessionToken`、`ReportImage`、`ReportTestimonial`、`ServiceInterval`、`HighlightFailReason`、`CurrentUser`、`FollowUpTask`,以及成片/上传/鉴权/寄语/退订/回访状态机/真实再次预约相关规则。 +**智能预约助手 M0** 已以 `documented` 证据录入 `BookingAgentSession`、5 个动作、3 个漏斗事件、`BR-BA-001..005` 和 10 条关系。它们表示 Batch 0 契约已冻结,**不表示代码已实现**;五个计划 endpoint 以 `planned METHOD /path` 录入,实现时必须与代码、测试同批转为 `anchored`。 + **门店 Web 后台 Phase A**:`AdminConsole`、`Workbench`、`WorkbenchTodoItem` 仍是非 JPA 读模型;`StoreCustomer` 与 `FollowUpTask` 已落为 JPA 稳定业务对象。`BR-ADMIN-001`…`004`、`BR-FU-001/002` 与 admin actions 继续约束后台;收银/会员资产仍为 gap。 P1/P2 方向(会员/储值/套餐/历史报告/看板)以 `gap` 证据条目录入,不展开字段;受 `rule:BR-ADMIN-004` 约束不得偷渡进 Phase A 设计。 diff --git a/ontology/actions.md b/ontology/actions.md index 23e0c89..81a2a64 100644 --- a/ontology/actions.md +++ b/ontology/actions.md @@ -715,3 +715,52 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_ - **入口**:Admin UI `/schedule`;`GET /api/schedule/day`;`POST/DELETE /api/schedule/block` - **适用规则**:`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-SCH-001` - **证据**:`anchored` + +## 智能预约助手 M0(Batch 0 · documented) + +> 下列入口已冻结但尚未注册;机器图以 `planned METHOD /path` 标识,不得误报为已实现 endpoint。 + +### action:create_booking_agent_session + +使用已选有效 `storeId` 为当前 customer 创建短期会话,并首次记录 `booking_agent_started`。 + +- **计划入口**:`POST /api/booking-agent/sessions` +- **输出**:`entity:booking_agent_session` +- **规则**:`rule:BR-AUTH-001`、`rule:BR-BA-001`、`rule:BR-BA-003`、`rule:BR-BA-005`、`rule:BR-BE-001` +- **证据**:`documented` + +### action:submit_booking_agent_message + +在 `sessionId + current.userId` 范围内校验草稿版本,将当轮文字解析为非权威约束,再由后端解析真实宠物、服务与号源。 + +- **计划入口**:`POST /api/booking-agent/sessions/{sessionId}/messages` +- **输入/输出**:`entity:booking_agent_session`、`draftVersion`、`inputType`、`text` +- **规则**:`rule:BR-BA-001` … `rule:BR-BA-005` +- **触发事件**:首次进入 `confirmable` 时 `event:booking_agent_draft_ready` +- **证据**:`documented` + +### action:transcribe_booking_agent_audio + +在本人未终止 session 下将短音频转为可编辑文字,不自动进入意图处理。 + +- **计划入口**:`POST /api/booking-agent/sessions/{sessionId}/transcriptions` +- **规则**:`rule:BR-BA-001`、`rule:BR-BA-004`、`rule:BR-BA-005` +- **证据**:`documented` + +### action:handoff_booking_agent_draft + +返回服务端验证的部分或完整草稿,将会话转为 `fallback`,由现有表单承担最终提交。 + +- **计划入口**:`POST /api/booking-agent/sessions/{sessionId}/fallback` +- **输出**:一次性草稿交接对象 +- **规则**:`rule:BR-BA-001`、`rule:BR-BA-003`、`rule:BR-BA-005` +- **触发事件**:`event:booking_agent_fallback` +- **证据**:`documented` + +### action:cancel_booking_agent_session + +由当前 customer 结束本人未终止会话,仅将 session 标记为 `cancelled`。 + +- **计划入口**:`DELETE /api/booking-agent/sessions/{sessionId}` +- **规则**:`rule:BR-BA-001`、`rule:BR-BA-003` +- **证据**:`documented` diff --git a/ontology/events.md b/ontology/events.md index a47fd24..b89d772 100644 --- a/ontology/events.md +++ b/ontology/events.md @@ -337,3 +337,35 @@ - **触发动作**:`action:view_workbench` - **payload**:`storeId`、`userId`、`ts` - **证据**:`gap`(A 期可不实现上报) + +## 智能预约助手 M0(Batch 0 · documented) + +### event:booking_agent_started + +智能预约 session 创建成功的首次事实。 + +- **触发动作**:`action:create_booking_agent_session` +- **幂等键**:`booking_agent_started:{sessionDbId}` +- **BusinessEvent**:`storeId=session.storeId`、`aggregateType=booking_agent_session`、`aggregateId=session.id` +- **metadata 白名单**:`entrySource` +- **证据**:`documented` + +### event:booking_agent_draft_ready + +session 首次进入 `confirmable` 的事实,不表示预约已创建。 + +- **触发动作**:`action:submit_booking_agent_message` +- **幂等键**:`booking_agent_draft_ready:{sessionDbId}` +- **metadata 白名单**:`inputModality` +- **证据**:`documented` + +### event:booking_agent_fallback + +用户带草稿返回普通表单,或 LLM 失败后进入降级的首次事实。 + +- **触发动作**:`action:handoff_booking_agent_draft` +- **幂等键**:`booking_agent_fallback:{sessionDbId}` +- **metadata 白名单**:`reason=user/llm_unavailable` +- **证据**:`documented` + +M0 三个事件均禁止写入输入原文、转写文本、备注、宠物名、服务名、供应商 request ID 或错误原文;M0 不定义 `booking_agent_confirmed`。 diff --git a/ontology/objects.md b/ontology/objects.md index d3b3dea..d14a48b 100644 --- a/ontology/objects.md +++ b/ontology/objects.md @@ -540,3 +540,30 @@ API 动态聚合字段:`pets`、`lastVisitAt`、`lastReportId`、`leadStatus` - `entity:analytics_dashboard`(经营分析看板)— B 期;A 期仅工作台轻量指标 后续如进入实现,再补完整字段、动作与规则。 + +--- + +## 智能预约助手 M0(Batch 0 契约) + +## entity:booking_agent_session + +**智能预约短期会话**。它承载已登录 customer 在一个已选门店下的结构化草稿和六态状态机,不是预约,不持有任何预约写入权。 + +| 字段 | 说明 | +|------|------| +| `id` | BIGINT 内部主键;用于低敏 `BusinessEvent.aggregateId` | +| `session_id` | 唯一外部 UUID | +| `customer_user_id` | 创建会话的 customer,只从登录上下文派生 | +| `store_id` | 创建时必填并验证,会话期间固定 | +| `status` | `collecting / proposing / confirmable / fallback / expired / cancelled` | +| `draft_json` / `draft_version` | 最小已验证草稿与乐观版本 | +| `entry_source` | M0 固定 `appointment_create` | +| `input_modality` | `text / voice / mixed`,首次成功消息后派生 | +| `expires_at` | 创建后 30 分钟,不滑动续期 | +| `create_time / update_time` | 服务端审计时间 | + +- **不存储**:原始语音、转写原文、完整对话、提示词、模型原始请求/响应、`appointment_id`。 +- **归属系统**:Backend Core(计划 `bookingagent` 包)。 +- **文档**:`docs/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md`。 +- **证据**:`documented`(尚未创建 JPA 实体,机器图临时 `jpa:false`;实现批必须切换为真实 JPA 证据)。 +- **约束**:`rule:BR-BA-001` … `rule:BR-BA-005`。 diff --git a/ontology/relations.md b/ontology/relations.md index 71e0cea..fc43463 100644 --- a/ontology/relations.md +++ b/ontology/relations.md @@ -187,3 +187,18 @@ | `business_event` PROJECTS_TO `customer_timeline` | 多条不可变事实经低敏白名单投影为时间线 | | `view_store_customer_timeline` READS `store_customer` / `business_event` | 同店校验后分页读取主档与事实 | | `admin_list_leads` READS `follow_up_task` | `/leads` 导航承载真实操作任务池,不直接编辑留资事实 | + +## 智能预约助手 M0 关系(Batch 0 · documented) + +| 关系 | 说明 | +|------|------| +| `user` HAS `booking_agent_session` | 一个 customer 可创建多个短期会话,每次读写校验 owner | +| `store` HAS `booking_agent_session` | 每个会话在创建时固定一家门店 | +| `create_booking_agent_session` CREATES `booking_agent_session` | 创建短期草稿会话 | +| `submit_booking_agent_message` UPDATES `booking_agent_session` | 状态与草稿版本乐观更新 | +| `transcribe_booking_agent_audio` READS `booking_agent_session` | 用 session 派生 owner/store 上下文,只返回可编辑文本 | +| `handoff_booking_agent_draft` UPDATES `booking_agent_session` | 转为 fallback 并返回一次性草稿 | +| `cancel_booking_agent_session` UPDATES `booking_agent_session` | 转为 cancelled | +| `create_booking_agent_session` EMITS `booking_agent_started` | 创建成功的首次低敏事实 | +| `submit_booking_agent_message` EMITS `booking_agent_draft_ready` | 首次进入 confirmable 时记录 | +| `handoff_booking_agent_draft` EMITS `booking_agent_fallback` | 用户交接或 LLM 降级的首次事实 | diff --git a/ontology/rules.md b/ontology/rules.md index fd50fca..da541b7 100644 --- a/ontology/rules.md +++ b/ontology/rules.md @@ -479,3 +479,40 @@ - **代码**:`backend/src/main/java/com/petstore/service/BusinessEventService.java`、`AdminBusinessEventService.java` - **测试**:`BusinessEventServiceTest`、`BusinessEventMigrationTest`、`AdminBusinessEventControllerTest` - **证据**:`anchored` + +## 智能预约助手 M0(Batch 0 · documented) + +### rule:BR-BA-001 + +**customer 与门店数据边界**:所有 endpoint 只允许已登录 customer;session 每次以 `sessionId + current.userId` 查询;他人与不存在 session 同样返回 `SESSION_NOT_FOUND`。`storeId` 只在创建时验证,后续不可覆盖。 + +- **适用动作**:M0 全部五个动作 +- **证据**:`documented` + +### rule:BR-BA-002 + +**模型非权威与结构强校验**:模型只返回通过 `booking-intent-v1` 的文本约束,未知字段拒绝。宠物、服务、日期时间和号源由后端确定性重新解析;模型不得产生 ID、价格、号源或预约状态事实。 + +- **适用动作**:`action:submit_booking_agent_message` +- **证据**:`documented` + +### rule:BR-BA-003 + +**状态、版本、TTL 与无写路径**:M0 只允许六个会话状态,草稿更新必须校验 `draftVersion`;TTL 固定 30 分钟且不滑动续期,过期 24 小时后物理清理。M0 不注册 `/confirm`、不调用预约写服务、不修改 `Appointment`。 + +- **适用动作**:创建、提交消息、草稿交接、结束会话 +- **证据**:`documented` + +### rule:BR-BA-004 + +**语音、原文与日志最小化**:原始语音、当轮用户原文、完整对话、提示词和模型原始请求/响应不落库、不进事件、不进日志。音频最多 60 秒/3 MB,转写完立即释放,且必须经用户编辑/确认后才发消息。 + +- **适用动作**:提交消息、语音转写、草稿交接 +- **证据**:`documented` + +### rule:BR-BA-005 + +**默认关闭、限流与可退化**:`PETSTORE_BOOKING_AGENT_ENABLED` 默认 `false`。每 customer 10 分钟内最多创建 5 会话、30 条消息、10 段语音;模型/ASR/外部查询失败时保留已验证草稿并退回普通表单,不影响普通预约。单实例进程内限流只是 M0 临时边界,多实例前必须改为共享状态。 + +- **适用动作**:创建会话、提交消息、语音转写、草稿交接 +- **证据**:`documented` diff --git a/宠小它智能预约助手-产品与技术方案-v0.1.md b/宠小它智能预约助手-产品与技术方案-v0.1.md index 20cde60..8acca51 100644 --- a/宠小它智能预约助手-产品与技术方案-v0.1.md +++ b/宠小它智能预约助手-产品与技术方案-v0.1.md @@ -1,7 +1,7 @@ # 宠小它智能预约助手:产品与技术方案 v0.1 > 日期:2026-08-02
-> 状态:**Draft / 待产品、架构、数据与隐私评审;尚未实现**
+> 状态:**M0 产品/架构/数据契约已冻结;尚未实现;真实供应商与生产隐私评审未完成**
> 目标用户:`customer` 宠主
> Workstream:`Core Booking Flow`
> 建议 Owner:`Product Design` + `Customer Experience FE` + `Backend Core`
@@ -15,12 +15,16 @@ - [模型与语音供应商选型](./智能预约助手-模型与语音供应商选型-2026-08-02.md) - [M0 编码任务 brief](./智能预约助手-M0编码任务brief-2026-08-02.md) +- [M0 契约与数据模型 ADR](./架构决策-智能预约助手M0契约与数据模型-2026-08-02.md) +- [`booking-intent-v1` JSON Schema](./contracts/booking-intent-v1.schema.json) +- [M0 OpenAPI 契约](./contracts/booking-agent-m0.openapi.yaml) +- [M0 执行队列](./智能预约助手-M0执行队列-2026-08-02.md) ## 1. 结论 建议将该能力定义为 **「可执行的智能预约助手」**,而不是泛化客服聊天机器。 -它负责把宠主的自然语言转换为结构化预约草稿,通过现有 Petstore 预约域查询真实宠物、门店服务和可约时段,最后由用户明确确认后才创建 `Appointment`。 +它负责把宠主的自然语言转换为结构化预约草稿,通过现有 Petstore 预约域查询真实宠物、门店服务和可约时段。M0 只将草稿带入现有表单,仍由用户在普通表单中提交;对话内直接创建 `Appointment` 是 M1 候选能力。 **核心原则:模型负责「听懂人话」,Petstore 预约域负责「决定什么能约」。** @@ -38,6 +42,8 @@ ### 2.2 入口优先级 +下列为长期入口价值排序。M0 已冻结为仅从 `CustAppointmentCreate` 顶部次级入口进入,且必须先用现有控件选定门店。 + 1. **服务报告或历史预约:「跟助手约下次」** 上下文最完整,可在登录与归属校验后预填门店、宠物和上次服务。 2. **宠主首页:「一句话预约」** @@ -108,6 +114,7 @@ - 普通预约仍保持「提交时登录 + guest 草稿恢复」的现有口径。 - 智能助手 MVP 在创建服务端会话前要求 customer 登录,因为它需要读取归属宠物和历史事实。 +- M0 在创建会话前还要求已选定有效门店;对话不负责选店,session 创建后不允许切店。 - 客户端可在登录前保存入口来源和非敏感意图,但不得在未登录状态调用宠物或历史数据工具。 - 会话过期后可将已解析的非敏感草稿回填到普通表单,不能自动续约或提交。 @@ -161,8 +168,10 @@ flowchart LR H --> K[确认卡] I --> K J --> K - K --> L[用户明确确认] - L --> M[AppointmentService 最终复核与创建] + K --> L[M0 带入普通预约] + L --> M[CustAppointmentCreate 回填] + M --> N[现有预约域复核与创建] + K -. M1 另立 ADR 后 .-> O[对话内受控确认] ``` ### 6.1 模型可以做的事 @@ -171,24 +180,24 @@ flowchart LR - 提取非权威文本约束:宠物称呼、服务需求、相对日期、时间偏好、备注; - 根据状态机的下一动作生成简短、可理解的回复。 -建议将模型输出限制为 JSON Schema: +模型输出必须通过冻结的 [`booking-intent-v1`](./contracts/booking-intent-v1.schema.json) JSON Schema: ```json { - "intent": "book | modify | end | fallback", - "draftPatch": { - "petQuery": "球球", - "serviceQuery": "洗澡", - "dateExpression": "本周六", - "timeWindow": { "start": "15:00", "end": null }, - "remark": null - }, + "schemaVersion": "booking-intent-v1", + "intent": "book", + "petQuery": "球球", + "serviceQuery": "洗澡", + "dateExpression": "本周六", + "timeWindow": { "start": "15:00", "end": null }, + "remark": null, + "clearFields": [], "ambiguities": [], - "nextAction": "ask | resolve_context | search_slots | show_confirmation | fallback" + "nextAction": "resolve_context" } ``` -`petQuery` 和 `serviceQuery` 只是搜索文本,不是业务 ID。所有 ID 必须由后端在当前登录上下文和门店数据范围中解析。 +`petQuery` 和 `serviceQuery` 只是搜索文本,不是业务 ID。所有 ID 必须由后端在当前登录上下文和门店数据范围中解析;未知字段和越界枚举直接拒绝,不从残缺输出猜测。 ### 6.2 模型不可以做的事 @@ -209,7 +218,7 @@ flowchart LR | `search_available_slots` | 只读 | 开始/结束时间、可约性 | 换日期或切换表单 | | `resolve_previous_booking` | 只读 | 归属校验后的门店/宠物/服务 ID | 忽略历史预填并继续收集 | -白名单中不包含写工具。最终创建由确认 endpoint 在模型调用之外完成。 +白名单中不包含写工具。M0 由普通表单在助手之外完成最终创建;M1 如增加确认 endpoint,也必须在模型调用之外进入预约域。 ## 7. 预约草稿与数据模型提案 @@ -225,43 +234,41 @@ flowchart LR | `timeWindow` | 模型提取 | 否 | 只是偏好约束 | | `appointmentTime` | 真实号源选择 | 是 | 必须是精确的未来时间 | | `remark` | 宠主原意摘要 | 否 | 限长、可编辑,不生成医疗结论 | -| `sourceAppointmentId` | 已验证复约入口 | 是 | 可空,必须归属当前 customer | | `draftVersion` | 服务端 | 是 | 每次草稿变更递增,确认时防旧版提交 | 确认时不直接信任草稿中的展示快照。后端需重新加载 Pet、ServiceType、Store 和容量事实,再由现有预约域生成 `petName`、`petType`、`serviceType`和 `durationMinutes` 快照。 ### 7.2 候选会话表 -长期建议新增 `t_booking_agent_session`,不将对话状态塞入 `t_appointment`。下表是覆盖 M1 直接确认能力的完整候选形态;M0 实际字段以配套编码 brief 为准。 +新增 `t_booking_agent_session`,不将对话状态塞入 `t_appointment`。M0 字段已由 ADR 冻结: | 字段 | 语义 | |---|---| -| `session_id` | 不可预测会话 ID,主键 | +| `id` | BIGINT 自增内部主键 | +| `session_id` | VARCHAR(36) 唯一外部 UUID,不暴露内部主键 | | `customer_user_id` | 从登录上下文派生 | -| `store_id` | 会话数据范围 | -| `source_appointment_id` | 复约上下文,可空 | -| `status` | 仅使用第 5 节对话状态 | +| `store_id` | NOT NULL,会话固定数据范围 | +| `status` | M0 六个状态之一 | | `draft_json` | 最小结构化草稿,不存原始语音 | | `draft_version` | 乐观版本号 | -| `entry_source` | `appointment_create` / `home` / `appointment_history` / `report_history` / `pet_profile` | +| `entry_source` | M0 固定 `appointment_create` | | `input_modality` | `text` / `voice` / `mixed` | -| `appointment_id` | M1 成功后关联的真实预约,可空;M0 不增加 | -| `expires_at` | 会话过期时间 | +| `expires_at` | 创建后 30 分钟,不滑动续期 | | `create_time` / `update_time` | 审计时间 | 数据建议: - 以 `(customer_user_id, status, expires_at)` 建立查询索引; -- M1 增加 `appointment_id` 可空唯一关联; -- M1 确认时对 session 行加悲观锁;如已是 `booked`,重复请求直接返回同一 `Appointment`; -- MVP 会话默认 30 分钟过期,过期不等于删除预约; +- 以 `(status, expires_at)` 支持过期标记和清理; +- 会话过期 24 小时后物理删除,M0 不增加 `deleted`; +- M0 不增加 `source_appointment_id` 或 `appointment_id`; - 不新增 `t_booking_agent_turn` 原文表;模型每轮以当前草稿和当次输入工作,避免默认持久化完整对话。 -该表、索引、过期清理和迁移顺序均是 **待 Data Model 评审的提案**,不代表当前生产库已存在。 +上述是 **M0 已评审契约**,不代表当前生产库已存在;迁移实现 diff 仍必须经 Data Model Review。 ### 7.3 预约来源与事件 -不改变现有 `BusinessEvent.source=customer/admin`的操作人语义。建议在 `appointment_created` 的 `metadata_json` 增加独立维度: +不改变现有 `BusinessEvent.source=customer/admin`的操作人语义。长期可在 `appointment_created` 的 `metadata_json` 评审独立维度: ```json { @@ -273,119 +280,52 @@ flowchart LR `bookingChannel` 候选值:`form`、`agent_draft`、`agent_text`、`agent_voice`、`admin`、`follow_up`。`agent_draft` 只表示智能助手提供了已验证草稿;`agent_text` 和 `agent_voice` 保留给 M1 对话内直接创建路径。初始 M0 不修改 `appointment_created` 事件,如需归因表单提交,必须另行评审服务端验证的一次性草稿交接,不接受客户端自由上报渠道。不建议 MVP 为此修改 `Appointment` 主表。 -候选新事件: +冻结的 M0 新事件: - `booking_agent_started`; - `booking_agent_draft_ready`; -- `booking_agent_fallback`; -- `booking_agent_confirmed`(M1)。 +- `booking_agent_fallback`。 -这些事件只用于漏斗和运行分析;只有真实创建成功的 `appointment_created` 才计为预约。编码前需将候选对象、动作、事件和规则同步到 `docs/ontology/` 及 `graph/ontology.jsonl`。 +这些事件只用于漏斗和运行分析;只有真实创建成功的 `appointment_created` 才计为预约。M0 不增加 `booking_agent_confirmed`,不修改现有 `appointment_created`。 -## 8. API 契约草案 +## 8. M0 API 冻结契约 -本节是评审草案,尚未实现。响应沿用当前 `{ code, message, bizCode, data }` 业务外壳,所有 endpoint 都要求 customer session token。 +契约已冻结、尚未实现。唯一机读权威是 [M0 OpenAPI](./contracts/booking-agent-m0.openapi.yaml);响应沿用当前 `{ code, message, bizCode, data }` 外壳,除鉴权拦截的 HTTP 401 外,客户端以 body `code` 为业务结果。本功能不顺带重构全局响应语义。 -### 8.1 创建会话 +### 8.1 Endpoint -`POST /api/booking-agent/sessions` - -```json -{ - "entrySource": "home", - "storeId": 1, - "sourceAppointmentId": null -} -``` - -规则: - -- 不接受 `customerUserId`,必须从 `CurrentUserContext` 派生; -- `sourceAppointmentId` 非空时必须归属当前 customer; -- `storeId` 与源预约冲突时拒绝,不静默覆盖; -- 返回 `sessionId`、`status`、`draft`、`assistantMessage`、`expiresAt` 和 `draftVersion`。 - -### 8.2 提交文字或语音转写结果 - -`POST /api/booking-agent/sessions/{sessionId}/messages` - -```json -{ - "inputType": "text", - "text": "周六下午给球球洗澡,三点以后都行" -} -``` - -建议响应 `data`: - -```json -{ - "sessionId": "", - "status": "proposing", - "assistantMessage": "本周六有 3 个符合的时段", - "draft": {}, - "slotOptions": [ - { "startTime": "2026-08-08T15:00:00", "endTime": "2026-08-08T16:00:00" } - ], - "quickReplies": ["15:00", "16:30", "17:00", "换一天"], - "confirmable": false, - "draftVersion": 2 -} -``` - -客户端只展示可理解错误,不展示模型原始输出、提示词、堆栈或供应商错误。 - -### 8.3 语音转写 - -`POST /api/booking-agent/transcriptions` - -- `multipart/form-data`,仅接受允许的音频 MIME 和扩展名; -- M0 单段不超过 60 秒、3 MB,与供应商选型和编码 brief 保持一致; -- 后端代理调用语音服务,不向客户端下发供应商密钥; -- 语音只用于当次转写,不进入通用报告媒体存储,不持久化原始录音; -- 返回可编辑 `text` 和脱敏 `requestId`,宠主可修改识别结果再发送。 - -### 8.4 确认预约(M1,M0 不实现) - -`POST /api/booking-agent/sessions/{sessionId}/confirm` - -```json -{ - "draftVersion": 3 -} -``` - -确认 endpoint 必须: - -1. 从 session token 重新派生 customer; -2. 对 `t_booking_agent_session` 行加锁; -3. 校验 `draftVersion`、会话状态和过期时间; -4. 重新校验宠物归属、门店服务和号源; -5. 调用现有 `AppointmentService.createBooking`,保留门店行锁和连续容量桶判定; -6. 记录 `appointment_created` 及助手漏斗事件; -7. 将 session 更新为 `booked` 并关联 `appointmentId`。 - -并发或重试时,同一 session 只能创建一个预约;后续重复确认返回已创建的同一预约。 - -### 8.5 返回草稿到普通表单 - -`POST /api/booking-agent/sessions/{sessionId}/fallback` - -返回经后端验证的可回填字段,客户端导航到现有 `CustAppointmentCreate`。不返回供应商上下文、提示词、置信度或内部调试字段。 - -### 8.6 业务码草案 - -| `bizCode` | 用户语义 | 前端处理 | +| Method | Endpoint | M0 语义 | |---|---|---| -| `AGENT_SESSION_NOT_FOUND` | 会话不存在或不属于当前宠主 | 返回预约入口 | -| `AGENT_SESSION_EXPIRED` | 会话已过期 | 新建会话或切表单 | -| `AGENT_INPUT_INVALID` | 输入为空或超限 | 允许重新输入 | -| `AGENT_UNAVAILABLE` | 模型超时或输出校验失败 | 保留草稿并切表单 | -| `ASR_UNAVAILABLE` | 语音转写失败 | 重试或改用文字 | -| `DRAFT_VERSION_CONFLICT` | 确认的不是最新草稿 | 刷新确认卡 | -| `PET_CUSTOMER_MISMATCH` | 宠物归属校验失败 | 重新选宠物 | -| `SERVICE_TYPE_INVALID` | 服务不属于门店或已下线 | 重新选服务 | -| `CAPACITY_FULL` | 确认时号源已变化 | 立即查询新候选时段 | +| `POST` | `/api/booking-agent/sessions` | 以已选 `storeId` 创建 customer 会话;`entrySource` 由服务端固定 | +| `POST` | `/api/booking-agent/sessions/{sessionId}/messages` | 携带 `draftVersion`、`inputType` 和文本更新草稿 | +| `POST` | `/api/booking-agent/sessions/{sessionId}/transcriptions` | 在本人 session 下转写不超过 60 秒/3 MB 的短音频,仅返回可编辑文本 | +| `POST` | `/api/booking-agent/sessions/{sessionId}/fallback` | 携带 `draftVersion`,返回服务端验证的一次性回填草稿 | +| `DELETE` | `/api/booking-agent/sessions/{sessionId}` | 将本人非终态会话标记为 `cancelled` | + +所有 endpoint 只允许已登录 `customer`;session 每次用 `sessionId + current.userId` 查询,他人 session 和不存在 session 统一为 `SESSION_NOT_FOUND`。创建后 `storeId` 固定,消息和 fallback 必须进行草稿版本检查。 + +M0 不注册 `/confirm`,不调用 `AppointmentService.createBooking`;M1 必须新立 ADR 审查写路径、行锁、幂等和事件归因。 + +### 8.2 业务码 + +| body code | `bizCode` | 语义 | +|---|---|---| +| 401 | `UNAUTHENTICATED` | session token 缺失或失效 | +| 403 | `FORBIDDEN` | 非 customer 调用 | +| 503 | `AGENT_DISABLED` | 功能开关关闭 | +| 404 | `STORE_NOT_FOUND` | 创建时目标门店无效 | +| 404 | `SESSION_NOT_FOUND` | session 不存在或不属于当前 customer | +| 410 | `SESSION_EXPIRED` | 会话 TTL 到期 | +| 409 | `SESSION_TERMINAL` | 会话已 fallback/cancelled | +| 409 | `DRAFT_VERSION_CONFLICT` | 客户端草稿版本过旧 | +| 400 | `INVALID_INPUT` | 空文本、超长或请求结构无效 | +| 400 | `INVALID_AUDIO` | 空音频、格式/MIME/文件头不匹配或时长非法 | +| 413 | `AUDIO_TOO_LARGE` | 超过 3 MB | +| 429 | `RATE_LIMITED` | customer 请求频率超限 | +| 503 | `AGENT_UNAVAILABLE` | LLM 超时、无效 JSON/schema 或外部失败 | +| 503 | `ASR_UNAVAILABLE` | ASR 超时或外部失败 | + +宠物/服务歧义、无号源和未选时段是正常 200 会话响应,不用异常码表达。 ## 9. 语音与模型集成边界 @@ -409,7 +349,7 @@ flowchart LR - 语音失败:保留本地输入状态,提供「重新说」和「改用文字」。 - 模型失败:不自动重复多次调用;保留已验证草稿并导向普通表单。 - 工具查询失败:展示业务错误,不让模型根据记忆补齐结果。 -- 最终号源冲突:会话进入 `needs_reselection`,不创建失败预约或假成功记录。 +- M1 最终号源冲突:会话进入 `needs_reselection`,不创建失败预约或假成功记录。M0 只查询候选号源并回填表单。 ## 10. 安全、隐私与可观测性 @@ -476,7 +416,7 @@ flowchart LR ### M1:受控确认创建 -- 增加 session 表、确认 endpoint、行锁和幂等返回; +- 在 M0 session 表上增加 `appointment_id`、确认 endpoint、行锁和幂等返回; - 确认时重新校验权限、宠物归属、服务和容量; - 补齐助手漏斗事件和 `bookingChannel`; - 通过硬性安全验收后,再允许确认卡直接创建预约。 @@ -492,19 +432,21 @@ flowchart LR ### 13.1 硬性门禁 -- [ ] 未点击「确认预约」前不创建 `Appointment`。 +- [ ] M0 不存在助手 `/confirm` endpoint,不调用预约写服务。 - [ ] 助手不会返回门店不存在的服务或后端未返回的号源。 -- [ ] 确认时复核服务时长、连续容量桶和门店锁,不产生超卖。 -- [ ] 同一 session 并发或重复确认只有一个 `Appointment`。 +- [ ] 草稿只使用当前 customer 的宠物、session 门店服务和后端真实号源。 +- [ ] 门店在会话前选定,session 创建后不能跨店更改。 - [ ] A customer 不能查询或选择 B customer 的宠物、历史预约或 session。 - [ ] 不将手机号、完整 `report_token`、session token 或密钥发送给模型或写入日志。 - [ ] 原始语音转写后立即释放,不出现在通用上传目录或报告媒体中。 - [ ] 模型、语音或工具失败时能保留已验证草稿并切换普通表单。 - [ ] `Appointment.status` 仍只有 `new`、`doing`、`done`、`cancel`。 -- [ ] 助手生成的真实预约有 `appointment_created` 事件和可统计 `bookingChannel`。 +- [ ] 仅记录 M0 三个漏斗事件,不修改 `appointment_created` 事件语义。 ### 13.2 价值验证建议 +首批 M0 先观察 `draft_ready / started`、`fallback / started`、语音转写修改率和普通表单回填成功率。下列「助手到真实预约」指标只能在有可验证的服务端草稿归因后使用,不接受客户端自由上报。 + 首批 20 个真实 session 用于发现语料和交互问题,不立即下商业化结论。稳定后可对 20~50 个老客 session 使用以下建议门槛: - 助手启动到真实预约成功率 ≥ 70%; @@ -515,22 +457,27 @@ flowchart LR 上述价值门槛是试点假设,应在首批真实数据后与普通表单基线一起复核;不得用演示会话、测试预约或未履约预约代替真实样本。 -## 14. 开发前待冻结决策 +## 14. M0 已冻结决策与生产前缺口 | ID | 决策 | 建议默认 | 责任人 | |---|---|---|---| -| D1 | M0 首发入口 | `CustAppointmentCreate` 顶部次级入口;历史预约、报告和首页入口后续评估 | Product Design | +| D1 | M0 首发入口 | `CustAppointmentCreate` 顶部次级入口;先选门店,再登录/创建 session | Product Design | | D2 | 是否支持匿名助手 | MVP 不支持,普通表单继续支持 guest 草稿 | Product Design + System Architect | | D3 | M0 是否直接创建预约 | 否,先回填现有表单 | Product Design + Backend Core | -| D4 | 模型与语音供应商 | M0 推荐阿里云百炼北京 `qwen3-asr-flash-2026-02-10 + qwen-plus-2025-12-01`,腾讯 ASR 作对照;生产前仍需语料和隐私复核 | System Architect + Backend Ops | +| D4 | 模型与语音供应商 | 开发适配目标已选;生产开启仍受语料对照、数据区域和隐私复核阻塞 | System Architect + Backend Ops | | D5 | 原文保留 | 默认不持久化对话原文,不持久化原始语音 | Access / Privacy Review | -| D6 | session TTL | MVP 30 分钟,编码前由 Data Model 冻结 | Data Model | +| D6 | session TTL | 创建后固定 30 分钟,不滑动续期;过期 24 小时后物理删除 | Data Model | | D7 | 助手对话风格 | 简短、一次一个关键问题,不模拟人格客服 | Product Design | | D8 | 试点启动时机 | RC6 基础链路已用真实门店跑通并建立普通表单基线后 | 主控 PM | -## 15. 实施任务队列草案 +## 15. 实施任务队列 -以下任务默认为 `Backlog`,不因本文档创建而自动进入开发。 +当前可执行状态、owner、锁、依赖和验收口径只以 [M0 执行队列](./智能预约助手-M0执行队列-2026-08-02.md) 为准。其中 Batch 1–3 已具备开发边界;真实供应商和生产开启仍为 `Blocked`。 + +
+已被独立执行队列取代的历史草案(仅供审计,不执行) + +以下 BA-01~BA-07 是契约冻结前的原始 Backlog,不再表示当前任务状态。 ### BA-01 冻结智能预约产品与交互契约 @@ -707,12 +654,14 @@ Data Model Review Required: Yes Access / Privacy Review Required: Yes ``` +
+ ## 16. 开发与验证清单 实现时至少需要: 1. 更新 `docs/ontology/objects.md`、`actions.md`、`events.md`、`rules.md`、`relations.md` 和 `graph/ontology.jsonl`; -2. 后端覆盖模型无效输出、跨 customer/跨店、宠物归属、服务下线、号源冲突、重复确认和 session 过期; +2. 后端覆盖模型无效输出、跨 customer/跨店、宠物归属、服务下线、无号源、`draftVersion` 冲突、session 过期和「不存在助手写路径」; 3. 前端覆盖语音失败、模型失败、空输入、草稿修改、无号源、提交 loading 和普通表单降级; 4. 生产前检查供应商密钥、允许数据区域、超时、频控、日志脱敏和降级开关; 5. 以普通预约为基线,不用智能助手取代现有真实门店试点指标。 @@ -737,6 +686,6 @@ git -C /Users/apple/_src/petstore/docs diff --check | 门店服务 | 服务项目具有名称和 `durationMinutes` | 智能助手只从真实列表中匹配 | | 可约时段 | `/api/appointment/available-slots` 按时长和容量返回 | 可复用领域服务,不由模型计算 | | 预约创建 | `AppointmentService.createBooking` 复核宠物、服务、时间和容量 | M1 确认 endpoint 的唯一写入权威 | -| 业务事件 | `appointment_created` 已持久化 | 增加独立 `bookingChannel` 和助手漏斗事件 | +| 业务事件 | `appointment_created` 已持久化 | M0 只增加三个助手漏斗事件;`bookingChannel` 与直接创建归因留待 M1 评审 | 本方案是对现有预约入口的语义化扩展,不建立第二套预约、容量或身份系统。 diff --git a/智能预约助手-M0执行队列-2026-08-02.md b/智能预约助手-M0执行队列-2026-08-02.md new file mode 100644 index 0000000..2a70134 --- /dev/null +++ b/智能预约助手-M0执行队列-2026-08-02.md @@ -0,0 +1,181 @@ +# 智能预约助手 M0 执行队列 + +> 日期:2026-08-02
+> 状态:**Batch 0 contract frozen / coding not started**
+> RC Included:`No`
+> 当前优先级说明:不抢占 `phase2-pilot-rc6` 真实门店基线问题;按独立 feature flag 开发 + +**冻结输入** + +- [架构决策:M0 契约与数据模型](./架构决策-智能预约助手M0契约与数据模型-2026-08-02.md) +- [`booking-intent-v1` JSON Schema](./contracts/booking-intent-v1.schema.json) +- [M0 OpenAPI 契约](./contracts/booking-agent-m0.openapi.yaml) +- [M0 编码任务 brief](./智能预约助手-M0编码任务brief-2026-08-02.md) + +## 队列摘要 + +| Task Name | Owner Agent | Status | Depends On | +|---|---|---|---| +| 实现智能预约供应商适配与契约测试 | Backend Core | Ready | Batch 0 冻结资产 | +| 实现智能预约会话与只读预约编排 | Backend Core | Ready | Batch 0 冻结资产;provider interface | +| 实现宠主一句话预约与草稿回填 | Customer Experience FE | Ready | OpenAPI;可先用 mock | +| 验收智能预约 M0 草稿链路 | Core Flow QA | Ready | 可先写 fixture/smoke;执行依赖前三项 | +| 完成真实供应商对照与生产隐私复核 | Backend Ops | Blocked | 云账号、密钥、授权语音和上线授权 | + +## Task:实现智能预约供应商适配与契约测试 + +| 字段 | 值 | +|---|---| +| Task Name | 实现智能预约供应商适配与契约测试 | +| Role | `Backend Core` | +| Owner Agent | `Backend Core` | +| Paired QA | `Core Flow QA` | +| Workstream | `Core Booking Flow` | +| Track | `Coding` | +| Status | `Ready` | +| Priority | `P0` | +| Repo/Path | `backend/src/main/java/com/petstore/bookingagent/provider`、对应 config/test | +| Service Lock | `backend:booking-agent-provider` | +| Depends On | M0 ADR、JSON Schema、供应商选型 | +| Acceptance | provider interface + 本地 HTTP stub;30 条文字 fixture;无效 JSON/schema 降级;未配置密钥时应用正常启动 | +| RC Included | `No` | +| Due Date | `TBD` | +| Output Link | `TBD — implementation commit` | +| Blocker Owner | `None`(真实 provider smoke 不在本任务验收内) | +| Commit / RC | `Not Frozen` | +| Architecture Review Required | `Yes — completed by M0 ADR` | +| Data Model Review Required | `No` | +| Access / Privacy Review Required | `Yes — development boundary completed; live smoke pending` | + +额外门禁: + +- 使用 JDK `HttpClient` 和可注入 timeout; +- 测试不得访问公网; +- prompt、原文、Authorization、API Key 和模型原始响应不进日志; +- `booking-intent-v1` 未通过完整校验时不返回部分 patch。 + +## Task:实现智能预约会话与只读预约编排 + +| 字段 | 值 | +|---|---| +| Task Name | 实现智能预约会话与只读预约编排 | +| Role | `Backend Core` | +| Owner Agent | `Backend Core` | +| Paired QA | `Core Flow QA` | +| Workstream | `Core Booking Flow` | +| Track | `Coding` | +| Status | `Ready` | +| Priority | `P0` | +| Repo/Path | `backend/src/main/java/com/petstore/bookingagent`、`backend/db/migrations/20260802_create_booking_agent_session.sql` | +| Service Lock | `backend:booking-agent-session+appointment-read-tools` | +| Depends On | M0 ADR、OpenAPI、provider interface | +| Acceptance | 五个 endpoint;customer 归属;六态状态机;draftVersion;30 分钟 TTL/24 小时清理;只读宠物/服务/容量;三个低敏事件;不存在 `/confirm` | +| RC Included | `No` | +| Due Date | `TBD` | +| Output Link | `TBD — implementation commit` | +| Blocker Owner | `None` | +| Commit / RC | `Not Frozen` | +| Architecture Review Required | `Yes — completed by M0 ADR` | +| Data Model Review Required | `Yes — proposal completed; implementation diff requires Data Model review` | +| Access / Privacy Review Required | `Yes — development boundary completed` | + +额外门禁: + +- `storeId` 创建时必填,后续只从 session 读取; +- session 必须用 `sessionId + current.userId` 查询; +- 整套测试验证不调用 `AppointmentService.createBooking` / `AppointmentMapper.save`; +- 迁移包含索引、状态验证和 TTL 清理验证查询; +- 实现时同步把对应本体证据从 `documented` 更新为 `anchored`。 + +## Task:实现宠主一句话预约与草稿回填 + +| 字段 | 值 | +|---|---| +| Task Name | 实现宠主一句话预约与草稿回填 | +| Role | `Customer Experience Frontend` | +| Owner Agent | `Customer Experience FE` | +| Paired QA | `Core Flow QA` | +| Workstream | `Core Booking Flow` | +| Track | `Coding` | +| Status | `Ready` | +| Priority | `P0` | +| Repo/Path | `frontend/src/pages/appointment`、`frontend/src/api/index.js`、`frontend/src/utils/bookingAgentDraft.js`、`frontend/src/pages.json` | +| Service Lock | `frontend:customer-booking-agent+shared-api` | +| Depends On | M0 OpenAPI;后端未完成前允许契约 mock | +| Acceptance | 先选门店;customer 入口;文字链路;按住说话与隐私告知;转写可编辑;选择芯片;草稿卡;一次性回填;失败不影响普通表单 | +| RC Included | `No` | +| Due Date | `TBD` | +| Output Link | `TBD — implementation commit` | +| Blocker Owner | `None`(真机录音最终验收依赖测试设备) | +| Commit / RC | `Not Frozen` | +| Architecture Review Required | `Yes — completed by M0 ADR` | +| Data Model Review Required | `No` | +| Access / Privacy Review Required | `Yes — 语音首次告知与权限拒绝降级必须 review` | + +额外门禁: + +- 未选门店时先使用现有门店控件,不创建 agent session; +- 转写不得自动调用 messages; +- 对话消息不写 Storage,一次性 handoff 草稿消费后删除; +- `frontend/src/api/index.js` 写入前通知 Backend Core 并持有 `shared-api` 锁; +- H5 不具备录音能力时只隐藏语音按钮,文字主链路仍可用。 + +## Task:验收智能预约 M0 草稿链路 + +| 字段 | 值 | +|---|---| +| Task Name | 验收智能预约 M0 草稿链路 | +| Role | `Core Flow QA` | +| Owner Agent | `Core Flow QA` | +| Paired QA | `Backend Core` + `Customer Experience FE` | +| Workstream | `Core Booking Flow` | +| Track | `QA` | +| Status | `Ready` | +| Priority | `P0` | +| Repo/Path | `docs/qa-reports`、必要 smoke 脚本;不改业务代码 | +| Service Lock | `docs:qa-booking-agent-m0` | +| Depends On | 立即可写 fixture/smoke;执行依赖三个实现任务 | +| Acceptance | 权限/跨 customer;状态与版本;时间解析;真实号源;语音限制;日志脱敏;无写路径;普通预约回归;H5/mp-weixin 构建 | +| RC Included | `No` | +| Due Date | `TBD` | +| Output Link | `TBD — QA report` | +| Blocker Owner | `None` | +| Commit / RC | `Not Frozen` | +| Architecture Review Required | `No` | +| Data Model Review Required | `No` | +| Access / Privacy Review Required | `Yes — 验收日志与语音告知` | + +QA 必须提供反证: + +- 后端没有 `/api/booking-agent/sessions/{sessionId}/confirm`; +- M0 测试执行期间没有 `AppointmentMapper.save`; +- 他人/不存在 session 返回同一 `SESSION_NOT_FOUND`; +- provider 超时、无效 JSON、ASR 失败均能回到普通预约; +- 原始语音、当轮原文、模型响应和密钥不出现在库、日志、截图或 fixture。 + +## Task:完成真实供应商对照与生产隐私复核 + +| 字段 | 值 | +|---|---| +| Task Name | 完成真实供应商对照与生产隐私复核 | +| Role | `Backend Ops` | +| Owner Agent | `Backend Ops` | +| Paired QA | `Core Flow QA` | +| Workstream | `Release` | +| Track | `Review` | +| Status | `Blocked` | +| Priority | `P1` | +| Repo/Path | 本地密钥存储、低敏 smoke 证据、`docs/qa-reports`;不改业务逻辑 | +| Service Lock | `ops:booking-agent-provider-smoke` | +| Depends On | 阿里云北京 Workspace/API Key;20 条已授权语音;隐私告知;实现任务完成 | +| Acceptance | 30 条文字/20 条语音门槛;腾讯 ASR 对照;p50/p95;费用告警;日志审计;默认 flag 关闭 | +| RC Included | `No` | +| Due Date | `TBD` | +| Output Link | `TBD — provider evaluation report` | +| Blocker Owner | `Product Owner / Access & Privacy / Cloud Account Owner` | +| Commit / RC | `Not Frozen` | +| Architecture Review Required | `Yes — supplier result may change baseline` | +| Data Model Review Required | `No` | +| Access / Privacy Review Required | `Yes — pending` | + +阻塞条件不会阻塞前三项本地开发。真实 Workspace ID 和 API Key 只从密钥存储读取,不进入任务评论或 Git。 diff --git a/智能预约助手-M0编码任务brief-2026-08-02.md b/智能预约助手-M0编码任务brief-2026-08-02.md index 1492272..702cac4 100644 --- a/智能预约助手-M0编码任务brief-2026-08-02.md +++ b/智能预约助手-M0编码任务brief-2026-08-02.md @@ -1,32 +1,38 @@ # 智能预约助手 M0 编码任务 brief > 日期:2026-08-02
-> 状态:**Ready for Review / 尚未进入编码**
+> 状态:**Ready / Batch 0 契约已冻结,尚未进入编码**
> 目标:宠主用文字或语音生成可验证预约草稿,回填现有表单,不由智能层创建 `Appointment`
> Workstream:`Core Booking Flow`
> Owner Agent:`Backend Core` + `Customer Experience FE`
> Paired QA:`Core Flow QA`
> Service Lock:`backend:booking-agent-session+read-tools` + `frontend:customer-booking-agent+shared-api`
-> Data Model Review Required:`Yes`
-> Access / Privacy Review Required:`Yes`
+> Architecture Review Required:`Yes — completed by M0 ADR`
+> Data Model Review Required:`Yes — proposal completed; implementation diff requires review`
+> Access / Privacy Review Required:`Yes — development boundary completed; live provider/production pending`
> RC Included:`No` **需求来源** - [智能预约助手产品与技术方案 v0.1](./宠小它智能预约助手-产品与技术方案-v0.1.md) - [模型与语音供应商选型](./智能预约助手-模型与语音供应商选型-2026-08-02.md) +- [M0 契约与数据模型 ADR](./架构决策-智能预约助手M0契约与数据模型-2026-08-02.md) +- [`booking-intent-v1` JSON Schema](./contracts/booking-intent-v1.schema.json) +- [M0 OpenAPI 契约](./contracts/booking-agent-m0.openapi.yaml) +- [M0 执行队列](./智能预约助手-M0执行队列-2026-08-02.md) ## 1. M0 交付结果 -宠主在已登录状态进入「一句话预约」,输入文字或按住说话。系统依次: +宠主在普通预约页先选定门店,并在已登录状态进入「一句话预约」。系统依次: -1. 转写语音(文字输入跳过); -2. 提取宠物查询词、服务查询词、日期表达、时间偏好和备注; -3. 在当前 customer 和门店范围内解析真实 Pet 与 ServiceType; -4. 调用现有容量服务获取真实号源; -5. 展示可修改草稿卡; -6. 宠主点击「带入普通预约」后,回填现有 `CustAppointmentCreate`; -7. 宠主仍在现有表单上点击「提交预约」。 +1. 使用已选 `storeId` 创建会话; +2. 转写语音(文字输入跳过); +3. 提取宠物查询词、服务查询词、日期表达、时间偏好和备注; +4. 在当前 customer 和 session 门店范围内解析真实 Pet 与 ServiceType; +5. 调用现有容量服务获取真实号源; +6. 展示可修改草稿卡; +7. 宠主点击「带入普通预约」后,回填现有 `CustAppointmentCreate`; +8. 宠主仍在现有表单上点击「提交预约」。 M0 成功的判定是 **「草稿真实、回填正确、普通预约未受影响」**,不是对话内创建预约。 @@ -47,21 +53,24 @@ M0 成功的判定是 **「草稿真实、回填正确、普通预约未受影 ```mermaid flowchart TD A[宠主预约页] --> B[点击一句话预约] - B --> C{已登录 customer} - C -- 否 --> D[登录并保留返回路由] - D --> E[创建助手会话] + B --> C{已选有效门店} + C -- 否 --> D[使用现有控件选择门店] + D --> E{已登录 customer} C -- 是 --> E - E --> F[文字或语音输入] - F --> G[结构化意图提取] - G --> H[服务端解析真实宠物和服务] - H --> I[查询真实号源] - I --> J{草稿完整} - J -- 否 --> K[询问一个关键缺失项] - K --> F - J -- 是 --> L[展示草稿卡] - L --> M[带入普通预约] - M --> N[CustAppointmentCreate 回填] - N --> O[用户在现有表单提交] + E -- 否 --> F[登录并保留返回路由和门店] + F --> G[创建助手会话] + E -- 是 --> G + G --> H[文字或语音输入] + H --> I[结构化意图提取] + I --> J[服务端解析真实宠物和服务] + J --> K[查询真实号源] + K --> L{草稿完整} + L -- 否 --> M[询问一个关键缺失项] + M --> H + L -- 是 --> N[展示草稿卡] + N --> O[带入普通预约] + O --> P[CustAppointmentCreate 回填] + P --> Q[用户在现有表单提交] ``` ### 3.1 入口 @@ -72,6 +81,8 @@ M0 只在宠主的 `CustAppointmentCreate` 顶部增加一个次级入口: 该入口不取代当前表单和主提交按钮,不改宠主首页主 CTA。历史预约、报告页和宠物档案深链放到后续迭代。 +M0 不在对话内选择门店。未选门店时先使用现有表单控件;创建 session 后 `storeId` 固定,切店需结束会话并重新创建。 + ### 3.2 对话风格 - 回复简短,一次只问一个关键缺失项; @@ -159,22 +170,23 @@ M0 只允许: M0 字段: -- `session_id` VARCHAR(64) 主键; +- `id` BIGINT 自增主键; +- `session_id` VARCHAR(36) 唯一外部 UUID; - `customer_user_id` BIGINT NOT NULL; -- `store_id` BIGINT NULL; -- `status` VARCHAR(32) NOT NULL; +- `store_id` BIGINT NOT NULL; +- `status` VARCHAR(24) NOT NULL; - `draft_json` TEXT NOT NULL; - `draft_version` INT NOT NULL; - `entry_source` VARCHAR(32) NOT NULL; - `input_modality` VARCHAR(16) NULL; - `expires_at` DATETIME NOT NULL; -- `create_time` / `update_time`; -- `deleted` TINYINT(1) NOT NULL DEFAULT 0。 +- `create_time` / `update_time`。 索引: +- `UNIQUE (session_id)`; - `(customer_user_id, status, expires_at)`; -- `(expires_at, deleted)` 用于过期清理。 +- `(status, expires_at)` 用于过期和物理清理。 M0 不增加 `appointment_id`,也不建立 `t_booking_agent_turn`。`appointment_id` 唯一关联在 M1 幂等写入评审时再增加,避免 M0 数据模型暗示已具备直接创建能力。 @@ -182,6 +194,8 @@ M0 的 `entry_source` 只允许 `appointment_create`;历史预约、报告、 `draft_json` 只保存已验证 ID、时间约束和必要备注;不保存每轮用户原文、提示词或模型原始响应。 +TTL 固定为创建后 30 分钟,不滑动续期;过期 24 小时后物理删除。M0 不增加 `deleted`,详细迁移和清理口径以 ADR 为准。 + ### 4.5 API 契约 M0 只实现: @@ -190,12 +204,14 @@ M0 只实现: |---|---|---| | `POST` | `/api/booking-agent/sessions` | 创建已登录 customer 会话 | | `POST` | `/api/booking-agent/sessions/{sessionId}/messages` | 提交文字/语音转写文本,刷新草稿 | -| `POST` | `/api/booking-agent/transcriptions` | 上传短音频并获取可编辑转写 | +| `POST` | `/api/booking-agent/sessions/{sessionId}/transcriptions` | 在本人 session 下上传短音频并获取可编辑转写 | | `POST` | `/api/booking-agent/sessions/{sessionId}/fallback` | 返回可回填普通表单的已验证草稿 | | `DELETE` | `/api/booking-agent/sessions/{sessionId}` | 将本人会话标记为 `cancelled` | 所有 endpoint 都要求 `CurrentUserContext.require()` 且 role 为 `customer`。每次读写 session 都以 `sessionId + current.userId` 查询;他人 session 和不存在 session 均返回统一 404 语义,避免泄漏 session 是否存在。 +创建 session 时 `storeId` 必填并验证存在;之后不再接受客户端传门店。消息和 fallback 请求必须携带当前 `draftVersion`,版本不一致返回 `DRAFT_VERSION_CONFLICT`。 + M0 绝对不注册 `/confirm` endpoint。 ### 4.6 模型输出强校验 @@ -214,6 +230,7 @@ M0 绝对不注册 `/confirm` endpoint。 "end": null }, "remark": null, + "clearFields": [], "ambiguities": [], "nextAction": "resolve_context" } @@ -221,7 +238,7 @@ M0 绝对不注册 `/confirm` endpoint。 校验规则: -- 未知字段拒绝或明确忽略,不得进业务实体; +- 未知字段拒绝,不得进业务实体; - `schemaVersion`、`intent` 和 `nextAction` 为枚举; - 文本字段 trim、限长; - 模型输出 ID、价格、号源或预约状态字段时整个响应降级; @@ -299,7 +316,7 @@ frontend/src/pages.json # 路由 ## 6. 业务事件与本体 -M0 候选事件: +M0 已冻结事件: - `booking_agent_started`; - `booking_agent_draft_ready`; @@ -307,15 +324,17 @@ M0 候选事件: M0 不记录 `booking_agent_confirmed`,也不修改现有 `appointment_created` metadata,因为助手没有直接确认写入,而客户端的 `source` 字符串不能当作服务端归因事实。如后续需要精确联结「助手草稿 -> 表单创建」,另立 brief 设计可验证、一次性的草稿交接机制。 -编码前需增加或更新: +Batch 0 已按 `documented` 证据增加: -- `ontology/objects.md`:`booking_agent_session` gap/implemented 证据; +- `ontology/objects.md`:`booking_agent_session` 的 `documented` 模型条目; - `ontology/actions.md`:创建会话、提交消息、语音转写、回填草稿、结束会话; - `ontology/events.md`:M0 三个漏斗事件; - `ontology/rules.md`:customer 数据范围、无写工具、原始语音/原文不持久化、超时降级; - `ontology/relations.md` 与 `graph/ontology.jsonl`; - `coverage/ontology-coverage-audit.md`。 +实现时必须在同一批把真实代码/测试路径补入,并将对应条目从 `documented` 更新为 `anchored`;不得提前宣称已实现。 + ## 7. 自动化测试 ### 7.1 后端必测 @@ -329,6 +348,7 @@ M0 不记录 `booking_agent_confirmed`,也不修改现有 `appointment_created **会话和权限** - 未登录、boss/staff 调用助手 endpoint 被拒绝; +- 创建 session 缺少/伪造门店被拒绝,创建后不能跨店覆盖; - A customer 无法查询、修改或结束 B customer session; - session 过期后不再接收消息; - M0 不存在 `/confirm` endpoint; @@ -369,10 +389,12 @@ M0 不记录 `booking_agent_confirmed`,也不修改现有 `appointment_created Owner:System Architect + Data Model + Product Design +状态:**Completed — 2026-08-02** + - 冻结 `booking-intent-v1`、M0 状态子集、API shape 和业务码; - 更新本体及机器图谱; - 评审 session 表、TTL 和清理方式; -- 完成 Access / Privacy Review。 +- 完成开发态 Access / Privacy Review;真实 provider 与生产复核仍在 Batch 4。 ### Batch 1:供应商适配与契约测试 diff --git a/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md b/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md new file mode 100644 index 0000000..20c73d8 --- /dev/null +++ b/架构决策-智能预约助手M0契约与数据模型-2026-08-02.md @@ -0,0 +1,295 @@ +# 架构决策:智能预约助手 M0 契约与数据模型 + +> 日期:2026-08-02
+> 状态:**Accepted for M0 implementation / 尚未实现**
+> Workstream:`Core Booking Flow`
+> Track:`Architecture`
+> Priority:`P0`(独立试验流,不高于 RC6 真实门店基线)
+> Owner Agent:`System Architect`
+> Paired QA:`Core Flow QA`
+> Service Lock:`docs:booking-agent-m0-contract+ontology`
+> Architecture Review Required:`Yes — completed by this ADR`
+> Data Model Review Required:`Yes — completed for implementation proposal`
+> Access / Privacy Review Required:`Yes — development boundary accepted; live provider and production review pending`
+> RC Included:`No` + +**关联资产** + +- [产品与技术方案](./宠小它智能预约助手-产品与技术方案-v0.1.md) +- [M0 编码任务 brief](./智能预约助手-M0编码任务brief-2026-08-02.md) +- [M0 执行队列](./智能预约助手-M0执行队列-2026-08-02.md) +- [`booking-intent-v1` JSON Schema](./contracts/booking-intent-v1.schema.json) +- [M0 OpenAPI 契约](./contracts/booking-agent-m0.openapi.yaml) +- [模型与语音供应商选型](./智能预约助手-模型与语音供应商选型-2026-08-02.md) + +## 1. 决策结论 + +M0 冻结为 **「已登录 customer 在已选门店范围内,用文字或语音生成已验证预约草稿,再回填现有普通预约表单」**。 + +冻结边界: + +1. 门店不是模型槽位。宠主必须先在 `CustAppointmentCreate` 选定门店,创建会话时只提交 `storeId`,服务端验证门店存在。 +2. 模型只输出通过 `booking-intent-v1` 校验的非权威文本约束,不输出或决定业务 ID、价格、服务事实、号源或预约状态。 +3. 宠物、服务、日期时间和号源全部由 Petstore 后端在 `current.userId + session.storeId` 范围内重新解析。 +4. M0 没有 `/confirm` endpoint,不调用 `AppointmentService.createBooking`,不修改 `Appointment` 表或状态机。 +5. 「带入普通预约」只返回一次页面级回填数据;最终仍由现有 `/api/appointment/create` 提交。 +6. 原始语音、当轮用户原文、模型请求/响应和完整对话不落库、不进业务事件、不进日志。 +7. 功能开关默认关闭,不进入 `phase2-pilot-rc6`。 + +## 2. 为什么先选门店 + +原方案允许助手在对话内收集门店,但会同时引入门店名称歧义、地理推荐、跨店服务匹配和无门店 `BusinessEvent` 等问题。当前产品仍是单店 SaaS 试点,不需要把门店发现变成 Agent 能力。 + +M0 的次级入口仍位于 `CustAppointmentCreate`,行为调整为: + +1. 未登录:先登录并返回原预约页; +2. 未选门店:先使用现有门店选择控件; +3. 已选有效门店:创建智能会话; +4. 会话内只解析该门店的服务与容量。 + +这样可以让每个 session 和漏斗事件都有明确 `storeId`,并阻止模型扩大门店数据范围。 + +## 3. `booking-intent-v1` 冻结 + +模型输出以 [`contracts/booking-intent-v1.schema.json`](./contracts/booking-intent-v1.schema.json) 为唯一机器契约。 + +### 3.1 字段语义 + +| 字段 | 语义 | 是否业务权威 | +|---|---|---| +| `schemaVersion` | 固定 `booking-intent-v1` | 是,仅契约版本 | +| `intent` | `book / modify / end / fallback` | 否,状态机重新判定 | +| `petQuery` | 宠物展示名搜索词 | 否 | +| `serviceQuery` | 服务名称/同义表达搜索词 | 否 | +| `dateExpression` | 用户日期表达 | 否 | +| `timeWindow` | `HH:mm` 起止偏好 | 否 | +| `remark` | 用户希望带入预约的短备注 | 否,可编辑且限 200 字 | +| `clearFields` | 用户明确要求清空的草稿字段 | 否,服务端只允许白名单字段 | +| `ambiguities` | 模型观察到的歧义类型 | 否,服务端重新确认 | +| `nextAction` | 下一步建议 | 否,编排器可忽略 | + +### 3.2 强校验顺序 + +1. 响应体必须是单个 JSON object,不接受 Markdown fence 或前后说明文字; +2. JSON Schema 校验通过,未知字段拒绝; +3. 文本 trim、控制字符过滤和长度校验; +4. 输出出现任意 ID、价格、手机号、号源或预约状态字段时整个响应降级; +5. 服务端时间解析、归属解析和号源查询成功后才更新 `BookingDraft`; +6. 失败不尝试从残缺 JSON 猜字段,返回 `AGENT_UNAVAILABLE` 并保留旧草稿。 + +### 3.3 时间口径 + +- 时区固定 `Asia/Shanghai`; +- 支持绝对日期、今天/明天/后天、本周 X/下周 X、精确时间、上午/下午、X 点以前/以后; +- 「过几天」「最近」「周末都行」等表达不得猜成绝对日期,必须返回日期选择项; +- `appointmentTime` 只有在现有容量服务返回真实 slot 后才能写入草稿; +- 对外时间字符串统一为 `yyyy-MM-dd'T'HH:mm:ss`,语义为 `Asia/Shanghai` 本地时间。 + +## 4. M0 会话状态机 + +```mermaid +stateDiagram-v2 + [*] --> collecting: create session + collecting --> collecting: 信息仍不足 + collecting --> proposing: 可搜索真实号源 + proposing --> collecting: 修改条件或需要澄清 + proposing --> confirmable: 选择真实 slot + confirmable --> collecting: 用户修改草稿 + collecting --> fallback: 用户回填表单或 LLM 降级 + proposing --> fallback: 用户回填表单或 LLM 降级 + confirmable --> fallback: 带入普通预约 + collecting --> cancelled: 用户结束 + proposing --> cancelled: 用户结束 + confirmable --> cancelled: 用户结束 + collecting --> expired: TTL 到期 + proposing --> expired: TTL 到期 + confirmable --> expired: TTL 到期 +``` + +不允许的状态:`submitting`、`booked`、`needs_reselection`。终态 `fallback / expired / cancelled` 不接受新消息或转写。 + +每次消息请求必须带客户端当前 `draftVersion`。服务端只在版本相等时更新,成功后加一;版本冲突返回 `DRAFT_VERSION_CONFLICT`,不得静默覆盖。 + +## 5. API 冻结 + +唯一契约是 [`contracts/booking-agent-m0.openapi.yaml`](./contracts/booking-agent-m0.openapi.yaml)。M0 只注册五个 endpoint: + +| Method | Endpoint | 结果 | +|---|---|---| +| `POST` | `/api/booking-agent/sessions` | 在已选 `storeId` 下创建 customer 会话 | +| `POST` | `/api/booking-agent/sessions/{sessionId}/messages` | 提交当轮已确认文字并刷新草稿 | +| `POST` | `/api/booking-agent/sessions/{sessionId}/transcriptions` | 转写短音频,文字返回输入框,不自动发送 | +| `POST` | `/api/booking-agent/sessions/{sessionId}/fallback` | 返回已验证的部分/完整草稿并结束会话 | +| `DELETE` | `/api/booking-agent/sessions/{sessionId}` | 结束本人会话 | + +### 5.1 身份与资源隐藏 + +- 所有 endpoint 都必须经过 `AuthInterceptor`; +- controller 再要求 `current.role == customer`,boss/staff 返回 `FORBIDDEN`; +- session 每次以 `sessionId + current.userId` 查询;不存在和他人 session 统一返回 `SESSION_NOT_FOUND`; +- `storeId` 只在创建时接收并验证,后续从 session 读取; +- 音频转写 endpoint 必须挂在 session 下,客户端不得上传宠物/服务词表。 + +### 5.2 响应语义 + +沿用 `{ code, message?, bizCode?, data? }`。当前前端以 body `code` 判断业务结果;除 `AuthInterceptor` 的 HTTP 401 外,M0 不借机重构全局 HTTP status 行为。 + +草稿中的 `storeId / petId / serviceTypeId / appointmentTime` 是经过后端重新解析的事实;客户端回填后,现有预约创建服务仍会重新执行最终权限、服务和容量校验。 + +### 5.3 业务码 + +| body code | bizCode | 语义 | +|---|---|---| +| 401 | `UNAUTHENTICATED` | session token 缺失或失效 | +| 403 | `FORBIDDEN` | 非 customer 调用 | +| 503 | `AGENT_DISABLED` | 功能开关关闭 | +| 404 | `STORE_NOT_FOUND` | 创建时目标门店无效 | +| 404 | `SESSION_NOT_FOUND` | session 不存在或不属于当前 customer | +| 410 | `SESSION_EXPIRED` | 会话 TTL 到期 | +| 409 | `SESSION_TERMINAL` | 会话已 fallback/cancelled | +| 409 | `DRAFT_VERSION_CONFLICT` | 客户端草稿版本过旧 | +| 400 | `INVALID_INPUT` | 空文本、超长或请求结构无效 | +| 400 | `INVALID_AUDIO` | 空音频、格式/MIME/文件头不匹配或时长非法 | +| 413 | `AUDIO_TOO_LARGE` | 超过 3 MB | +| 429 | `RATE_LIMITED` | customer 请求频率超限 | +| 503 | `AGENT_UNAVAILABLE` | LLM 超时、无效 JSON/schema 或外部失败 | +| 503 | `ASR_UNAVAILABLE` | ASR 超时或外部失败 | + +宠物/服务歧义、无号源和未选时段是正常 `200` 会话响应,不用异常码表达。 + +## 6. 数据模型冻结 + +计划迁移:`backend/db/migrations/20260802_create_booking_agent_session.sql`。Batch 0 只冻结设计,不在 docs 任务内创建迁移。 + +### 6.1 `t_booking_agent_session` + +| 字段 | 类型 | 规则 | +|---|---|---| +| `id` | BIGINT | 自增主键,只在后端和 `BusinessEvent.aggregate_id` 使用 | +| `session_id` | VARCHAR(36) | UUID,唯一,对客户端暴露 | +| `customer_user_id` | BIGINT | 必须是创建会话的 customer | +| `store_id` | BIGINT | M0 必填;创建时验证存在,后续不接受覆盖 | +| `status` | VARCHAR(24) | 仅六个 M0 状态 | +| `draft_json` | TEXT | 最小已验证草稿,不存当轮原文/模型响应 | +| `draft_version` | INT | 初始 0,每次成功草稿更新加一 | +| `entry_source` | VARCHAR(32) | M0 固定 `appointment_create` | +| `input_modality` | VARCHAR(16) | `text / voice / mixed`,首次成功消息后派生 | +| `expires_at` | DATETIME | 创建时固定 `now + 30min`,M0 不滑动续期 | +| `create_time / update_time` | DATETIME | 服务端时间 | + +索引: + +- `UNIQUE uk_booking_agent_session_public_id (session_id)`; +- `INDEX idx_booking_agent_customer_status_expire (customer_user_id, status, expires_at)`; +- `INDEX idx_booking_agent_status_expire (status, expires_at)`。 + +不增加: + +- `appointment_id`; +- `t_booking_agent_turn`; +- 原始语音/转写/提示词/模型响应列; +- `deleted`。会话是短期对象,使用明确终态并按 TTL 物理清理。 + +### 6.2 TTL 与清理 + +1. 会话固定 30 分钟有效,不因消息滑动延长; +2. endpoint 发现过期时返回 `SESSION_EXPIRED`,并以幂等方式标记 `expired`; +3. 每小时清理任务将到期活动会话标记 `expired`; +4. `expires_at` 早于当前时间 24 小时的 session 物理删除; +5. 清理失败记录数量和错误类别,不记录 `draft_json`; +6. `BusinessEvent` 是独立低敏事实,不随短期 session 删除。 + +## 7. 业务事件冻结 + +M0 只新增三个首次漏斗事实: + +| 事件 | 触发 | 幂等键 | metadata 白名单 | +|---|---|---|---| +| `booking_agent_started` | session 创建成功 | `booking_agent_started:{sessionDbId}` | `entrySource` | +| `booking_agent_draft_ready` | session 首次进入 `confirmable` | `booking_agent_draft_ready:{sessionDbId}` | `inputModality` | +| `booking_agent_fallback` | 用户回填表单或 LLM 降级 | `booking_agent_fallback:{sessionDbId}` | `reason=user/llm_unavailable` | + +事件使用: + +- `storeId=session.storeId`; +- `aggregateType=booking_agent_session`; +- `aggregateId=session.id`; +- `actorUserId=session.customerUserId`、`actorRole=customer`、`source=customer`; +- 已有 `StoreCustomer` 时可关联,不能只为启动助手创建客户主档。 + +禁止把输入原文、转写文本、备注、宠物名、服务名、供应商 request ID 或错误原文写入 metadata。 + +M0 不写 `booking_agent_confirmed`,也不修改现有 `appointment_created` metadata。精确联结「助手草稿 -> 表单创建」需要另行设计服务端一次性交接机制。 + +## 8. 外部调用与隐私门禁 + +### 8.1 语音 + +- 单段不超过 60 秒、3 MB;客户端预检,服务端按字节、文件头、MIME 和解析时长复核; +- ASR 使用 `qwen3-asr-flash-2026-02-10` Base64 Data URL,不中转 OSS; +- 上下文只包含当前门店服务名和当前 customer 的宠物展示名; +- 返回转写后立即释放音频字节,不缓存、不写文件、不返回供应商 request ID; +- 转写必须由用户编辑/确认后才能调用 messages。 + +### 8.2 LLM + +- 使用固定 `qwen-plus-2025-12-01`、非思考、非流式、无工具、无联网; +- LLM 超时 3 秒,ASR 超时 5 秒;不做多次自动重试; +- 请求只包含当次文字、最小草稿和枚举说明,不包含手机号、token、客户时间线或其他门店数据; +- API Key、Workspace ID 和 Authorization header 不进 Git、测试 fixture、聊天、截图或日志。 + +### 8.3 频率门禁 + +M0 默认每个 customer: + +- 10 分钟内最多创建 5 个 session; +- 10 分钟内最多提交 30 条 message; +- 10 分钟内最多转写 10 段音频。 + +超限返回 `RATE_LIMITED`,普通预约继续可用。当前单实例可先使用进程内限流;进入多实例前必须切换共享限流状态。 + +## 9. Review 结论 + +### Architecture Review + +**通过,可进入实现。** 条件是严格保留五个 endpoint、无 `/confirm`、门店先选、普通表单独立可用、feature flag 默认关闭。 + +### Data Model Review + +**通过提案,可由 Backend Core 实现迁移。** 相比原 brief 的修订: + +- 使用 `id BIGINT` 内部主键 + `session_id UUID` 外部唯一键; +- `store_id` 从可空改为 M0 必填; +- 去掉 `deleted` 和 `appointment_id`; +- 增加固定 TTL、物理清理和 `draft_version` 并发规则。 + +### Access / Privacy Review + +**开发边界通过,真实供应商 smoke 与生产开启未通过。** 不需要真实密钥即可实现接口、stub、状态机和测试;真实调用前仍需完成供应商数据处理确认、小程序隐私告知、费用告警和已知情语音样本评测。 + +## 10. 后果与风险 + +正向结果: + +- M0 不增加第二条预约写路径,模型失败不会污染 `Appointment`; +- session 可短期恢复结构化草稿,又不形成对话原文数据库; +- OpenAPI、JSON Schema 和本体可以直接驱动测试与 review; +- 后续 M1 必须另立 ADR 才能新增 confirm/appointment 关联。 + +已知限制: + +- 门店必须先选,无法用一句话同时完成「找店 + 预约」; +- M0 不精确统计回填后最终创建转化; +- 进程内限流只适用于当前单实例; +- 供应商效果仍需真实语料验证,当前推荐不是生产准入结论。 + +## 11. 实现门禁 + +编码 PR 必须同时满足: + +- 引用本 ADR、OpenAPI、JSON Schema 和对应 `ontologyRefs`; +- provider 单测只访问本地 HTTP stub; +- 后端测试证明整个 M0 不调用 `AppointmentMapper.save`; +- 未配置密钥或 feature flag 关闭时应用正常启动、普通预约正常; +- 本体从 `documented` 更新为 `anchored` 只能与代码和测试同一批完成; +- 真实供应商 smoke、生产配置和 RC 纳入必须重新授权。