Skip to content

Architecture interface design(draft)

hqy edited this page Jul 23, 2026 · 4 revisions

3. 统一模块边界

模块类型 标准模块名 本次接口合并处理
AI 编排模块 母 AI 编排模块 合并感知层和执行层,内部划分为“输入理解阶段”和“执行编排阶段”
AI 编排模块 反问机制 归入母 AI 编排模块内部函数,不单独作为独立服务
子 Agent 日程待办 Agent 提供事项 CRUD 候选生成,不直接落盘
子 Agent 反馈 Agent 提供反馈目标定位和反馈结构化候选,不直接落盘
子 Agent 重排 Agent 提供重排方案生成,不直接应用
子 Agent 复盘 Agent 提供文本复盘结果,默认不写数据库
子 Agent 长任务拆分 Agent 提供宏观规划文本和近期子任务候选,不直接创建正式子任务
公共基础模块 全局数据模块 提供事实查询、校验、确认后写入
公共基础模块 LLM 管理模块 提供结构化模型调用、提示词渲染和模型切换
公共基础模块 系统日志模块 MVP 暂不实现数据库和接口
公共基础模块 对象存储 提供图片、音频上传和 source_url
公共基础模块 OCR 识别模块 根据 source_url 输出 OCR 文本
公共基础模块 ASR 语音识别模块 根据 source_url 输出 ASR 文本
公共基础模块 用户账号模块 提供注册、登录、当前用户识别和业务用户 ID
公共基础模块 全局用户画像管理模块 提供全局画像查询和更新
公共基础模块 任务级画像管理模块 提供长任务维度画像查询和创建落盘
公共基础模块 弹窗确认机制 前端交互能力,后端只接收确认结果
公共基础模块 基础业务模块 提供今日事项、长目标、个人主页等基础页面数据
前端模块 消息推送模块 前端实现,不进入主 Agent 函数调用链
前端模块 前端交互与展示模块 调用 API、建立 WS、展示结果和确认卡片

4. 母 AI 编排模块合并结果

合并后,母 AI 编排模块只有一个模块,不再把“感知层”和“执行层”作为外部并列模块。

内部阶段如下:

阶段 职责 输入 输出 禁止行为
输入理解阶段 接收用户输入、归一化文本、读取最近 20 条对话、识别意图、解析参数、触发反问 WsEnvelope(dialogue.input / dialogue.answer) PerceptionDecisionClarificationRequest 不查询业务候选事项,不调用子 Agent,不写业务事实
执行编排阶段 校验结构化决策、查询必要事实、指代消解、调用目标子 Agent、处理返回、创建确认请求 ExecutionLayerInput MainAgentEvent 不重新识别意图,不重新补参,不绕过确认写业务事实

感知层和执行层的唯一交接对象是:

class ExecutionLayerInput(BaseModel):
    target_agent_name: AgentName
    target_function_name: str
    parsed_params: dict[str, Any]
    time_range: TimeRange
    raw_input: str
    source_message_id: UUID
    recent_messages: list[ConversationMessageDTO]
    trace_id: str | None = None

4.1 输入理解阶段规则

场景 处理规则 输出
普通任务参数完整 根据子 Agent 能力表识别 target_agent_nametarget_function_name,解析参数并抽取顶层 time_range ExecutionLayerInput
普通任务参数缺失 不进入执行编排阶段,先生成反问 dialogue.clarification
用户回答反问 通过 reply_to_question_id 恢复上一轮问题;如果回答无效,继续反问;如果无法关联,退化为新的普通输入 dialogue.decisiondialogue.clarification
长任务拆分 命中长任务拆分后先检查最近 20 条对话是否已有可复用问答;没有则一次性生成本次拆分所需问题 dialogue.clarification(mode=batch)ExecutionLayerInput

4.2 执行编排阶段规则

场景 处理规则 输出
创建类函数 不需要定位已有事项,可直接装配上下文并调用子 Agent AgentResponseEnvelope
更新 / 删除 / 反馈类函数 先按 time_range 和结构化参数粗筛数据库候选,再做范围内指代消解 single / range / ambiguous / not_found
候选唯一 直接装配子 Agent 入参 SubAgentInputBase
候选不唯一 通过 WS 返回候选选择,不调用子 Agent selection.required
子 Agent 返回写入候选 创建 write_requests,前端确认后再落盘 write.confirmation_required

5. 统一 WebSocket 协议

5.1 统一信封

所有 WS 消息统一使用同一信封,所有业务字段和关联字段全部放入 payload

class WsEnvelope(BaseModel):
    type: str
    timestamp: datetime
    payload: dict[str, Any]

字段说明:

字段 用途 生成方
type 消息类型,决定进入哪条流程 前端或后端
timestamp 消息产生时间 消息发送方
payload 当前消息的业务数据与关联字段 消息发送方

5.2 WS 消息类型

类型 方向 所属阶段 说明
dialogue.input 前端 -> 后端 输入理解阶段 用户发送文本、语音或图片输入
dialogue.answer 前端 -> 后端 输入理解阶段 用户回答反问问题
dialogue.clarification 后端 -> 前端 输入理解阶段 参数不足、答非所问或长任务拆分需要补充信息
dialogue.decision 后端 -> 前端 输入理解阶段 感知层已经识别出目标子 Agent、函数和参数
execution.processing 后端 -> 前端 执行编排阶段 已进入执行编排,正在查询事实或调用子 Agent
selection.required 后端 -> 前端 执行编排阶段 多候选无法唯一定位,需要用户选择
selection.submit 前端 -> 后端 执行编排阶段 用户提交候选选择
selection.cancel 前端 -> 后端 执行编排阶段 用户取消候选选择
write.confirmation_required 后端 -> 前端 确认阶段 有写入候选,需要用户确认
write.decide 前端 -> 后端 确认阶段 用户确认或拒绝写入
write.applied 后端 -> 前端 确认阶段 确认写入已完成
execution.result 后端 -> 前端 执行编排阶段 查询、复盘等无需写入的展示结果
dialogue.error 后端 -> 前端 通用 输入理解失败
execution.error 后端 -> 前端 通用 执行编排失败
transport.ping / transport.pong 双向 传输层 心跳
transport.resume / transport.snapshot 双向 传输层 断线恢复

5.3 输入消息 payload

InputModality = Literal["text", "voice_asr", "image_ocr"]

class DialogueInputPayload(BaseModel):
    client_message_id: str
    modality: InputModality
    raw_content: str | None = None
    source_url: str | None = None
    reply_to_question_id: str | None = None
    source_message_id: UUID | None = None
    trace_id: str | None = None

