docs: freeze booking agent m0 contract

This commit is contained in:
malei 2026-08-02 15:23:55 +08:00
parent 031d2db36e
commit d0da10b35b
14 changed files with 1542 additions and 197 deletions

View File

@ -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 MBMIME 与真实文件头必须一致
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

View File

@ -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"
]
}
}
}

View File

@ -3,7 +3,7 @@
> 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。 > 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。
> 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。 > 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。
> >
> **刷新时间**2026-08-02FollowUpTask 回访闭环收口后刷新) > **刷新时间**2026-08-02智能预约 M0 Batch 0 契约冻结后刷新)
## 审计方法 ## 审计方法
@ -18,16 +18,16 @@
| 类型 | 数量 | | 类型 | 数量 |
|------|------| |------|------|
| Entity | 29 | | Entity | 30 |
| Action | 75 | | Action | 80 |
| Event | 33 | | Event | 36 |
| Rule | 49 | | Rule | 55 |
| Relation | 102 | | Relation | 112 |
| **合计** | **288** | | **合计** | **313** |
证据强度:`anchored` 24986.5%/ `documented` 28 / `gap` 9 / `implemented` 2。 证据强度:`anchored` 25079.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 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 | | customer_timeline | ✅ | ✅ read model | StoreCustomerTimelineServiceTest | anchored | StoreCustomer + BusinessEvent 的同店低敏事实投影jpa:false |
| highlight_fail_reason | ✅ | ✅ 枚举 | ReportHighlightVideoServiceTest | anchored | jpa:false | | highlight_fail_reason | ✅ | ✅ 枚举 | ReportHighlightVideoServiceTest | anchored | jpa:false |
| current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false | | current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false |
| booking_agent_session | ✅ | —Batch 0 设计) | — | documented | M0 短期会话;实现批创建 JPA/迁移/测试后才可改 anchored |
| membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 | | membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 |
## 规则覆盖度(49 条) ## 规则覆盖度(55 条)
| 规则 | 测试 | 证据 | | 规则 | 测试 | 证据 |
|------|------|------| |------|------|------|
@ -101,6 +102,7 @@ python3 docs/graph/audit_drift.py
| BR-AUDIT-001 | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored | | BR-AUDIT-001 | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored |
| BR-SC-002 客户时间线低敏投影 | StoreCustomerTimelineServiceTest / AdminServiceCustomerControllerTest | anchored | | BR-SC-002 客户时间线低敏投影 | StoreCustomerTimelineServiceTest / AdminServiceCustomerControllerTest | anchored |
| BR-FU-001/002 回访状态机与真实再次预约 | FollowUpTaskServiceTest / AppointmentServiceTest / MigrationTest | anchored | | BR-FU-001/002 回访状态机与真实再次预约 | FollowUpTaskServiceTest / AppointmentServiceTest / MigrationTest | anchored |
| BR-BA-001..005 智能预约 M0 边界 | 实现前ADR + OpenAPI + JSON Schema 已冻结 | documented |
## 待补 gap 清单 ## 待补 gap 清单
@ -118,6 +120,7 @@ python3 docs/graph/audit_drift.py
| 低 | 成片真 FFmpeg 端到端集成测(本机有 ffmpeg | 🟡 单测已覆盖状态机与归类 | | 低 | 成片真 FFmpeg 端到端集成测(本机有 ffmpeg | 🟡 单测已覆盖状态机与归类 |
| 低 | `action:logout` / `event:session_revoked` 服务端吊销 | 🟡 gap | | 低 | `action:logout` / `event:session_revoked` 服务端吊销 | 🟡 gap |
| 低 | 废弃 `register_store` / wechat OAuth demo 代码路径 | 🟡 deprecated 已标 | | 低 | 废弃 `register_store` / wechat OAuth demo 代码路径 | 🟡 deprecated 已标 |
| 高 | 智能预约 M0 会话、五个 endpoint、三个事件和五条规则实现证据 | 🟡 documented / Ready代码+测试同批才转 anchored |
| — | 会员/储值/套餐/历史报告/看板 | gap 占位,不展开 | | — | 会员/储值/套餐/历史报告/看板 | gap 占位,不展开 |
## P1/P2 方向jsonl 已占位) ## P1/P2 方向jsonl 已占位)
@ -144,4 +147,16 @@ python3 docs/graph/audit_drift.py
**页面清单 PRD已写**`docs/门店管理后台-PhaseA-页面清单PRD.md`A0A7 全页字段/筛选/API/验收)。 **页面清单 PRD已写**`docs/门店管理后台-PhaseA-页面清单PRD.md`A0A7 全页字段/筛选/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` 文字。

View File

@ -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: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_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":"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"}

View File

@ -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`,以及成片/上传/鉴权/寄语/退订/回访状态机/真实再次预约相关规则。 已覆盖 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。 **门店 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 设计。 P1/P2 方向(会员/储值/套餐/历史报告/看板)以 `gap` 证据条目录入,不展开字段;受 `rule:BR-ADMIN-004` 约束不得偷渡进 Phase A 设计。

View File

@ -715,3 +715,52 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_
- **入口**Admin UI `/schedule``GET /api/schedule/day``POST/DELETE /api/schedule/block` - **入口**Admin UI `/schedule``GET /api/schedule/day``POST/DELETE /api/schedule/block`
- **适用规则**`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-SCH-001` - **适用规则**`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-SCH-001`
- **证据**`anchored` - **证据**`anchored`
## 智能预约助手 M0Batch 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`

View File

@ -337,3 +337,35 @@
- **触发动作**`action:view_workbench` - **触发动作**`action:view_workbench`
- **payload**`storeId`、`userId`、`ts` - **payload**`storeId`、`userId`、`ts`
- **证据**`gap`A 期可不实现上报) - **证据**`gap`A 期可不实现上报)
## 智能预约助手 M0Batch 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`

