Files
story/docs/saas-core-boundary.md

81 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SaaS核心业务边界与首批接口冻结
## 1. 本阶段目标
本阶段把现有登录、家庭成员权限和设备通话数据扩展为家庭用户可使用的SaaS业务纵向链路。当前范围覆盖家庭、成员邀请与角色协作、长者档案、稳定事实、分项授权、设备绑定与状态、设备影子配置、通话查询、最终字幕、摘要、故事人工审核和已确认记忆;设备命令、录音对象管理及生产级模型供应商在后续批次按相同租户与家庭边界继续实现。
## 2. 服务端边界
- OIDC仅证明外部身份;平台根据`issuer + subject`解析有效账号、客户组织和家庭成员关系。
- 集合查询不得接受浏览器直接提交内部租户主键。`X-Tenant-Id`使用公开客户组织编号,服务端仍需验证当前外部身份在该客户组织中存在有效账号。
- 家庭、长者、设备、通话和授权查询必须同时带客户组织条件与家庭成员关系,跨家庭访问统一返回无权限或不存在,不泄露资源是否真实存在。
- 家庭角色控制管理能力,但不自动授予录音播放、下载、字幕正文等高敏感数据访问权。
- 家庭和长者修改使用强ETag与`If-Match`;版本不一致返回`VERSION_CONFLICT`,禁止静默覆盖。
- 授权记录保存授予事实并只允许追加;撤回只填写原记录的`withdrawn_at`,不得修改授权类型、政策、告知摘要、证据或授权时间。
- 每次授权变化创建新的授权快照供后续新通话使用。历史通话快照保持不可变;但活动通话正在使用的字幕、原录音、故事整理或个性化记忆授权被撤回时,为避免继续按旧权限处理数据,撤回事务同时把通话转入安全停止。家庭分享撤回只影响分享访问,不中断实时语音。
- 长者档案中的背景和交流偏好允许扩展保存,但只有OpenAPI声明的背景、兴趣、交流目标、节奏和优先话题会进入新通话上下文;其他字段保留在SaaS业务域,不向语音供应商传递。
## 3. 首批网页接口
| 方法 | 路径 | 权限与并发规则 |
|---|---|---|
| GET/POST | `/api/v1/households` | 登录账号可读;创建必须选择并验证`X-Tenant-Id` |
| GET/PATCH | `/api/v1/households/{id}` | `household.read` / `household.manage`;PATCH要求`If-Match` |
| POST | `/api/v1/member-invitations/preview` | 登录后按一次性凭据只读预览目标客户组织、家庭、角色和到期时间;不创建账号或成员关系,响应禁止缓存 |
| POST | `/api/v1/member-invitations/accept` | 以当前OIDC身份接受一次性邀请;事务内重新校验邀请状态、账号状态和既有成员关系 |
| GET/POST | `/api/v1/households/{id}/subjects` | `subject.read` / `subject.write` |
| GET/PATCH | `/api/v1/subjects/{id}` | `subject.read` / `subject.write`;PATCH要求`If-Match` |
| GET | `/api/v1/subjects/{id}/conversation-readiness` | `subject.read`;汇总家庭可处理的首次通话阻断项与运行提醒,不替代创建时的额度和路由原子门禁 |
| GET/POST/PATCH | `/api/v1/subjects/{id}/facts` | 候选与已确认事实分离;确认必须有来源,PATCH要求`If-Match` |
| GET/POST | `/api/v1/subjects/{id}/consents` | 查看授权历史;有写权限才能授予 |
| POST | `/api/v1/subjects/{id}/consents/{consent_id}/withdraw` | 有写权限;同一撤回请求可安全重复;涉及活动通话时同步保存停止意图 |
| POST | `/api/v1/device-claims` | 一次性绑定码仅保存摘要;同一目标安全重试不生成第二条绑定 |
| GET | `/api/v1/devices`、`/api/v1/devices/{id}` | 只返回当前用户家庭内设备及脱敏状态 |
| GET/PATCH | `/api/v1/devices/{id}/twin` | 读写期望/实际配置;PATCH要求`If-Match`且禁止长期密钥字段 |
| GET | `/api/v1/devices/{id}/presence` | 优先读取Redis热状态,缺失时返回数据库最后上报并标记离线 |
| GET | `/api/v1/subjects/{id}/conversations` | `conversation.read`;HMAC签名游标分页 |
| GET | `/api/v1/conversations/{id}` | 返回状态、快照版本、RTC任务摘要和状态历史,不返回Token或长期密钥 |
通话记录页面直接使用上述查询契约:状态、语音处理方式、创建时间下界和创建时间上界均作为服务端过滤条件,页面不会先拉取全部数据后在浏览器内筛选。默认每页20条;存在`next_cursor`时才显示“加载更多”,后续请求只回传服务端签发的游标,浏览器不解析、不修改游标。页面上的结束日期按用户所在时区的次日零点转换为接口的不包含上界,使所选结束日期当天仍被完整纳入查询。切换筛选条件会清理旧详情,普通SSE状态刷新则保留当前详情,避免用户阅读过程中被实时事件打断。
列表与详情把`s2s_pure`、`s2s_hybrid`、`cascade`以及三种保存策略转换为家庭成员可理解的业务名称,不直接展示内部枚举。打开通话时并行读取详情、话轮、整理结果和费用;切换通话或筛选条件会取消尚未完成的旧详情请求,避免迟到响应覆盖当前选择。任一核心详情请求失败时清空旧详情,在右侧显示本次错误和页面内重试入口,不把错误混入长者档案提示。仅当本次冻结策略为`transcript_and_audio`时才请求录音媒体;不保存录音的通话不会发起无意义的媒体权限请求。
首次登录但尚无家庭成员关系时,网页仍根据`/api/v1/me`返回的有效客户组织账号展示“创建第一个家庭”。创建入口先打开页面内受控操作区,固定展示客户组织、当前账号和新家庭名称,并明确当前账号将成为所有者以及家庭初始不包含长者、授权、设备或通话;确认前不发送创建请求。浏览器只提交所选公开客户组织编号和家庭名称;服务端重新验证当前OIDC身份在该客户组织中的有效账号,并在一个事务内创建家庭、所有者成员关系和审计记录。创建后网页重新读取家庭清单并选中新家庭,不依赖客户端自行伪造角色或能力。
## 4. 数据模型增量
- `subject_profile`补充语言、IANA时区、扩展资料、交流偏好及创建/修改人;`profile_version`继续作为乐观锁版本。
- 新增长者档案使用页面内受控操作区,固定家庭、称呼、语言、时区、背景、兴趣、交流目标、节奏和优先话题,并展示经过数量和单项长度限制后实际保存的内容。操作者必须确认这些资料可作为长期个人档案保存、只从后续新通话开始按版本使用,并理解创建档案不等于授予通话、录音、故事或记忆权限;取消或切换业务上下文不发送创建请求。
- 修订长者档案时,页面先按字段展示当前版本到下一版本的实际差异,并明确只影响后续新通话,当前和历史通话继续保留原冻结快照,分项授权也不会随档案修订扩大。无变化时禁止提交;确认后仍使用当前档案强ETag,其他成员已经保存新版本时返回冲突而不覆盖。
- `subject_fact`单独保存稳定事实,明确来源类型、确认状态、敏感等级和版本,模型候选不能冒充已确认事实。
- 家庭成员登记稳定事实时必须显式选择事实类型和敏感等级;选择健康背景会把默认等级提升为敏感,但允许操作者进一步核对调整。提交前的受控操作区固定长者、完整事实内容、家庭录入来源、敏感等级和待核对状态,并明确创建候选不会自动确认、创建记忆或改写历史通话。
- 待核对稳定事实使用页面内受控确认区,固定展示长者、事实类型、完整内容、来源类型/引用、敏感等级、当前状态和行版本。操作者必须确认事实不是未经复核的模型推断,并理解确认只从后续新通话开始作为已核对背景,不改写历史通话;取消或业务上下文变化不发送PATCH。服务端继续要求来源引用、写权限和强`If-Match`。
- `consent_record`保存五类分项授权、范围、政策版本、告知摘要、授权人、证据、有效期和撤回时间。
- `consent_snapshot`仍是通话使用的不可变授权依据;网页授权变化不修改历史通话快照。
- 新增分项授权先进入页面内受控操作区。网页冻结长者、授权类型、仅作用于后续新通话的范围、当前政策版本、告知摘要、证据引用和有效期,并按授权类型说明实际数据影响;操作者必须确认已经向长者或合法授权人单独说明数据范围、用途和撤回方式后才可提交。切换授权类型会清除上一类型的告知确认;当前长者已有同类型有效授权时,页面直接阻止重复提交,服务端仍在事务内二次校验并返回稳定冲突,避免并发绕过页面形成重复有效授权。
## 5. 不变量
1. 对外响应只使用公开随机编号,不返回数据库内部主键。
2. 所有写操作在业务数据、审计和可靠待办需要同时成立时使用同一数据库事务。
3. 自由文本、档案和授权证据不写入普通请求日志。
4. 删除、撤回、邀请和绑定等跨资源动作在对应批次完成前不得以简单物理删除替代正式流程。
## 家庭成员协作边界
- 邀请使用256位随机凭据,明文只在创建响应中返回一次,服务端只保存SHA-256摘要;邀请可以撤销并具有明确过期时间。
- 创建成员邀请使用页面内受控操作区,固定家庭、接收人提示、加入角色和七天有效期。页面明确接收人提示只供人工识别,不绑定或校验真实身份;实际持有凭据的登录账号会被映射到目标客户组织并以所选角色加入家庭。操作者确认凭据只显示一次、应通过受控渠道交付且泄露时立即撤销后,页面才调用创建接口;取消不生成邀请,也不提前清除上一条尚未保存的邀请凭据。
- 接受邀请前,网页使用独立只读预览接口按凭据摘要解析目标客户组织、家庭、接收人提示、角色和到期时间;预览响应使用`Cache-Control: no-store`,页面只显示掩码凭据且不建立任何关系。操作者确认以当前登录身份加入所示家庭后才调用接受接口;正式提交仍在事务内重新校验邀请未使用、未撤销、未过期、账号未禁用且当前身份尚不是该家庭成员。
- 接受邀请时以当前OIDC的`issuer + subject`建立目标客户组织内的平台账号映射;已有禁用账号不会被邀请流程自动重新启用。
- 同一邀请只能使用一次;重复、过期或已撤销的接受请求返回稳定冲突错误,并保留失败审计。
- 待接受邀请的撤销使用页面内受控操作表单,固定展示家庭、接收人提示、目标角色、邀请编号、到期时间和当前状态。操作者必须确认原一次性凭据将立即失效且已有成员关系不受影响;取消,或切换页签、家庭、长者,会清空草稿且不发送撤销请求。
- 所有者不能通过普通角色修改或成员移除接口降级、删除;必须使用独立所有权转移事务,把新所有者提升和原所有者降为管理员同步提交。
- 角色修改、成员移除和所有权转移使用强ETag,旧版本写入返回`412`。
- 非所有者成员的角色调整不再由列表下拉框立即保存,而是打开页面内受控操作区,固定家庭、成员、当前角色、成员编号和行版本。目标角色变化会清除旧确认,页面逐项说明管理员、编辑者和查看者的一般能力,并明确角色不会隐式授予录音播放、下载或其他高敏感资源权限。
- 授权撤回、成员移除和所有权转移使用页面内受控操作表单,不再依赖浏览器确认弹窗。表单固定展示长者/家庭、目标成员、角色、授权策略及行版本,并在提交前说明记忆停止使用、成员访问立即失效或原所有者降为管理员等业务影响。
- 成员移除和所有权转移必须逐字输入目标成员显示名,授权撤回必须显式勾选影响确认。切换业务页签、家庭或长者,以及取消操作,都会清空尚未提交的操作草稿且不产生业务写入;服务端角色、租户、状态和ETag并发门禁保持不变。
邀请创建页面采用真实OIDC无提交轻量冒烟:验证家庭、提示、管理员角色、七天有效期、提示不绑定身份和一次显示说明,未确认时提交按钮禁用,取消后无凭据展示、零邀请POST且邀请表数量不变。家庭创建和邀请接受复用一次真实OIDC轻量烟测:创建确认区验证组织、账号、家庭名称和所有者影响;接受流程只发起一次只读预览并验证目标、角色、掩码凭据和二次确认,取消后零家庭创建/邀请接受请求,家庭、成员和邀请状态均不变。邀请随机数、摘要落库、一次消费、跨客户组织账号映射、撤销、过期和复用失败审计继续由既有服务集成测试覆盖。
分项授权与固件灰度创建复用一次真实OIDC无提交轻量冒烟:验证已有同类有效授权被明确提示并阻断、切换类型会清除旧告知确认、新授权确认区冻结长者/政策/证据/数据影响,取消后不创建授权;服务端的授权唯一性、政策版本、不可变快照和撤回语义继续由既有定向集成测试覆盖。