约束:

输入类型 raw_content source_url 处理方式
text 必填 必须为空 直接进入意图识别
voice_asr 为空 必填 母 AI 内部调用 ASR 得到文本
image_ocr 为空 必填 母 AI 内部调用 OCR 得到文本

约束:

字段 约束
client_message_id 必填,用于客户端幂等
reply_to_question_id 仅在回答反问时可填
source_message_id 仅在恢复、继续或执行链路中可填
trace_id 可空,由后端链路注入

落库映射:

WS payload 字段 conversation_messages.metadata 对应字段
client_message_id client_message_id
reply_to_question_id reply_to_question_id
trace_id trace_id
target_agent_name / target_function_name 同名字段
parsed_params / time_range 同名字段

说明:source_message_id 不进入 metadata;它只作为当前消息主键 conversation_messages.id 的结果引用,或者作为主流程链路中的外部引用字段单独消费,避免和追问状态重复表达。

6. 统一数据结构

InputModality = Literal["text", "voice_asr", "image_ocr"]

AgentName = Literal[
    "schedule_todo_agent",
    "feedback_agent",
    "replan_agent",
    "review_agent",
    "long_task_split_agent",
]

AgentDisplayName = Literal[
    "日程待办 Agent",
    "反馈 Agent",
    "重排 Agent",
    "复盘 Agent",
    "长任务拆分 Agent",
]

说明程序内部统一使用 `AgentName` 作为分发和提示词映射键前端展示和文档说明可使用 `AgentDisplayName`class TimeRange(BaseModel):
    start_at: datetime
    end_at: datetime
    source: Literal["user_explicit", "agent_inferred", "system_default"]

class ConversationMessageDTO(BaseModel):
    id: UUID
    user_id: UUID
    message_index: int
    role: Literal["user", "assistant", "system", "tool"]
    modality: InputModality | None
    raw_content: str
    source_url: str | None
    metadata: "ConversationMessageMetadata"
    created_at: datetime

class ConversationMessageMetadata(BaseModel):
    schema_version: int = 1
    message_kind: Literal[
        "input",
        "clarification_question",
        "clarification_answer",
        "decision",
        "execution_processing",
        "selection_required",
        "selection_submit",
        "selection_cancel",
        "execution_result",
        "confirmation_request",
        "confirmation_decision",
        "system_notice",
        "error",
    ]
    client_message_id: str | None = None
    trace_id: str | None = None
    question_id: UUID | None = None
    question_status: Literal["open", "answered", "expired"] | None = None
    question_type: Literal["normal", "long_task_split", "disambiguation", "confirmation"] | None = None
    expires_at: datetime | None = None
    reply_to_question_id: UUID | None = None
    target_agent_name: AgentName | None = None
    target_function_name: str | None = None
    missing_fields: list[str] = Field(default_factory=list)
    parsed_params: dict[str, Any] = Field(default_factory=dict)
    time_range: TimeRange | None = None
    write_request_id: UUID | None = None
    decision: Literal["confirmed", "rejected"] | None = None

class TaskItemDTO(BaseModel):
    id: UUID
    user_id: UUID
    long_goal_id: UUID | None = None
    item_type: Literal["schedule", "todo", "subtask"]
    title: str
    description: str | None = None
    start_at: datetime | None = None
    end_at: datetime | None = None
    due_at: datetime | None = None
    status: Literal["planned", "in_progress", "completed", "cancelled", "deferred"]
    version: int

class LongGoalDTO(BaseModel):
    id: UUID
    user_id: UUID
    title: str
    description: str | None = None
    plan_overview: str | None = None
    start_at: datetime | None = None
    deadline_at: datetime | None = None
    status: Literal["active", "completed", "cancelled"]
    version: int

class FeedbackDTO(BaseModel):
    id: UUID
    user_id: UUID
    task_item_id: UUID
    feedback_status: Literal["completed", "not_completed", "partially_completed", "deferred", "cancelled"]
    normalized_feedback: str
    duration_minutes: int | None = None
    occurred_at: datetime

class TaskProfileSnapshot(BaseModel):
    id: UUID
    user_id: UUID
    long_goal_id: UUID
    profile_summary: str
    source_message_id: UUID
    version: int

class GoalSplitCandidate(BaseModel):
    plan_overview: str
    task_profile_summary: str
    subtasks: list[dict[str, Any]] = Field(default_factory=list)

class WriteDecisionResult(BaseModel):
    write_request_id: UUID
    status: Literal["confirmed", "rejected", "applied", "expired"]
    applied_target_id: UUID | None = None
    message: str | None = None

class AgentFunctionDefinition(BaseModel):
    agent_name: AgentName
    function_name: str
    required_fields: list[str]
    optional_fields: list[str] = Field(default_factory=list)
    description: str

class ClarificationQuestion(BaseModel):
    question_id: str
    field_name: str
    question_text: str
    options: list[dict[str, str]] = Field(default_factory=list)

class ClarificationRequest(BaseModel):
    reason: Literal["missing_required_params", "invalid_answer", "long_task_split_questions", "ambiguous_reference"]
    mode: Literal["single", "batch"]
    questions: list[ClarificationQuestion]
    original_agent_name: AgentName | None
    original_function_name: str | None

class AgentResponseEnvelope(BaseModel):
    agent_name: AgentName
    function_name: str
    isNeedUser: bool
    isDisplayResult: bool
    isError: bool
    result: dict[str, Any] | str | list[Any]
    db_action: dict[str, Any] | None = None
    error_message: str | None = None

class PerceptionDecision(BaseModel):
    target_agent_name: AgentName
    target_function_name: str
    parsed_params: dict[str, Any]
    time_range: TimeRange
    missing_fields: list[str] = Field(default_factory=list)
    raw_input: str
    source_message_id: UUID
    recent_messages: list[ConversationMessageDTO]
    trace_id: str | None = None

class MainAgentEvent(BaseModel):
    event_type: Literal[
        "dialogue.decision",
        "dialogue.clarification",
        "execution.processing",
        "selection.required",
        "write.confirmation_required",
        "write.applied",
        "execution.result",
        "dialogue.error",
        "execution.error",
    ]
    source_message_id: UUID | None = None
    trace_id: str | None = None
    payload: dict[str, Any]

class SubAgentInputBase(BaseModel):
    user_id: UUID
    raw_input: str
    time_range: TimeRange
    recent_messages: list[ConversationMessageDTO]
    parsed_params: dict[str, Any]
    candidate_items: list[dict[str, Any]] = Field(default_factory=list)