View File

@ -540,3 +540,30 @@ API 动态聚合字段:`pets`、`lastVisitAt`、`lastReportId`、`leadStatus`
- `entity:analytics_dashboard`(经营分析看板)— B 期A 期仅工作台轻量指标 - `entity:analytics_dashboard`(经营分析看板)— B 期A 期仅工作台轻量指标
后续如进入实现,再补完整字段、动作与规则。 后续如进入实现,再补完整字段、动作与规则。
---
## 智能预约助手 M0Batch 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`

View File

@ -187,3 +187,18 @@
| `business_event` PROJECTS_TO `customer_timeline` | 多条不可变事实经低敏白名单投影为时间线 | | `business_event` PROJECTS_TO `customer_timeline` | 多条不可变事实经低敏白名单投影为时间线 |
| `view_store_customer_timeline` READS `store_customer` / `business_event` | 同店校验后分页读取主档与事实 | | `view_store_customer_timeline` READS `store_customer` / `business_event` | 同店校验后分页读取主档与事实 |
| `admin_list_leads` READS `follow_up_task` | `/leads` 导航承载真实操作任务池,不直接编辑留资事实 | | `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 降级的首次事实 |

View File

@ -479,3 +479,40 @@
- **代码**`backend/src/main/java/com/petstore/service/BusinessEventService.java`、`AdminBusinessEventService.java` - **代码**`backend/src/main/java/com/petstore/service/BusinessEventService.java`、`AdminBusinessEventService.java`
- **测试**`BusinessEventServiceTest`、`BusinessEventMigrationTest`、`AdminBusinessEventControllerTest` - **测试**`BusinessEventServiceTest`、`BusinessEventMigrationTest`、`AdminBusinessEventControllerTest`
- **证据**`anchored` - **证据**`anchored`
## 智能预约助手 M0Batch 0 · documented
### rule:BR-BA-001
**customer 与门店数据边界**:所有 endpoint 只允许已登录 customersession 每次以 `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`

View File

@ -1,7 +1,7 @@
# 宠小它智能预约助手:产品与技术方案 v0.1 # 宠小它智能预约助手:产品与技术方案 v0.1
> 日期2026-08-02<br> > 日期2026-08-02<br>
> 状态:**Draft / 待产品、架构、数据与隐私评审;尚未实现**<br> > 状态:**M0 产品/架构/数据契约已冻结;尚未实现;真实供应商与生产隐私评审未完成**<br>
> 目标用户:`customer` 宠主<br> > 目标用户:`customer` 宠主<br>
> Workstream`Core Booking Flow`<br> > Workstream`Core Booking Flow`<br>
> 建议 Owner`Product Design` + `Customer Experience FE` + `Backend Core`<br> > 建议 Owner`Product Design` + `Customer Experience FE` + `Backend Core`<br>
@ -15,12 +15,16 @@
- [模型与语音供应商选型](./智能预约助手-模型与语音供应商选型-2026-08-02.md) - [模型与语音供应商选型](./智能预约助手-模型与语音供应商选型-2026-08-02.md)
- [M0 编码任务 brief](./智能预约助手-M0编码任务brief-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. 结论 ## 1. 结论
建议将该能力定义为 **「可执行的智能预约助手」**,而不是泛化客服聊天机器。 建议将该能力定义为 **「可执行的智能预约助手」**,而不是泛化客服聊天机器。
它负责把宠主的自然语言转换为结构化预约草稿,通过现有 Petstore 预约域查询真实宠物、门店服务和可约时段,最后由用户明确确认后才创建 `Appointment` 它负责把宠主的自然语言转换为结构化预约草稿,通过现有 Petstore 预约域查询真实宠物、门店服务和可约时段。M0 只将草稿带入现有表单,仍由用户在普通表单中提交;对话内直接创建 `Appointment` 是 M1 候选能力
**核心原则模型负责「听懂人话」Petstore 预约域负责「决定什么能约」。** **核心原则模型负责「听懂人话」Petstore 预约域负责「决定什么能约」。**
@ -38,6 +42,8 @@
### 2.2 入口优先级 ### 2.2 入口优先级
下列为长期入口价值排序。M0 已冻结为仅从 `CustAppointmentCreate` 顶部次级入口进入,且必须先用现有控件选定门店。
1. **服务报告或历史预约:「跟助手约下次」** 1. **服务报告或历史预约:「跟助手约下次」**
上下文最完整,可在登录与归属校验后预填门店、宠物和上次服务。 上下文最完整,可在登录与归属校验后预填门店、宠物和上次服务。
2. **宠主首页:「一句话预约」** 2. **宠主首页:「一句话预约」**
@ -108,6 +114,7 @@
- 普通预约仍保持「提交时登录 + guest 草稿恢复」的现有口径。 - 普通预约仍保持「提交时登录 + guest 草稿恢复」的现有口径。
- 智能助手 MVP 在创建服务端会话前要求 customer 登录,因为它需要读取归属宠物和历史事实。 - 智能助手 MVP 在创建服务端会话前要求 customer 登录,因为它需要读取归属宠物和历史事实。
- M0 在创建会话前还要求已选定有效门店对话不负责选店session 创建后不允许切店。
- 客户端可在登录前保存入口来源和非敏感意图,但不得在未登录状态调用宠物或历史数据工具。 - 客户端可在登录前保存入口来源和非敏感意图,但不得在未登录状态调用宠物或历史数据工具。
- 会话过期后可将已解析的非敏感草稿回填到普通表单,不能自动续约或提交。 - 会话过期后可将已解析的非敏感草稿回填到普通表单,不能自动续约或提交。
@ -161,8 +168,10 @@ flowchart LR
H --> K[确认卡] H --> K[确认卡]
I --> K I --> K
J --> K J --> K
K --> L[用户明确确认] K --> L[M0 带入普通预约]
L --> M[AppointmentService 最终复核与创建] L --> M[CustAppointmentCreate 回填]
M --> N[现有预约域复核与创建]
K -. M1 另立 ADR 后 .-> O[对话内受控确认]
``` ```
### 6.1 模型可以做的事 ### 6.1 模型可以做的事
@ -171,24 +180,24 @@ flowchart LR
- 提取非权威文本约束:宠物称呼、服务需求、相对日期、时间偏好、备注; - 提取非权威文本约束:宠物称呼、服务需求、相对日期、时间偏好、备注;
- 根据状态机的下一动作生成简短、可理解的回复。 - 根据状态机的下一动作生成简短、可理解的回复。
建议将模型输出限制为 JSON Schema 模型输出必须通过冻结的 [`booking-intent-v1`](./contracts/booking-intent-v1.schema.json) JSON Schema
```json ```json
{ {
"intent": "book | modify | end | fallback", "schemaVersion": "booking-intent-v1",
"draftPatch": { "intent": "book",
"petQuery": "球球", "petQuery": "球球",
"serviceQuery": "洗澡", "serviceQuery": "洗澡",
"dateExpression": "本周六", "dateExpression": "本周六",
"timeWindow": { "start": "15:00", "end": null }, "timeWindow": { "start": "15:00", "end": null },
"remark": null "remark": null,
}, "clearFields": [],
"ambiguities": [], "ambiguities": [],
"nextAction": "ask | resolve_context | search_slots | show_confirmation | fallback" "nextAction": "resolve_context"
} }
``` ```
`petQuery``serviceQuery` 只是搜索文本,不是业务 ID。所有 ID 必须由后端在当前登录上下文和门店数据范围中解析。 `petQuery``serviceQuery` 只是搜索文本,不是业务 ID。所有 ID 必须由后端在当前登录上下文和门店数据范围中解析;未知字段和越界枚举直接拒绝,不从残缺输出猜测
### 6.2 模型不可以做的事 ### 6.2 模型不可以做的事
@ -209,7 +218,7 @@ flowchart LR
| `search_available_slots` | 只读 | 开始/结束时间、可约性 | 换日期或切换表单 | | `search_available_slots` | 只读 | 开始/结束时间、可约性 | 换日期或切换表单 |
| `resolve_previous_booking` | 只读 | 归属校验后的门店/宠物/服务 ID | 忽略历史预填并继续收集 | | `resolve_previous_booking` | 只读 | 归属校验后的门店/宠物/服务 ID | 忽略历史预填并继续收集 |
白名单中不包含写工具。最终创建由确认 endpoint 在模型调用之外完成 白名单中不包含写工具。M0 由普通表单在助手之外完成最终创建M1 如增加确认 endpoint也必须在模型调用之外进入预约域
## 7. 预约草稿与数据模型提案 ## 7. 预约草稿与数据模型提案
@ -225,43 +234,41 @@ flowchart LR
| `timeWindow` | 模型提取 | 否 | 只是偏好约束 | | `timeWindow` | 模型提取 | 否 | 只是偏好约束 |
| `appointmentTime` | 真实号源选择 | 是 | 必须是精确的未来时间 | | `appointmentTime` | 真实号源选择 | 是 | 必须是精确的未来时间 |
| `remark` | 宠主原意摘要 | 否 | 限长、可编辑,不生成医疗结论 | | `remark` | 宠主原意摘要 | 否 | 限长、可编辑,不生成医疗结论 |
| `sourceAppointmentId` | 已验证复约入口 | 是 | 可空,必须归属当前 customer |
| `draftVersion` | 服务端 | 是 | 每次草稿变更递增,确认时防旧版提交 | | `draftVersion` | 服务端 | 是 | 每次草稿变更递增,确认时防旧版提交 |
确认时不直接信任草稿中的展示快照。后端需重新加载 Pet、ServiceType、Store 和容量事实,再由现有预约域生成 `petName`、`petType`、`serviceType`和 `durationMinutes` 快照。 确认时不直接信任草稿中的展示快照。后端需重新加载 Pet、ServiceType、Store 和容量事实,再由现有预约域生成 `petName`、`petType`、`serviceType`和 `durationMinutes` 快照。
### 7.2 候选会话表 ### 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` | 从登录上下文派生 | | `customer_user_id` | 从登录上下文派生 |
| `store_id` | 会话数据范围 | | `store_id` | NOT NULL会话固定数据范围 |
| `source_appointment_id` | 复约上下文,可空 | | `status` | M0 六个状态之一 |
| `status` | 仅使用第 5 节对话状态 |
| `draft_json` | 最小结构化草稿,不存原始语音 | | `draft_json` | 最小结构化草稿,不存原始语音 |
| `draft_version` | 乐观版本号 | | `draft_version` | 乐观版本号 |
| `entry_source` | `appointment_create` / `home` / `appointment_history` / `report_history` / `pet_profile` | | `entry_source` | M0 固定 `appointment_create` |
| `input_modality` | `text` / `voice` / `mixed` | | `input_modality` | `text` / `voice` / `mixed` |
| `appointment_id` | M1 成功后关联的真实预约可空M0 不增加 | | `expires_at` | 创建后 30 分钟,不滑动续期 |
| `expires_at` | 会话过期时间 |
| `create_time` / `update_time` | 审计时间 | | `create_time` / `update_time` | 审计时间 |
数据建议: 数据建议:
- 以 `(customer_user_id, status, expires_at)` 建立查询索引; - 以 `(customer_user_id, status, expires_at)` 建立查询索引;
- M1 增加 `appointment_id` 可空唯一关联 - `(status, expires_at)` 支持过期标记和清理
- M1 确认时对 session 行加悲观锁;如已是 `booked`,重复请求直接返回同一 `Appointment` - 会话过期 24 小时后物理删除M0 不增加 `deleted`
- MVP 会话默认 30 分钟过期,过期不等于删除预约 - M0 不增加 `source_appointment_id``appointment_id`
- 不新增 `t_booking_agent_turn` 原文表;模型每轮以当前草稿和当次输入工作,避免默认持久化完整对话。 - 不新增 `t_booking_agent_turn` 原文表;模型每轮以当前草稿和当次输入工作,避免默认持久化完整对话。
该表、索引、过期清理和迁移顺序均是 **待 Data Model 评审的提案**,不代表当前生产库已存在 上述是 **M0 已评审契约**,不代表当前生产库已存在;迁移实现 diff 仍必须经 Data Model Review
### 7.3 预约来源与事件 ### 7.3 预约来源与事件
不改变现有 `BusinessEvent.source=customer/admin`的操作人语义。建议在 `appointment_created``metadata_json` 增加独立维度: 不改变现有 `BusinessEvent.source=customer/admin`的操作人语义。长期可在 `appointment_created``metadata_json` 评审独立维度:
```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` 主表。 `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_started`
- `booking_agent_draft_ready` - `booking_agent_draft_ready`
- `booking_agent_fallback` - `booking_agent_fallback`
- `booking_agent_confirmed`M1
这些事件只用于漏斗和运行分析;只有真实创建成功的 `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` | Method | Endpoint | M0 语义 |
```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": "<opaque-session-id>",
"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 确认预约M1M0 不实现)
`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` | 用户语义 | 前端处理 |
|---|---|---| |---|---|---|
| `AGENT_SESSION_NOT_FOUND` | 会话不存在或不属于当前宠主 | 返回预约入口 | | `POST` | `/api/booking-agent/sessions` | 以已选 `storeId` 创建 customer 会话;`entrySource` 由服务端固定 |
| `AGENT_SESSION_EXPIRED` | 会话已过期 | 新建会话或切表单 | | `POST` | `/api/booking-agent/sessions/{sessionId}/messages` | 携带 `draftVersion`、`inputType` 和文本更新草稿 |
| `AGENT_INPUT_INVALID` | 输入为空或超限 | 允许重新输入 | | `POST` | `/api/booking-agent/sessions/{sessionId}/transcriptions` | 在本人 session 下转写不超过 60 秒/3 MB 的短音频,仅返回可编辑文本 |
| `AGENT_UNAVAILABLE` | 模型超时或输出校验失败 | 保留草稿并切表单 | | `POST` | `/api/booking-agent/sessions/{sessionId}/fallback` | 携带 `draftVersion`,返回服务端验证的一次性回填草稿 |
| `ASR_UNAVAILABLE` | 语音转写失败 | 重试或改用文字 | | `DELETE` | `/api/booking-agent/sessions/{sessionId}` | 将本人非终态会话标记为 `cancelled` |
| `DRAFT_VERSION_CONFLICT` | 确认的不是最新草稿 | 刷新确认卡 |
| `PET_CUSTOMER_MISMATCH` | 宠物归属校验失败 | 重新选宠物 | 所有 endpoint 只允许已登录 `customer`session 每次用 `sessionId + current.userId` 查询,他人 session 和不存在 session 统一为 `SESSION_NOT_FOUND`。创建后 `storeId` 固定,消息和 fallback 必须进行草稿版本检查。
| `SERVICE_TYPE_INVALID` | 服务不属于门店或已下线 | 重新选服务 |
| `CAPACITY_FULL` | 确认时号源已变化 | 立即查询新候选时段 | 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. 语音与模型集成边界 ## 9. 语音与模型集成边界
@ -409,7 +349,7 @@ flowchart LR
- 语音失败:保留本地输入状态,提供「重新说」和「改用文字」。 - 语音失败:保留本地输入状态,提供「重新说」和「改用文字」。
- 模型失败:不自动重复多次调用;保留已验证草稿并导向普通表单。 - 模型失败:不自动重复多次调用;保留已验证草稿并导向普通表单。
- 工具查询失败:展示业务错误,不让模型根据记忆补齐结果。 - 工具查询失败:展示业务错误,不让模型根据记忆补齐结果。
- 最终号源冲突:会话进入 `needs_reselection`,不创建失败预约或假成功记录。 - M1 最终号源冲突:会话进入 `needs_reselection`,不创建失败预约或假成功记录。M0 只查询候选号源并回填表单。
## 10. 安全、隐私与可观测性 ## 10. 安全、隐私与可观测性
@ -476,7 +416,7 @@ flowchart LR
### M1受控确认创建 ### M1受控确认创建
- 增加 session 表、确认 endpoint、行锁和幂等返回 - 在 M0 session 表上增加 `appointment_id`、确认 endpoint、行锁和幂等返回
- 确认时重新校验权限、宠物归属、服务和容量; - 确认时重新校验权限、宠物归属、服务和容量;
- 补齐助手漏斗事件和 `bookingChannel` - 补齐助手漏斗事件和 `bookingChannel`
- 通过硬性安全验收后,再允许确认卡直接创建预约。 - 通过硬性安全验收后,再允许确认卡直接创建预约。
@ -492,19 +432,21 @@ flowchart LR
### 13.1 硬性门禁 ### 13.1 硬性门禁
- [ ] 未点击「确认预约」前不创建 `Appointment` - [ ] M0 不存在助手 `/confirm` endpoint不调用预约写服务
- [ ] 助手不会返回门店不存在的服务或后端未返回的号源。 - [ ] 助手不会返回门店不存在的服务或后端未返回的号源。
- [ ] 确认时复核服务时长、连续容量桶和门店锁,不产生超卖 - [ ] 草稿只使用当前 customer 的宠物、session 门店服务和后端真实号源
- [ ] 同一 session 并发或重复确认只有一个 `Appointment` - [ ] 门店在会话前选定session 创建后不能跨店更改
- [ ] A customer 不能查询或选择 B customer 的宠物、历史预约或 session。 - [ ] A customer 不能查询或选择 B customer 的宠物、历史预约或 session。
- [ ] 不将手机号、完整 `report_token`、session token 或密钥发送给模型或写入日志。 - [ ] 不将手机号、完整 `report_token`、session token 或密钥发送给模型或写入日志。
- [ ] 原始语音转写后立即释放,不出现在通用上传目录或报告媒体中。 - [ ] 原始语音转写后立即释放,不出现在通用上传目录或报告媒体中。
- [ ] 模型、语音或工具失败时能保留已验证草稿并切换普通表单。 - [ ] 模型、语音或工具失败时能保留已验证草稿并切换普通表单。
- [ ] `Appointment.status` 仍只有 `new`、`doing`、`done`、`cancel`。 - [ ] `Appointment.status` 仍只有 `new`、`doing`、`done`、`cancel`。
- [ ] 助手生成的真实预约有 `appointment_created` 事件和可统计 `bookingChannel` - [ ] 仅记录 M0 三个漏斗事件,不修改 `appointment_created` 事件语义
### 13.2 价值验证建议 ### 13.2 价值验证建议
首批 M0 先观察 `draft_ready / started`、`fallback / started`、语音转写修改率和普通表单回填成功率。下列「助手到真实预约」指标只能在有可验证的服务端草稿归因后使用,不接受客户端自由上报。
首批 20 个真实 session 用于发现语料和交互问题,不立即下商业化结论。稳定后可对 2050 个老客 session 使用以下建议门槛: 首批 20 个真实 session 用于发现语料和交互问题,不立即下商业化结论。稳定后可对 2050 个老客 session 使用以下建议门槛:
- 助手启动到真实预约成功率 ≥ 70% - 助手启动到真实预约成功率 ≥ 70%
@ -515,22 +457,27 @@ flowchart LR
上述价值门槛是试点假设,应在首批真实数据后与普通表单基线一起复核;不得用演示会话、测试预约或未履约预约代替真实样本。 上述价值门槛是试点假设,应在首批真实数据后与普通表单基线一起复核;不得用演示会话、测试预约或未履约预约代替真实样本。
## 14. 开发前待冻结决策 ## 14. M0 已冻结决策与生产前缺口
| ID | 决策 | 建议默认 | 责任人 | | ID | 决策 | 建议默认 | 责任人 |
|---|---|---|---| |---|---|---|---|
| D1 | M0 首发入口 | `CustAppointmentCreate` 顶部次级入口;历史预约、报告和首页入口后续评估 | Product Design | | D1 | M0 首发入口 | `CustAppointmentCreate` 顶部次级入口;先选门店,再登录/创建 session | Product Design |
| D2 | 是否支持匿名助手 | MVP 不支持,普通表单继续支持 guest 草稿 | Product Design + System Architect | | D2 | 是否支持匿名助手 | MVP 不支持,普通表单继续支持 guest 草稿 | Product Design + System Architect |
| D3 | M0 是否直接创建预约 | 否,先回填现有表单 | Product Design + Backend Core | | 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 | | D5 | 原文保留 | 默认不持久化对话原文,不持久化原始语音 | Access / Privacy Review |
| D6 | session TTL | MVP 30 分钟,编码前由 Data Model 冻结 | Data Model | | D6 | session TTL | 创建后固定 30 分钟,不滑动续期;过期 24 小时后物理删除 | Data Model |
| D7 | 助手对话风格 | 简短、一次一个关键问题,不模拟人格客服 | Product Design | | D7 | 助手对话风格 | 简短、一次一个关键问题,不模拟人格客服 | Product Design |
| D8 | 试点启动时机 | RC6 基础链路已用真实门店跑通并建立普通表单基线后 | 主控 PM | | D8 | 试点启动时机 | RC6 基础链路已用真实门店跑通并建立普通表单基线后 | 主控 PM |
## 15. 实施任务队列草案 ## 15. 实施任务队列
以下任务默认为 `Backlog`,不因本文档创建而自动进入开发。 当前可执行状态、owner、锁、依赖和验收口径只以 [M0 执行队列](./智能预约助手-M0执行队列-2026-08-02.md) 为准。其中 Batch 13 已具备开发边界;真实供应商和生产开启仍为 `Blocked`
<details>
<summary>已被独立执行队列取代的历史草案(仅供审计,不执行)</summary>
以下 BA-01BA-07 是契约冻结前的原始 Backlog不再表示当前任务状态。
### BA-01 冻结智能预约产品与交互契约 ### BA-01 冻结智能预约产品与交互契约
@ -707,12 +654,14 @@ Data Model Review Required: Yes
Access / Privacy Review Required: Yes Access / Privacy Review Required: Yes
``` ```
</details>
## 16. 开发与验证清单 ## 16. 开发与验证清单
实现时至少需要: 实现时至少需要:
1. 更新 `docs/ontology/objects.md`、`actions.md`、`events.md`、`rules.md`、`relations.md` 和 `graph/ontology.jsonl` 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 和普通表单降级; 3. 前端覆盖语音失败、模型失败、空输入、草稿修改、无号源、提交 loading 和普通表单降级;
4. 生产前检查供应商密钥、允许数据区域、超时、频控、日志脱敏和降级开关; 4. 生产前检查供应商密钥、允许数据区域、超时、频控、日志脱敏和降级开关;
5. 以普通预约为基线,不用智能助手取代现有真实门店试点指标。 5. 以普通预约为基线,不用智能助手取代现有真实门店试点指标。
@ -737,6 +686,6 @@ git -C /Users/apple/_src/petstore/docs diff --check
| 门店服务 | 服务项目具有名称和 `durationMinutes` | 智能助手只从真实列表中匹配 | | 门店服务 | 服务项目具有名称和 `durationMinutes` | 智能助手只从真实列表中匹配 |
| 可约时段 | `/api/appointment/available-slots` 按时长和容量返回 | 可复用领域服务,不由模型计算 | | 可约时段 | `/api/appointment/available-slots` 按时长和容量返回 | 可复用领域服务,不由模型计算 |
| 预约创建 | `AppointmentService.createBooking` 复核宠物、服务、时间和容量 | M1 确认 endpoint 的唯一写入权威 | | 预约创建 | `AppointmentService.createBooking` 复核宠物、服务、时间和容量 | M1 确认 endpoint 的唯一写入权威 |
| 业务事件 | `appointment_created` 已持久化 | 增加独立 `bookingChannel` 和助手漏斗事件 | | 业务事件 | `appointment_created` 已持久化 | M0 只增加三个助手漏斗事件;`bookingChannel` 与直接创建归因留待 M1 评审 |
本方案是对现有预约入口的语义化扩展,不建立第二套预约、容量或身份系统。 本方案是对现有预约入口的语义化扩展,不建立第二套预约、容量或身份系统。

View File

@ -0,0 +1,181 @@
# 智能预约助手 M0 执行队列
> 日期2026-08-02<br>
> 状态:**Batch 0 contract frozen / coding not started**<br>
> RC Included`No`<br>
> 当前优先级说明:不抢占 `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 stub30 条文字 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 | 五个 endpointcustomer 归属六态状态机draftVersion30 分钟 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 Key20 条已授权语音;隐私告知;实现任务完成 |
| 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。