class ScopeResolutionResult(BaseModel):
    decision: Literal["single", "range", "ambiguous", "not_found"]
    resolved_item_ids: list[UUID] = Field(default_factory=list)
    resolved_item_id: UUID | None = None
    question_text: str | None = None

7. 前后端交互级别 API

7.1 HTTP API

API 名称 方法与路径 调用方 承接模块 入参 出参 是否写入 说明
注册用户 POST /api/v1/auth/register 前端交互与展示模块 用户账号模块 emailpassworddisplay_name user_idbusiness_user_idaccess_token 创建用户账号,不进入母 AI
登录用户 POST /api/v1/auth/login 前端交互与展示模块 用户账号模块 emailpassword user_idbusiness_user_idaccess_token 建立登录态
获取当前用户 GET /api/v1/auth/me 前端交互与展示模块 用户账号模块 登录凭证 user_idbusiness_user_iddisplay_name 页面初始化使用
上传原始媒体文件 POST /api/v1/media/upload 前端交互与展示模块 对象存储 filefile_type=image/audio source_urlobject_key 是,写对象存储 只保存原始文件,不做 OCR / ASR
查询会话消息 GET /api/v1/conversation/messages 前端交互与展示模块 全局数据模块 user_idbefore_message_idlimit messages: list[ConversationMessageDTO]has_more 用于聊天页历史展示
恢复会话快照 GET /api/v1/conversation/snapshot 前端交互与展示模块 母 AI 编排模块 user_idlast_seen_message_id recent_messagespending_clarificationlast_event WS 断线恢复
查询时间顺序事项 GET /api/v1/items/timeline 前端交互与展示模块 基础业务模块 user_idstart_atend_atitem_type? items: list[TaskItemDTO] 时间顺序 Tab 展示
查询长目标列表 GET /api/v1/long-goals 前端交互与展示模块 基础业务模块 user_idstatus? goals: list[LongGoalDTO] 长任务目标管理 Tab
查询长目标详情 GET /api/v1/long-goals/{goal_id} 前端交互与展示模块 基础业务模块 user_idgoal_id goalsubtaskstask_profile 长目标详情展示
查询全局用户画像 GET /api/v1/user/profile 前端交互与展示模块 全局用户画像管理模块 user_id UserProfileDTO 个人主页展示
创建手动写入请求 POST /api/v1/write-requests 前端交互与展示模块 全局数据模块 actiontarget_typetarget_id?payloadpreview_text write_request_idpayload_hashconfirmation_payload 是,写入请求 手动新增、修改、删除事项、长目标或画像时先创建确认门禁
提交手动确认决定 POST /api/v1/write-requests/{write_request_id}/decide 前端交互与展示模块 全局数据模块 decisionidempotency_key WriteDecisionResult 确认时写入 手动 CRUD 与 AI 写入走同一套确认和校验流程

7.2 WebSocket API

统一连接地址:

WebSocket /ws/v1/conversation
WS 事件 调用方 承接模块 payload 入参 payload 出参 是否写入 说明
dialogue.input 前端交互与展示模块 母 AI 编排模块 WsEnvelope.payload dialogue.decision / dialogue.clarification / dialogue.error 条件写入对话消息 用户输入进入母 AI
dialogue.answer 前端交互与展示模块 母 AI 编排模块 WsEnvelope.payload dialogue.decision / dialogue.clarification / dialogue.error 条件写入对话消息 用户回答反问
selection.submit 前端交互与展示模块 母 AI 编排模块 interaction_idselected_candidate_ids execution.processing / write.confirmation_required / execution.result 用户从候选对象中选择
selection.cancel 前端交互与展示模块 母 AI 编排模块 interaction_id execution.result 取消中间选择
write.decide 前端交互与展示模块 母 AI 编排模块 write_request_iddecision=confirmed/rejectedidempotency_key write.applied / execution.result / execution.error 确认时写入 用户确认或拒绝写入
transport.resume 前端交互与展示模块 母 AI 编排模块 last_seen_message_id transport.snapshot 断线恢复

说明:client_message_idreply_to_question_idsource_message_idtrace_id 统一放在 WsEnvelope.payload 中,不再拆到顶层字段。

8. 模块之间函数级接口

8.1 母 AI 编排模块

函数名称 调用方 入参 出参 依赖模块 是否写入 说明
处理对话输入 WS 网关 WsEnvelope MainAgentEvent 对象存储、OCR 识别模块、ASR 语音识别模块、全局数据模块、LLM 管理模块 是,对话消息 归一化输入、保存消息、读取最近 20 条对话、识别意图、解析参数
处理反问回答 WS 网关 WsEnvelope MainAgentEvent 全局数据模块、LLM 管理模块 是,对话消息 根据问题 ID 恢复追问语义,校验回答是否有效
生成反问请求 母 AI 编排模块 missing_fieldsraw_inputrecent_messagestarget_agent_nametarget_function_name ClarificationRequest LLM 管理模块 参数缺失或答非所问时返回追问
输出执行层输入 母 AI 编排模块 target_agent_nametarget_function_nameparsed_paramstime_rangeraw_inputsource_message_idrecent_messages ExecutionLayerInput 感知阶段到执行阶段的唯一交接对象
执行主编排流程 母 AI 编排模块 ExecutionLayerInput MainAgentEvent 全局数据模块、目标子 Agent、LLM 管理模块 条件写入 查询事实、指代消解、调用子 Agent、处理返回
执行范围内指代消解 母 AI 编排模块 raw_inputcandidate_itemstime_rangerecent_messages ScopeResolutionResult LLM 管理模块 只在候选事实范围内判断 singlerangeambiguousnot_found
调用目标子 Agent 母 AI 编排模块 target_agent_nametarget_function_nameSubAgentInputBase AgentResponseEnvelope 目标子 Agent 程序按注册表确定性分发,不让模型直接执行函数
创建待确认写入请求 母 AI 编排模块 source_message_iddb_actionpreview_payload write_request_idconfirmation_payload 全局数据模块 是,写入请求 子 Agent 返回写入候选后创建确认门禁
处理写入确认决定 WS 网关 write_request_iddecisionidempotency_key WriteDecisionResult 全局数据模块 确认时写入 用户确认后由数据模块事务落盘

8.2 子 Agent

模块 函数名称 调用方 入参 出参 是否写入 说明
日程待办 Agent 生成事项操作候选 母 AI 编排模块 user_idraw_inputtime_rangerecent_messagesaction=create/update/delete/querycandidate_items? AgentResponseEnvelope(result=item_operation_candidate, db_action?) 生成日程、待办、子任务基础 CRUD 候选
反馈 Agent 生成执行反馈候选 母 AI 编排模块 user_idraw_inputtime_rangerecent_messagescandidate_items AgentResponseEnvelope(result=feedback_candidate, db_action) 在候选范围内定位目标事项并规范化反馈
重排 Agent 生成重排方案 母 AI 编排模块 user_idraw_inputtime_rangerecent_messagesfuture_itemsunfinished_feedbacksuser_profile?task_profile_snapshot? AgentResponseEnvelope(result=replan_candidate, db_action) 生成方案,不直接应用;任务级画像只在重排时作为提示词使用
复盘 Agent 生成复盘报告 母 AI 编排模块 user_idraw_inputtime_rangerecent_messagesreview_factsreview_type AgentResponseEnvelope(result=review_text) 基于事实数据生成文本复盘
长任务拆分 Agent 生成长任务拆分候选 母 AI 编排模块 user_idraw_inputtime_rangerecent_messagesuser_profile? AgentResponseEnvelope(result=GoalSplitCandidate, db_action) 输出宏观规划文本、近期子任务候选和任务级画像总结;确认后与长目标创建数据一并落盘

子 Agent 入参基线:

子 Agent 必填参数 条件参数 说明
日程待办 Agent user_idraw_inputtime_rangerecent_messagesparsed_params.action candidate_items create 不需要候选事项;update/delete/query 需要执行层先粗筛候选
反馈 Agent user_idraw_inputtime_rangerecent_messagescandidate_items 反馈目标必须来自执行层提供的候选事实,子 Agent 不查库
重排 Agent user_idraw_inputtime_rangerecent_messagesfuture_itemsunfinished_feedbacks user_profiletask_profile_snapshot time_range 用于限定重排影响范围,未来事项由执行层查询;任务级画像只读取已落盘快照
复盘 Agent user_idraw_inputtime_rangerecent_messagesreview_factsreview_type long_goal_id 复盘只基于执行层给定事实生成文本,不默认写库
长任务拆分 Agent user_idraw_inputtime_rangerecent_messagesuser_profile 输出 plan_overview、近期子任务候选和 task_profile_summary;长期宏观规划与任务级画像在确认后同步落盘

子 Agent 出参统一使用 AgentResponseEnvelope。长任务拆分 Agent 还必须额外返回 task_profile_summary,用于在创建长目标时与长目标数据一并落盘。如果 db_action 非空,则 isNeedUser 必须为 true;否则母 AI 编排模块直接拦截为 execution.error

8.3 公共基础模块

模块 函数名称 调用方 入参 出参 是否写入 说明
全局数据模块 保存对话消息 母 AI 编排模块 user_idrolemodalityraw_contentsource_urlmetadata ConversationMessageDTO 保存用户输入、系统追问和主要结果消息
全局数据模块 查询最近对话记录 母 AI 编排模块 user_idlimit=20before_message_id? list[ConversationMessageDTO] 给母 AI 和子 Agent 提供对话连续性
全局数据模块 查询事项列表 母 AI 编排模块、基础业务模块 user_idtime_rangeitem_type?status? list[TaskItemDTO] 用于候选定位、重排、复盘和页面展示
全局数据模块 查询长目标及子任务 母 AI 编排模块、基础业务模块 user_idlong_goal_id LongGoalDTOsubtasks 用于长任务拆分、重排和详情页
全局数据模块 查询反馈记录 母 AI 编排模块 user_idtime_rangetarget_item_ids? list[FeedbackDTO] 用于重排和复盘
全局数据模块 查询全局用户画像 母 AI 编排模块、全局用户画像管理模块 user_id UserProfileDTO 用于长任务拆分和重排
全局数据模块 查询任务级画像 母 AI 编排模块、任务级画像管理模块 user_idlong_goal_id TaskProfileSnapshot? 用于重排阶段读取已落盘快照
全局数据模块 创建写入请求 母 AI 编排模块 source_message_idactiontarget_typepayloadpreview_text write_request_idpayload_hash 写入确认门禁
全局数据模块 应用已确认写入 母 AI 编排模块 write_request_iddecisionidempotency_keyconfirmed_by WriteDecisionResult 校验确认后事务写入业务事实
LLM 管理模块 调用结构化模型 母 AI 编排模块、子 Agent prompt_namemessagesoutput_schemamodel_config? structured_outputtrace_id 所有模型调用统一入口
LLM 管理模块 渲染子 Agent 能力提示词 母 AI 编排模块 list[AgentFunctionDefinition] prompt_fragment 程序维护能力表,渲染注入系统提示词
对象存储 上传原始媒体文件 前端交互与展示模块 user_idfilefile_type source_urlobject_key 是,对象存储 不做识别和业务理解
OCR 识别模块 识别图片文本 母 AI 编排模块 source_url recognized_textstatus 图片到文本
ASR 语音识别模块 识别语音文本 母 AI 编排模块 source_url recognized_textstatus 语音到文本
用户账号模块 获取当前用户 前端交互与展示模块、母 AI 编排模块 登录凭证 user_idbusiness_user_id 鉴权和用户归属
全局用户画像管理模块 更新全局用户画像 前端交互与展示模块 user_id、画像字段 UserProfileDTO 用户主动维护全局画像
任务级画像管理模块 查询任务级画像快照 母 AI 编排模块、重排 Agent user_idlong_goal_id TaskProfileSnapshot 任务级画像只在重排时使用,作为提示词输入
任务级画像管理模块 保存任务级画像快照 母 AI 编排模块 user_idlong_goal_idsource_message_idprofile_summaryversion TaskProfileSnapshot 长任务拆分确认落盘时,与长目标创建数据同步写入

9. 完整业务链路时序图