View File

@ -1,32 +1,38 @@
# 智能预约助手 M0 编码任务 brief # 智能预约助手 M0 编码任务 brief
> 日期2026-08-02<br> > 日期2026-08-02<br>
> 状态:**Ready for Review / 尚未进入编码**<br> > 状态:**Ready / Batch 0 契约已冻结,尚未进入编码**<br>
> 目标:宠主用文字或语音生成可验证预约草稿,回填现有表单,不由智能层创建 `Appointment`<br> > 目标:宠主用文字或语音生成可验证预约草稿,回填现有表单,不由智能层创建 `Appointment`<br>
> Workstream`Core Booking Flow`<br> > Workstream`Core Booking Flow`<br>
> Owner Agent`Backend Core` + `Customer Experience FE`<br> > Owner Agent`Backend Core` + `Customer Experience FE`<br>
> Paired QA`Core Flow QA`<br> > Paired QA`Core Flow QA`<br>
> Service Lock`backend:booking-agent-session+read-tools` + `frontend:customer-booking-agent+shared-api`<br> > Service Lock`backend:booking-agent-session+read-tools` + `frontend:customer-booking-agent+shared-api`<br>
> Data Model Review Required`Yes`<br> > Architecture Review Required`Yes — completed by M0 ADR`<br>
> Access / Privacy Review Required`Yes`<br> > Data Model Review Required`Yes — proposal completed; implementation diff requires review`<br>
> Access / Privacy Review Required`Yes — development boundary completed; live provider/production pending`<br>
> RC Included`No` > RC Included`No`
**需求来源** **需求来源**
- [智能预约助手产品与技术方案 v0.1](./宠小它智能预约助手-产品与技术方案-v0.1.md) - [智能预约助手产品与技术方案 v0.1](./宠小它智能预约助手-产品与技术方案-v0.1.md)
- [模型与语音供应商选型](./智能预约助手-模型与语音供应商选型-2026-08-02.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. M0 交付结果
宠主在已登录状态进入「一句话预约」,输入文字或按住说话。系统依次: 宠主在普通预约页先选定门店,并在已登录状态进入「一句话预约」。系统依次:
1. 转写语音(文字输入跳过); 1. 使用已选 `storeId` 创建会话;
2. 提取宠物查询词、服务查询词、日期表达、时间偏好和备注; 2. 转写语音(文字输入跳过);
3. 在当前 customer 和门店范围内解析真实 Pet 与 ServiceType 3. 提取宠物查询词、服务查询词、日期表达、时间偏好和备注;
4. 调用现有容量服务获取真实号源; 4. 在当前 customer 和 session 门店范围内解析真实 Pet 与 ServiceType
5. 展示可修改草稿卡; 5. 调用现有容量服务获取真实号源;
6. 宠主点击「带入普通预约」后,回填现有 `CustAppointmentCreate` 6. 展示可修改草稿卡;
7. 宠主仍在现有表单上点击「提交预约」。 7. 宠主点击「带入普通预约」后,回填现有 `CustAppointmentCreate`
8. 宠主仍在现有表单上点击「提交预约」。
M0 成功的判定是 **「草稿真实、回填正确、普通预约未受影响」**,不是对话内创建预约。 M0 成功的判定是 **「草稿真实、回填正确、普通预约未受影响」**,不是对话内创建预约。
@ -47,21 +53,24 @@ M0 成功的判定是 **「草稿真实、回填正确、普通预约未受影
```mermaid ```mermaid
flowchart TD flowchart TD
A[宠主预约页] --> B[点击一句话预约] A[宠主预约页] --> B[点击一句话预约]
B --> C{已登录 customer} B --> C{已选有效门店}
C -- 否 --> D[登录并保留返回路由] C -- 否 --> D[使用现有控件选择门店]
D --> E[创建助手会话] D --> E{已登录 customer}
C -- 是 --> E C -- 是 --> E
E --> F[文字或语音输入] E -- 否 --> F[登录并保留返回路由和门店]
F --> G[结构化意图提取] F --> G[创建助手会话]
G --> H[服务端解析真实宠物和服务] E -- 是 --> G
H --> I[查询真实号源] G --> H[文字或语音输入]
I --> J{草稿完整} H --> I[结构化意图提取]
J -- 否 --> K[询问一个关键缺失项] I --> J[服务端解析真实宠物和服务]
K --> F J --> K[查询真实号源]
J -- 是 --> L[展示草稿卡] K --> L{草稿完整}
L --> M[带入普通预约] L -- 否 --> M[询问一个关键缺失项]
M --> N[CustAppointmentCreate 回填] M --> H
N --> O[用户在现有表单提交] L -- 是 --> N[展示草稿卡]
N --> O[带入普通预约]
O --> P[CustAppointmentCreate 回填]
P --> Q[用户在现有表单提交]
``` ```
### 3.1 入口 ### 3.1 入口
@ -72,6 +81,8 @@ M0 只在宠主的 `CustAppointmentCreate` 顶部增加一个次级入口:
该入口不取代当前表单和主提交按钮,不改宠主首页主 CTA。历史预约、报告页和宠物档案深链放到后续迭代。 该入口不取代当前表单和主提交按钮,不改宠主首页主 CTA。历史预约、报告页和宠物档案深链放到后续迭代。
M0 不在对话内选择门店。未选门店时先使用现有表单控件;创建 session 后 `storeId` 固定,切店需结束会话并重新创建。
### 3.2 对话风格 ### 3.2 对话风格
- 回复简短,一次只问一个关键缺失项; - 回复简短,一次只问一个关键缺失项;
@ -159,22 +170,23 @@ M0 只允许:
M0 字段: M0 字段:
- `session_id` VARCHAR(64) 主键; - `id` BIGINT 自增主键;
- `session_id` VARCHAR(36) 唯一外部 UUID
- `customer_user_id` BIGINT NOT NULL - `customer_user_id` BIGINT NOT NULL
- `store_id` BIGINT NULL - `store_id` BIGINT NOT NULL
- `status` VARCHAR(32) NOT NULL - `status` VARCHAR(24) NOT NULL
- `draft_json` TEXT NOT NULL - `draft_json` TEXT NOT NULL
- `draft_version` INT NOT NULL - `draft_version` INT NOT NULL
- `entry_source` VARCHAR(32) NOT NULL - `entry_source` VARCHAR(32) NOT NULL
- `input_modality` VARCHAR(16) NULL - `input_modality` VARCHAR(16) NULL
- `expires_at` DATETIME NOT NULL - `expires_at` DATETIME NOT NULL
- `create_time` / `update_time` - `create_time` / `update_time`
- `deleted` TINYINT(1) NOT NULL DEFAULT 0。
索引: 索引:
- `UNIQUE (session_id)`
- `(customer_user_id, status, expires_at)` - `(customer_user_id, status, expires_at)`
- `(expires_at, deleted)` 用于过期清理。 - `(status, expires_at)` 用于过期和物理清理。
M0 不增加 `appointment_id`,也不建立 `t_booking_agent_turn`。`appointment_id` 唯一关联在 M1 幂等写入评审时再增加,避免 M0 数据模型暗示已具备直接创建能力。 M0 不增加 `appointment_id`,也不建立 `t_booking_agent_turn`。`appointment_id` 唯一关联在 M1 幂等写入评审时再增加,避免 M0 数据模型暗示已具备直接创建能力。
@ -182,6 +194,8 @@ M0 的 `entry_source` 只允许 `appointment_create`;历史预约、报告、
`draft_json` 只保存已验证 ID、时间约束和必要备注不保存每轮用户原文、提示词或模型原始响应。 `draft_json` 只保存已验证 ID、时间约束和必要备注不保存每轮用户原文、提示词或模型原始响应。
TTL 固定为创建后 30 分钟,不滑动续期;过期 24 小时后物理删除。M0 不增加 `deleted`,详细迁移和清理口径以 ADR 为准。
### 4.5 API 契约 ### 4.5 API 契约
M0 只实现: M0 只实现:
@ -190,12 +204,14 @@ M0 只实现:
|---|---|---| |---|---|---|
| `POST` | `/api/booking-agent/sessions` | 创建已登录 customer 会话 | | `POST` | `/api/booking-agent/sessions` | 创建已登录 customer 会话 |
| `POST` | `/api/booking-agent/sessions/{sessionId}/messages` | 提交文字/语音转写文本,刷新草稿 | | `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` | 返回可回填普通表单的已验证草稿 | | `POST` | `/api/booking-agent/sessions/{sessionId}/fallback` | 返回可回填普通表单的已验证草稿 |
| `DELETE` | `/api/booking-agent/sessions/{sessionId}` | 将本人会话标记为 `cancelled` | | `DELETE` | `/api/booking-agent/sessions/{sessionId}` | 将本人会话标记为 `cancelled` |
所有 endpoint 都要求 `CurrentUserContext.require()` 且 role 为 `customer`。每次读写 session 都以 `sessionId + current.userId` 查询;他人 session 和不存在 session 均返回统一 404 语义,避免泄漏 session 是否存在。 所有 endpoint 都要求 `CurrentUserContext.require()` 且 role 为 `customer`。每次读写 session 都以 `sessionId + current.userId` 查询;他人 session 和不存在 session 均返回统一 404 语义,避免泄漏 session 是否存在。
创建 session 时 `storeId` 必填并验证存在;之后不再接受客户端传门店。消息和 fallback 请求必须携带当前 `draftVersion`,版本不一致返回 `DRAFT_VERSION_CONFLICT`
M0 绝对不注册 `/confirm` endpoint。 M0 绝对不注册 `/confirm` endpoint。
### 4.6 模型输出强校验 ### 4.6 模型输出强校验
@ -214,6 +230,7 @@ M0 绝对不注册 `/confirm` endpoint。
"end": null "end": null
}, },
"remark": null, "remark": null,
"clearFields": [],
"ambiguities": [], "ambiguities": [],
"nextAction": "resolve_context" "nextAction": "resolve_context"
} }
@ -221,7 +238,7 @@ M0 绝对不注册 `/confirm` endpoint。
校验规则: 校验规则:
- 未知字段拒绝或明确忽略,不得进业务实体; - 未知字段拒绝,不得进业务实体;
- `schemaVersion`、`intent` 和 `nextAction` 为枚举; - `schemaVersion`、`intent` 和 `nextAction` 为枚举;
- 文本字段 trim、限长 - 文本字段 trim、限长
- 模型输出 ID、价格、号源或预约状态字段时整个响应降级 - 模型输出 ID、价格、号源或预约状态字段时整个响应降级
@ -299,7 +316,7 @@ frontend/src/pages.json # 路由
## 6. 业务事件与本体 ## 6. 业务事件与本体
M0 候选事件: M0 已冻结事件:
- `booking_agent_started` - `booking_agent_started`
- `booking_agent_draft_ready` - `booking_agent_draft_ready`
@ -307,15 +324,17 @@ M0 候选事件:
M0 不记录 `booking_agent_confirmed`,也不修改现有 `appointment_created` metadata因为助手没有直接确认写入而客户端的 `source` 字符串不能当作服务端归因事实。如后续需要精确联结「助手草稿 -> 表单创建」,另立 brief 设计可验证、一次性的草稿交接机制。 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/actions.md`:创建会话、提交消息、语音转写、回填草稿、结束会话;
- `ontology/events.md`M0 三个漏斗事件; - `ontology/events.md`M0 三个漏斗事件;
- `ontology/rules.md`customer 数据范围、无写工具、原始语音/原文不持久化、超时降级; - `ontology/rules.md`customer 数据范围、无写工具、原始语音/原文不持久化、超时降级;
- `ontology/relations.md``graph/ontology.jsonl` - `ontology/relations.md``graph/ontology.jsonl`
- `coverage/ontology-coverage-audit.md` - `coverage/ontology-coverage-audit.md`
实现时必须在同一批把真实代码/测试路径补入,并将对应条目从 `documented` 更新为 `anchored`;不得提前宣称已实现。
## 7. 自动化测试 ## 7. 自动化测试
### 7.1 后端必测 ### 7.1 后端必测
@ -329,6 +348,7 @@ M0 不记录 `booking_agent_confirmed`,也不修改现有 `appointment_created
**会话和权限** **会话和权限**
- 未登录、boss/staff 调用助手 endpoint 被拒绝; - 未登录、boss/staff 调用助手 endpoint 被拒绝;
- 创建 session 缺少/伪造门店被拒绝,创建后不能跨店覆盖;
- A customer 无法查询、修改或结束 B customer session - A customer 无法查询、修改或结束 B customer session
- session 过期后不再接收消息; - session 过期后不再接收消息;
- M0 不存在 `/confirm` endpoint - M0 不存在 `/confirm` endpoint
@ -369,10 +389,12 @@ M0 不记录 `booking_agent_confirmed`,也不修改现有 `appointment_created
OwnerSystem Architect + Data Model + Product Design OwnerSystem Architect + Data Model + Product Design
状态:**Completed — 2026-08-02**
- 冻结 `booking-intent-v1`、M0 状态子集、API shape 和业务码; - 冻结 `booking-intent-v1`、M0 状态子集、API shape 和业务码;
- 更新本体及机器图谱; - 更新本体及机器图谱;
- 评审 session 表、TTL 和清理方式; - 评审 session 表、TTL 和清理方式;
- 完成 Access / Privacy Review。 - 完成开发态 Access / Privacy Review;真实 provider 与生产复核仍在 Batch 4
### Batch 1供应商适配与契约测试 ### Batch 1供应商适配与契约测试

View File

@ -0,0 +1,295 @@
# 架构决策:智能预约助手 M0 契约与数据模型
> 日期2026-08-02<br>
> 状态:**Accepted for M0 implementation / 尚未实现**<br>
> Workstream`Core Booking Flow`<br>
> Track`Architecture`<br>
> Priority`P0`(独立试验流,不高于 RC6 真实门店基线)<br>
> Owner Agent`System Architect`<br>
> Paired QA`Core Flow QA`<br>
> Service Lock`docs:booking-agent-m0-contract+ontology`<br>
> Architecture Review Required`Yes — completed by this ADR`<br>
> Data Model Review Required`Yes — completed for implementation proposal`<br>
> Access / Privacy Review Required`Yes — development boundary accepted; live provider and production review pending`<br>
> 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 纳入必须重新授权。