sequenceDiagram
    autonumber

    participant U as 用户
    participant FE as 前端交互与展示模块
    participant OSS as 对象存储
    participant WS as WebSocket
    participant MA as 母AI编排模块
    participant ASR as ASR语音识别模块
    participant OCR as OCR识别模块
    participant DATA as 全局数据模块
    participant LLM as LLM管理模块
    participant SA as 目标子Agent

    alt 文本输入
        U->>FE: 输入文本
        FE->>WS: dialogue.input(payload)
    else 语音输入
        U->>FE: 输入语音
        FE->>OSS: HTTP 上传音频
        OSS-->>FE: source_url
        FE->>WS: dialogue.input(payload)
    else 图片输入
        U->>FE: 上传图片
        FE->>OSS: HTTP 上传图片
        OSS-->>FE: source_url
        FE->>WS: dialogue.input(payload)
    end

    WS->>MA: 转发输入事件

    alt modality=voice_asr
        MA->>ASR: 识别语音(source_url)
        ASR-->>MA: recognized_text
    else modality=image_ocr
        MA->>OCR: 识别图片(source_url)
        OCR-->>MA: recognized_text
    else modality=text
        MA->>MA: raw_content 作为 normalized_text
    end

    MA->>DATA: 保存对话消息(normalized_text, source_url)
    DATA-->>MA: source_message_id
    MA->>DATA: 查询最近20条对话(user_id)
    DATA-->>MA: recent_messages
    MA->>LLM: 意图识别 + 参数解析(能力表, normalized_text, recent_messages)
    LLM-->>MA: target_agent_name, target_function_name, parsed_params, time_range, missing_fields

    alt 长任务拆分且无可复用问答
        MA->>LLM: 生成长任务拆分问题
        LLM-->>MA: ClarificationRequest(mode=batch)
        MA-->>WS: dialogue.clarification
        WS-->>FE: 一次性展示拆分问题
    else 参数缺失或回答无效
        MA->>LLM: 生成反问(missing_fields)
        LLM-->>MA: ClarificationRequest
        MA-->>WS: dialogue.clarification
        WS-->>FE: 展示追问
    else 参数完整
        MA-->>WS: dialogue.decision
        MA-->>WS: execution.processing
        MA->>DATA: 按 time_range 查询候选事实
        DATA-->>MA: candidate_facts
        opt 存在候选歧义
            MA->>LLM: 范围内指代消解(candidate_facts)
            LLM-->>MA: single / range / ambiguous / not_found
        end

        alt 仍需用户选择
            MA-->>WS: selection.required
            WS-->>FE: 展示候选
            U->>FE: 选择候选
            FE->>WS: selection.submit
            WS->>MA: 继续执行编排
        else 可调用子Agent
            MA->>SA: 调用目标函数(SubAgentInput)
            SA-->>MA: AgentResponseEnvelope
            alt 仅展示
                MA-->>WS: execution.result
                WS-->>FE: 展示结果
            else 需要写入确认
                MA->>DATA: 创建写入请求(db_action)
                DATA-->>MA: write_request_id
                MA-->>WS: write.confirmation_required
                WS-->>FE: 展示确认卡片
                U->>FE: 确认或拒绝
                FE->>WS: write.decide
                WS->>MA: 处理确认决定
                MA->>DATA: 应用已确认写入
                DATA-->>MA: WriteDecisionResult
                MA-->>WS: write.applied / execution.result
                WS-->>FE: 展示最终结果
            end
        end
    end
Loading

10. MVP 最小业务闭环数据库设计

10.1 保留表

表名 用途 保留原因
users 用户账号 登录、用户归属、业务用户唯一 ID
user_profiles 全局用户画像 长任务拆分和重排需要读取
long_goals 长目标 长任务拆分、目标复盘、任务级画像归属
task_items 统一事项表 保存日程、待办、长目标子任务;其中只有 subtask 关联长目标
conversation_messages 对话消息 保存文本、ASR 文本、OCR 文本和原始媒体 URL
feedback_records 执行反馈 反馈、重排、复盘的事实来源
task_profile_snapshots 任务级画像 长任务拆分时写入的当前快照
write_requests 待确认写入请求 所有 AI 写入候选的确认门禁
operation_confirmations 用户确认记录 记录用户确认或拒绝

10.2 暂不保留表

表名 处理方式 原因
agent_run_records MVP 暂不实现 用户明确系统审计模块先不实现
audit_events MVP 暂不实现 用户明确系统审计模块先不实现
task_profile_observations 暂不保留 任务级画像只保留长任务创建时写入的当前快照,减少冗余来源表
replan_proposals 暂不保留 重排候选可存入 write_requests.payload,确认后直接更新事项
replan_proposal_changes 暂不保留 变更列表作为写入请求 payload 的一部分
replan_applications 暂不保留 应用结果由 write_requests 和业务表状态体现

10.3 核心表字段

users

字段 类型 必填 说明
id uuid 主键
business_user_id text 业务生成的用户唯一 ID
email text 登录邮箱,唯一
password_hash text 密码哈希
display_name text 昵称
status text active / disabled
is_deleted boolean 软删除标记
deleted_at timestamptz 软删除时间
created_at timestamptz 创建时间
updated_at timestamptz 更新时间

user_profiles

字段 类型 必填 说明
id uuid 主键
user_id uuid 用户 ID
profile_data jsonb 全局画像结构化数据
is_deleted boolean 软删除标记
created_at / updated_at timestamptz 时间字段

long_goals

字段 类型 必填 说明
id uuid 主键
user_id uuid 用户 ID
title text 长目标标题
description text 目标说明
plan_overview text 全周期粗略规划文本
start_at timestamptz 开始时间
deadline_at timestamptz 截止时间
status text active / completed / cancelled
version integer 乐观锁版本
is_deleted boolean 软删除标记
created_at / updated_at / deleted_at timestamptz 时间字段

task_items

字段 类型 必填 说明
id uuid 主键
user_id uuid 用户 ID
long_goal_id uuid subtask 允许关联长目标;scheduletodo 必须为空
item_type text schedule / todo / subtask
title text 事项标题
description text 描述
start_at timestamptz 开始时间
end_at timestamptz 结束时间
due_at timestamptz 截止时间
status text planned / in_progress / completed / cancelled / deferred
version integer 乐观锁版本
is_deleted boolean 软删除标记
created_at / updated_at / deleted_at timestamptz 时间字段

conversation_messages

字段 类型 必填 说明
id uuid 主键,作为 source_message_id 使用
user_id uuid 用户 ID
message_index integer 单一对话框内递增序号
role text user / assistant / system / tool
modality text text / voice_asr / image_ocr
raw_content text 文本原文、ASR 文本或 OCR 文本
source_url text 原始图片或音频 URL
metadata jsonb 统一消息元数据,保存消息类型、追问状态、回填关联和执行上下文
created_at timestamptz 创建时间

conversation_messages.metadata

conversation_messages.metadata 采用扁平化固定键结构,不再额外拆表。

字段 类型 必填 说明
schema_version integer 元数据版本,MVP 固定为 1
message_kind text input / clarification_question / clarification_answer / decision / execution_processing / selection_required / selection_submit / selection_cancel / execution_result / confirmation_request / confirmation_decision / system_notice / error
client_message_id text 客户端幂等标识;用户消息必填,系统消息可空
trace_id text 模型调用或链路追踪标识
question_id uuid 追问消息自身 ID;仅 clarification_question 需要
question_status text open / answered / expired
question_type text normal / long_task_split / disambiguation / confirmation
expires_at timestamptz 追问过期时间
reply_to_question_id uuid 用户回答所对应的追问消息 ID
target_agent_name text 相关子 Agent 名称
target_function_name text 相关子 Agent 函数名
missing_fields jsonb 仍缺失的字段名列表
parsed_params jsonb 已解析出的结构化参数
time_range jsonb 结构化时间范围
write_request_id uuid 写入请求 ID
decision text confirmed / rejected

一致性规则:

场景 规则
元数据类型来源 message_kind 由 WS type 或后端事件类型派生,不能手填成另一个语义
用户首次输入 message_kind=inputclient_message_id 必填
生成反问 message_kind=clarification_questionquestion_id 必填且等于该消息的 conversation_messages.idquestion_status=openexpires_at 必填
用户回答反问 message_kind=clarification_answerreply_to_question_id 必填且必须指向一条 question_status=openclarification_question
反问被消费 服务端将原 clarification_question.question_status 更新为 answered
反问过期 服务端将原 clarification_question.question_status 更新为 expired
执行层结果 message_kind=decision / execution_result / confirmation_request / confirmation_decision 之一,按结果填充对应字段

示例:

{
  "schema_version": 1,
  "message_kind": "clarification_question",
  "client_message_id": "c_001",
  "question_id": "b7f0c1d8-8c7a-4a75-9f63-9d2b8be6b2b1",
  "question_status": "open",
  "question_type": "long_task_split",
  "expires_at": "2026-07-23T18:00:00+08:00",
  "target_agent_name": "long_task_split_agent",
  "target_function_name": "生成长任务拆分候选",
  "missing_fields": ["deadline_at", "duration_minutes"]
}
{
  "schema_version": 1,
  "message_kind": "clarification_answer",
  "client_message_id": "c_002",
  "reply_to_question_id": "b7f0c1d8-8c7a-4a75-9f63-9d2b8be6b2b1",
  "parsed_params": {
    "deadline_at": "2026-08-01T18:00:00+08:00",
    "duration_minutes": 120
  }
}

feedback_records

字段 类型 必填 说明
id uuid 主键
user_id uuid 用户 ID
task_item_id uuid 反馈目标事项
source_message_id uuid 来源对话消息
feedback_status text completed / not_completed / partially_completed / deferred / cancelled
normalized_feedback text 规范化反馈文本
duration_minutes integer 实际耗时
occurred_at timestamptz 反馈发生时间
is_deleted boolean 软删除标记
created_at / updated_at / deleted_at timestamptz 时间字段

task_profile_snapshots

字段 类型 必填 说明
id uuid 主键
user_id uuid 用户 ID
long_goal_id uuid 长目标 ID
profile_summary text 长任务拆分 Agent 在创建长任务时输出的任务级画像总结
source_message_id uuid 创建该长目标的来源对话消息
version integer 版本
is_deleted boolean 软删除标记
created_at / updated_at / deleted_at timestamptz 时间字段

write_requests

字段 类型 必填 说明
id uuid 主键
user_id uuid 用户 ID
source_message_id uuid 来源消息
action text create / update / delete / feedback / replan / goal_split
target_type text task_item / long_goal / feedback / profile
target_id uuid 目标对象 ID
payload jsonb 待确认写入载荷
payload_hash text 防止前端篡改载荷
status text pending / confirmed / rejected / applied / expired
expires_at timestamptz 过期时间
created_at / updated_at timestamptz 时间字段

operation_confirmations

字段 类型 必填 说明
id uuid 主键
write_request_id uuid 写入请求 ID
user_id uuid 用户 ID
decision text confirmed / rejected
idempotency_key text 确认事件幂等键
created_at timestamptz 确认时间

##sql脚本

-- TimeFlow MVP minimal PostgreSQL schema
-- Step 1: create database manually if needed
--   CREATE DATABASE timeflow;
-- Step 2: connect to the timeflow database, then run this script.

CREATE SCHEMA IF NOT EXISTS timeflow;
SET search_path TO timeflow;

CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- -----------------------------
-- Shared helper
-- -----------------------------

CREATE OR REPLACE FUNCTION timeflow.set_write_request_payload_hash()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    NEW.payload_hash := encode(digest(COALESCE(NEW.payload::text, '{}'), 'sha256'), 'hex');
    RETURN NEW;
END;
$$;

-- -----------------------------
-- Core tables
-- -----------------------------

CREATE TABLE IF NOT EXISTS users (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    business_user_id text NOT NULL,
    email text NOT NULL,
    password_hash text NOT NULL,
    display_name text,
    status text NOT NULL DEFAULT 'active',
    is_deleted boolean NOT NULL DEFAULT false,
    deleted_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT users_business_user_id_uq UNIQUE (business_user_id),
    CONSTRAINT users_email_uq UNIQUE (email),
    CONSTRAINT users_status_check CHECK (status IN ('active', 'disabled')),
    CONSTRAINT users_soft_delete_check CHECK (
        (is_deleted = false AND deleted_at IS NULL)
        OR (is_deleted = true AND deleted_at IS NOT NULL)
    )
);

CREATE TABLE IF NOT EXISTS user_profiles (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL,
    profile_data jsonb NOT NULL DEFAULT '{}'::jsonb,
    version integer NOT NULL DEFAULT 1,
    is_deleted boolean NOT NULL DEFAULT false,
    deleted_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT user_profiles_user_fk FOREIGN KEY (user_id)
        REFERENCES users (id) ON DELETE CASCADE,
    CONSTRAINT user_profiles_user_id_uq UNIQUE (user_id),
    CONSTRAINT user_profiles_profile_object CHECK (jsonb_typeof(profile_data) = 'object'),
    CONSTRAINT user_profiles_soft_delete_check CHECK (
        (is_deleted = false AND deleted_at IS NULL)
        OR (is_deleted = true AND deleted_at IS NOT NULL)
    )
);

CREATE TABLE IF NOT EXISTS long_goals (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL,
    title text NOT NULL,
    description text,
    plan_overview text,
    start_at timestamptz,
    deadline_at timestamptz,
    status text NOT NULL DEFAULT 'active',
    version integer NOT NULL DEFAULT 1,
    is_deleted boolean NOT NULL DEFAULT false,
    deleted_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT long_goals_user_fk FOREIGN KEY (user_id)
        REFERENCES users (id) ON DELETE CASCADE,
    CONSTRAINT long_goals_user_id_uq UNIQUE (user_id, id),
    CONSTRAINT long_goals_title_not_blank CHECK (length(btrim(title)) > 0),
    CONSTRAINT long_goals_status_check CHECK (status IN ('active', 'completed', 'cancelled')),
    CONSTRAINT long_goals_version_positive CHECK (version > 0),
    CONSTRAINT long_goals_soft_delete_check CHECK (
        (is_deleted = false AND deleted_at IS NULL)
        OR (is_deleted = true AND deleted_at IS NOT NULL)
    )
);

CREATE TABLE IF NOT EXISTS task_items (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL,
    long_goal_id uuid,
    item_type text NOT NULL,
    title text NOT NULL,
    description text,
    start_at timestamptz,
    end_at timestamptz,
    due_at timestamptz,
    status text NOT NULL DEFAULT 'planned',
    version integer NOT NULL DEFAULT 1,
    is_deleted boolean NOT NULL DEFAULT false,
    deleted_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT task_items_user_fk FOREIGN KEY (user_id)
        REFERENCES users (id) ON DELETE CASCADE,
    CONSTRAINT task_items_user_id_uq UNIQUE (user_id, id),
    CONSTRAINT task_items_goal_fk FOREIGN KEY (user_id, long_goal_id)
        REFERENCES long_goals (user_id, id) ON DELETE CASCADE,
    CONSTRAINT task_items_item_type_check CHECK (item_type IN ('schedule', 'todo', 'subtask')),
    CONSTRAINT task_items_status_check CHECK (status IN ('planned', 'in_progress', 'completed', 'cancelled', 'deferred')),
    CONSTRAINT task_items_title_not_blank CHECK (length(btrim(title)) > 0),
    CONSTRAINT task_items_version_positive CHECK (version > 0),
    CONSTRAINT task_items_soft_delete_check CHECK (
        (is_deleted = false AND deleted_at IS NULL)
        OR (is_deleted = true AND deleted_at IS NOT NULL)
    ),
    CONSTRAINT task_items_long_goal_binding_check CHECK (
        (item_type = 'subtask' AND long_goal_id IS NOT NULL)
        OR (item_type IN ('schedule', 'todo') AND long_goal_id IS NULL)
    )
);

CREATE TABLE IF NOT EXISTS conversation_messages (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL,
    message_index integer NOT NULL,
    role text NOT NULL,
    modality text,
    raw_content text NOT NULL,
    source_url text,
    metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT conversation_messages_user_fk FOREIGN KEY (user_id)
        REFERENCES users (id) ON DELETE CASCADE,
    CONSTRAINT conversation_messages_user_id_uq UNIQUE (user_id, id),
    CONSTRAINT conversation_messages_index_positive CHECK (message_index > 0),
    CONSTRAINT conversation_messages_user_index_uq UNIQUE (user_id, message_index),
    CONSTRAINT conversation_messages_role_check CHECK (role IN ('user', 'assistant', 'system', 'tool')),
    CONSTRAINT conversation_messages_modality_check CHECK (modality IS NULL OR modality IN ('text', 'voice_asr', 'image_ocr')),
    CONSTRAINT conversation_messages_source_url_check CHECK (
        (modality = 'text' AND source_url IS NULL)
        OR (modality IN ('voice_asr', 'image_ocr') AND source_url IS NOT NULL)
        OR (modality IS NULL AND source_url IS NULL)
    ),
    CONSTRAINT conversation_messages_metadata_object CHECK (jsonb_typeof(metadata) = 'object')
);

COMMENT ON COLUMN conversation_messages.metadata IS
'扁平化 JSON 元数据,保存 schema_version、message_kind、client_message_id、question_id、question_status、question_type、expires_at、reply_to_question_id、target_agent_name、target_function_name、missing_fields、parsed_params、time_range、write_request_id、decision 等固定键。';

CREATE TABLE IF NOT EXISTS feedback_records (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL,
    task_item_id uuid NOT NULL,
    source_message_id uuid,
    feedback_status text NOT NULL,
    normalized_feedback text NOT NULL,
    duration_minutes integer,
    occurred_at timestamptz NOT NULL,
    is_deleted boolean NOT NULL DEFAULT false,
    deleted_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT feedback_records_user_fk FOREIGN KEY (user_id)
        REFERENCES users (id) ON DELETE CASCADE,
    CONSTRAINT feedback_records_user_id_uq UNIQUE (user_id, id),
    CONSTRAINT feedback_records_task_fk FOREIGN KEY (user_id, task_item_id)
        REFERENCES task_items (user_id, id) ON DELETE RESTRICT,
    CONSTRAINT feedback_records_message_fk FOREIGN KEY (user_id, source_message_id)
        REFERENCES conversation_messages (user_id, id) ON DELETE SET NULL,
    CONSTRAINT feedback_records_status_check CHECK (
        feedback_status IN ('completed', 'not_completed', 'partially_completed', 'deferred', 'cancelled')
    ),
    CONSTRAINT feedback_records_duration_check CHECK (duration_minutes IS NULL OR duration_minutes >= 0),
    CONSTRAINT feedback_records_soft_delete_check CHECK (
        (is_deleted = false AND deleted_at IS NULL)
        OR (is_deleted = true AND deleted_at IS NOT NULL)
    )
);

CREATE TABLE IF NOT EXISTS task_profile_snapshots (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL,
    long_goal_id uuid NOT NULL,
    profile_summary text NOT NULL,
    source_message_id uuid NOT NULL,
    version integer NOT NULL DEFAULT 1,
    is_deleted boolean NOT NULL DEFAULT false,
    deleted_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT task_profile_snapshots_user_fk FOREIGN KEY (user_id)
        REFERENCES users (id) ON DELETE CASCADE,
    CONSTRAINT task_profile_snapshots_user_goal_uq UNIQUE (user_id, long_goal_id),
    CONSTRAINT task_profile_snapshots_goal_fk FOREIGN KEY (user_id, long_goal_id)
        REFERENCES long_goals (user_id, id) ON DELETE RESTRICT,
    CONSTRAINT task_profile_snapshots_message_fk FOREIGN KEY (user_id, source_message_id)
        REFERENCES conversation_messages (user_id, id) ON DELETE RESTRICT,
    CONSTRAINT task_profile_snapshots_version_positive CHECK (version > 0),
    CONSTRAINT task_profile_snapshots_soft_delete_check CHECK (
        (is_deleted = false AND deleted_at IS NULL)
        OR (is_deleted = true AND deleted_at IS NOT NULL)
    )
);

CREATE TABLE IF NOT EXISTS write_requests (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL,
    source_message_id uuid,
    action text NOT NULL,
    target_type text NOT NULL,
    target_id uuid,
    payload jsonb NOT NULL DEFAULT '{}'::jsonb,
    payload_hash text NOT NULL DEFAULT '',
    preview_text text NOT NULL,
    status text NOT NULL DEFAULT 'pending',
    idempotency_key text NOT NULL,
    base_version integer,
    expires_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT write_requests_user_fk FOREIGN KEY (user_id)
        REFERENCES users (id) ON DELETE CASCADE,
    CONSTRAINT write_requests_user_id_uq UNIQUE (user_id, id),
    CONSTRAINT write_requests_message_fk FOREIGN KEY (user_id, source_message_id)
        REFERENCES conversation_messages (user_id, id) ON DELETE SET NULL,
    CONSTRAINT write_requests_action_check CHECK (action IN ('create', 'update', 'delete', 'feedback', 'replan', 'goal_split')),
    CONSTRAINT write_requests_target_type_check CHECK (target_type IN ('task_item', 'long_goal', 'feedback', 'profile')),
    CONSTRAINT write_requests_payload_object CHECK (jsonb_typeof(payload) IN ('object', 'array')),
    CONSTRAINT write_requests_preview_not_blank CHECK (length(btrim(preview_text)) > 0),
    CONSTRAINT write_requests_status_check CHECK (status IN ('pending', 'confirmed', 'rejected', 'applied', 'expired')),
    CONSTRAINT write_requests_idempotency_not_blank CHECK (length(btrim(idempotency_key)) > 0),
    CONSTRAINT write_requests_base_version_positive CHECK (base_version IS NULL OR base_version > 0)
);

CREATE UNIQUE INDEX IF NOT EXISTS write_requests_user_idempotency_uq
    ON write_requests (user_id, idempotency_key);

DROP TRIGGER IF EXISTS write_requests_payload_hash ON write_requests;
CREATE TRIGGER write_requests_payload_hash
BEFORE INSERT OR UPDATE OF payload ON write_requests
FOR EACH ROW EXECUTE FUNCTION timeflow.set_write_request_payload_hash();

CREATE TABLE IF NOT EXISTS operation_confirmations (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL,
    write_request_id uuid NOT NULL,
    decision text NOT NULL,
    idempotency_key text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT operation_confirmations_user_fk FOREIGN KEY (user_id)
        REFERENCES users (id) ON DELETE CASCADE,
    CONSTRAINT operation_confirmations_user_id_uq UNIQUE (user_id, id),
    CONSTRAINT operation_confirmations_request_fk FOREIGN KEY (user_id, write_request_id)
        REFERENCES write_requests (user_id, id) ON DELETE RESTRICT,
    CONSTRAINT operation_confirmations_decision_check CHECK (decision IN ('confirmed', 'rejected')),
    CONSTRAINT operation_confirmations_idempotency_not_blank CHECK (length(btrim(idempotency_key)) > 0),
    CONSTRAINT operation_confirmations_user_request_uq UNIQUE (user_id, write_request_id)
);

-- -----------------------------
-- Indexes
-- -----------------------------

CREATE INDEX IF NOT EXISTS user_profiles_user_idx
    ON user_profiles (user_id);

CREATE INDEX IF NOT EXISTS long_goals_user_status_idx
    ON long_goals (user_id, status, updated_at DESC);

CREATE INDEX IF NOT EXISTS task_items_user_time_idx
    ON task_items (user_id, start_at, end_at);

CREATE INDEX IF NOT EXISTS task_items_user_type_status_idx
    ON task_items (user_id, item_type, status, updated_at DESC);

CREATE INDEX IF NOT EXISTS task_items_goal_idx
    ON task_items (user_id, long_goal_id, due_at);

CREATE INDEX IF NOT EXISTS conversation_messages_window_idx
    ON conversation_messages (user_id, message_index DESC);

CREATE INDEX IF NOT EXISTS conversation_messages_kind_idx
    ON conversation_messages (user_id, ((metadata->>'message_kind')), created_at DESC);

CREATE UNIQUE INDEX IF NOT EXISTS conversation_messages_client_message_uq
    ON conversation_messages (user_id, ((metadata->>'client_message_id')))
    WHERE (metadata->>'client_message_id') IS NOT NULL;

CREATE INDEX IF NOT EXISTS conversation_messages_question_idx
    ON conversation_messages (user_id, ((metadata->>'question_id')))
    WHERE (metadata->>'question_id') IS NOT NULL;

CREATE INDEX IF NOT EXISTS conversation_messages_reply_question_idx
    ON conversation_messages (user_id, ((metadata->>'reply_to_question_id')))
    WHERE (metadata->>'reply_to_question_id') IS NOT NULL;

CREATE INDEX IF NOT EXISTS conversation_messages_question_status_idx
    ON conversation_messages (user_id, ((metadata->>'question_status')), created_at DESC)
    WHERE (metadata->>'question_status') IS NOT NULL;

CREATE INDEX IF NOT EXISTS feedback_records_user_time_idx
    ON feedback_records (user_id, occurred_at DESC);

CREATE INDEX IF NOT EXISTS task_profile_snapshots_goal_time_idx
    ON task_profile_snapshots (user_id, long_goal_id, updated_at DESC);

CREATE INDEX IF NOT EXISTS write_requests_user_status_idx
    ON write_requests (user_id, status, created_at DESC);

CREATE INDEX IF NOT EXISTS operation_confirmations_user_time_idx
    ON operation_confirmations (user_id, created_at DESC);

-- -----------------------------
-- Comments
-- -----------------------------

COMMENT ON TABLE conversation_messages IS
'保存对话消息、ASR 文本、OCR 文本和原始媒体 URL;id 即 source_message_id,metadata 保存消息状态机。';

COMMENT ON TABLE write_requests IS
'保存所有需要用户确认的 AI 或手动写入候选。';

COMMENT ON TABLE task_profile_snapshots IS
'保存长任务拆分时写入的任务级画像当前快照。';

Clone this wiki locally