-
Notifications
You must be signed in to change notification settings - Fork 6
Architecture interface design(draft)
hqy edited this page Jul 23, 2026
·
4 revisions
| 模块类型 | 标准模块名 | 本次接口合并处理 |
|---|---|---|
| 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、展示结果和确认卡片 |
合并后,母 AI 编排模块只有一个模块,不再把“感知层”和“执行层”作为外部并列模块。
内部阶段如下:
| 阶段 | 职责 | 输入 | 输出 | 禁止行为 |
|---|---|---|---|---|
| 输入理解阶段 | 接收用户输入、归一化文本、读取最近 20 条对话、识别意图、解析参数、触发反问 | WsEnvelope(dialogue.input / dialogue.answer) |
PerceptionDecision 或 ClarificationRequest
|
不查询业务候选事项,不调用子 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| 场景 | 处理规则 | 输出 |
|---|---|---|
| 普通任务参数完整 | 根据子 Agent 能力表识别 target_agent_name、target_function_name,解析参数并抽取顶层 time_range
|
ExecutionLayerInput |
| 普通任务参数缺失 | 不进入执行编排阶段,先生成反问 | dialogue.clarification |
| 用户回答反问 | 通过 reply_to_question_id 恢复上一轮问题;如果回答无效,继续反问;如果无法关联,退化为新的普通输入 |
dialogue.decision 或 dialogue.clarification
|
| 长任务拆分 | 命中长任务拆分后先检查最近 20 条对话是否已有可复用问答;没有则一次性生成本次拆分所需问题 |
dialogue.clarification(mode=batch) 或 ExecutionLayerInput
|
| 场景 | 处理规则 | 输出 |
|---|---|---|
| 创建类函数 | 不需要定位已有事项,可直接装配上下文并调用子 Agent | AgentResponseEnvelope |
| 更新 / 删除 / 反馈类函数 | 先按 time_range 和结构化参数粗筛数据库候选,再做范围内指代消解 |
single / range / ambiguous / not_found
|
| 候选唯一 | 直接装配子 Agent 入参 | SubAgentInputBase |
| 候选不唯一 | 通过 WS 返回候选选择,不调用子 Agent | selection.required |
| 子 Agent 返回写入候选 | 创建 write_requests,前端确认后再落盘 |
write.confirmation_required |
所有 WS 消息统一使用同一信封,所有业务字段和关联字段全部放入 payload。
class WsEnvelope(BaseModel):
type: str
timestamp: datetime
payload: dict[str, Any]字段说明:
| 字段 | 用途 | 生成方 |
|---|---|---|
type |
消息类型,决定进入哪条流程 | 前端或后端 |
timestamp |
消息产生时间 | 消息发送方 |
payload |
当前消息的业务数据与关联字段 | 消息发送方 |
| 类型 | 方向 | 所属阶段 | 说明 |
|---|---|---|---|
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
|
双向 | 传输层 | 断线恢复 |
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 的结果引用,或者作为主流程链路中的外部引用字段单独消费,避免和追问状态重复表达。
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| API 名称 | 方法与路径 | 调用方 | 承接模块 | 入参 | 出参 | 是否写入 | 说明 |
|---|---|---|---|---|---|---|---|
| 注册用户 | POST /api/v1/auth/register |
前端交互与展示模块 | 用户账号模块 |
email、password、display_name
|
user_id、business_user_id、access_token
|
是 | 创建用户账号,不进入母 AI |
| 登录用户 | POST /api/v1/auth/login |
前端交互与展示模块 | 用户账号模块 |
email、password
|
user_id、business_user_id、access_token
|
否 | 建立登录态 |
| 获取当前用户 | GET /api/v1/auth/me |
前端交互与展示模块 | 用户账号模块 | 登录凭证 |
user_id、business_user_id、display_name
|
否 | 页面初始化使用 |
| 上传原始媒体文件 | POST /api/v1/media/upload |
前端交互与展示模块 | 对象存储 |
file、file_type=image/audio
|
source_url、object_key
|
是,写对象存储 | 只保存原始文件,不做 OCR / ASR |
| 查询会话消息 | GET /api/v1/conversation/messages |
前端交互与展示模块 | 全局数据模块 |
user_id、before_message_id、limit
|
messages: list[ConversationMessageDTO]、has_more
|
否 | 用于聊天页历史展示 |
| 恢复会话快照 | GET /api/v1/conversation/snapshot |
前端交互与展示模块 | 母 AI 编排模块 |
user_id、last_seen_message_id
|
recent_messages、pending_clarification、last_event
|
否 | WS 断线恢复 |
| 查询时间顺序事项 | GET /api/v1/items/timeline |
前端交互与展示模块 | 基础业务模块 |
user_id、start_at、end_at、item_type?
|
items: list[TaskItemDTO] |
否 | 时间顺序 Tab 展示 |
| 查询长目标列表 | GET /api/v1/long-goals |
前端交互与展示模块 | 基础业务模块 |
user_id、status?
|
goals: list[LongGoalDTO] |
否 | 长任务目标管理 Tab |
| 查询长目标详情 | GET /api/v1/long-goals/{goal_id} |
前端交互与展示模块 | 基础业务模块 |
user_id、goal_id
|
goal、subtasks、task_profile
|
否 | 长目标详情展示 |
| 查询全局用户画像 | GET /api/v1/user/profile |
前端交互与展示模块 | 全局用户画像管理模块 | user_id |
UserProfileDTO |
否 | 个人主页展示 |
| 创建手动写入请求 | POST /api/v1/write-requests |
前端交互与展示模块 | 全局数据模块 |
action、target_type、target_id?、payload、preview_text
|
write_request_id、payload_hash、confirmation_payload
|
是,写入请求 | 手动新增、修改、删除事项、长目标或画像时先创建确认门禁 |
| 提交手动确认决定 | POST /api/v1/write-requests/{write_request_id}/decide |
前端交互与展示模块 | 全局数据模块 |
decision、idempotency_key
|
WriteDecisionResult |
确认时写入 | 手动 CRUD 与 AI 写入走同一套确认和校验流程 |
统一连接地址:
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_id、selected_candidate_ids
|
execution.processing / write.confirmation_required / execution.result
|
否 | 用户从候选对象中选择 |
selection.cancel |
前端交互与展示模块 | 母 AI 编排模块 | interaction_id |
execution.result |
否 | 取消中间选择 |
write.decide |
前端交互与展示模块 | 母 AI 编排模块 |
write_request_id、decision=confirmed/rejected、idempotency_key
|
write.applied / execution.result / execution.error
|
确认时写入 | 用户确认或拒绝写入 |
transport.resume |
前端交互与展示模块 | 母 AI 编排模块 | last_seen_message_id |
transport.snapshot |
否 | 断线恢复 |
说明:client_message_id、reply_to_question_id、source_message_id、trace_id 统一放在 WsEnvelope.payload 中,不再拆到顶层字段。
| 函数名称 | 调用方 | 入参 | 出参 | 依赖模块 | 是否写入 | 说明 |
|---|---|---|---|---|---|---|
| 处理对话输入 | WS 网关 | WsEnvelope |
MainAgentEvent |
对象存储、OCR 识别模块、ASR 语音识别模块、全局数据模块、LLM 管理模块 | 是,对话消息 | 归一化输入、保存消息、读取最近 20 条对话、识别意图、解析参数 |
| 处理反问回答 | WS 网关 | WsEnvelope |
MainAgentEvent |
全局数据模块、LLM 管理模块 | 是,对话消息 | 根据问题 ID 恢复追问语义,校验回答是否有效 |
| 生成反问请求 | 母 AI 编排模块 |
missing_fields、raw_input、recent_messages、target_agent_name、target_function_name
|
ClarificationRequest |
LLM 管理模块 | 否 | 参数缺失或答非所问时返回追问 |
| 输出执行层输入 | 母 AI 编排模块 |
target_agent_name、target_function_name、parsed_params、time_range、raw_input、source_message_id、recent_messages
|
ExecutionLayerInput |
无 | 否 | 感知阶段到执行阶段的唯一交接对象 |
| 执行主编排流程 | 母 AI 编排模块 | ExecutionLayerInput |
MainAgentEvent |
全局数据模块、目标子 Agent、LLM 管理模块 | 条件写入 | 查询事实、指代消解、调用子 Agent、处理返回 |
| 执行范围内指代消解 | 母 AI 编排模块 |
raw_input、candidate_items、time_range、recent_messages
|
ScopeResolutionResult |
LLM 管理模块 | 否 | 只在候选事实范围内判断 single、range、ambiguous、not_found
|
| 调用目标子 Agent | 母 AI 编排模块 |
target_agent_name、target_function_name、SubAgentInputBase
|
AgentResponseEnvelope |
目标子 Agent | 否 | 程序按注册表确定性分发,不让模型直接执行函数 |
| 创建待确认写入请求 | 母 AI 编排模块 |
source_message_id、db_action、preview_payload
|
write_request_id、confirmation_payload
|
全局数据模块 | 是,写入请求 | 子 Agent 返回写入候选后创建确认门禁 |
| 处理写入确认决定 | WS 网关 |
write_request_id、decision、idempotency_key
|
WriteDecisionResult |
全局数据模块 | 确认时写入 | 用户确认后由数据模块事务落盘 |
| 模块 | 函数名称 | 调用方 | 入参 | 出参 | 是否写入 | 说明 |
|---|---|---|---|---|---|---|
| 日程待办 Agent | 生成事项操作候选 | 母 AI 编排模块 |
user_id、raw_input、time_range、recent_messages、action=create/update/delete/query、candidate_items?
|
AgentResponseEnvelope(result=item_operation_candidate, db_action?) |
否 | 生成日程、待办、子任务基础 CRUD 候选 |
| 反馈 Agent | 生成执行反馈候选 | 母 AI 编排模块 |
user_id、raw_input、time_range、recent_messages、candidate_items
|
AgentResponseEnvelope(result=feedback_candidate, db_action) |
否 | 在候选范围内定位目标事项并规范化反馈 |
| 重排 Agent | 生成重排方案 | 母 AI 编排模块 |
user_id、raw_input、time_range、recent_messages、future_items、unfinished_feedbacks、user_profile?、task_profile_snapshot?
|
AgentResponseEnvelope(result=replan_candidate, db_action) |
否 | 生成方案,不直接应用;任务级画像只在重排时作为提示词使用 |
| 复盘 Agent | 生成复盘报告 | 母 AI 编排模块 |
user_id、raw_input、time_range、recent_messages、review_facts、review_type
|
AgentResponseEnvelope(result=review_text) |
否 | 基于事实数据生成文本复盘 |
| 长任务拆分 Agent | 生成长任务拆分候选 | 母 AI 编排模块 |
user_id、raw_input、time_range、recent_messages、user_profile?
|
AgentResponseEnvelope(result=GoalSplitCandidate, db_action) |
否 | 输出宏观规划文本、近期子任务候选和任务级画像总结;确认后与长目标创建数据一并落盘 |
子 Agent 入参基线:
| 子 Agent | 必填参数 | 条件参数 | 说明 |
|---|---|---|---|
| 日程待办 Agent |
user_id、raw_input、time_range、recent_messages、parsed_params.action
|
candidate_items |
create 不需要候选事项;update/delete/query 需要执行层先粗筛候选 |
| 反馈 Agent |
user_id、raw_input、time_range、recent_messages、candidate_items
|
无 | 反馈目标必须来自执行层提供的候选事实,子 Agent 不查库 |
| 重排 Agent |
user_id、raw_input、time_range、recent_messages、future_items、unfinished_feedbacks
|
user_profile、task_profile_snapshot
|
time_range 用于限定重排影响范围,未来事项由执行层查询;任务级画像只读取已落盘快照 |
| 复盘 Agent |
user_id、raw_input、time_range、recent_messages、review_facts、review_type
|
long_goal_id |
复盘只基于执行层给定事实生成文本,不默认写库 |
| 长任务拆分 Agent |
user_id、raw_input、time_range、recent_messages、user_profile
|
无 | 输出 plan_overview、近期子任务候选和 task_profile_summary;长期宏观规划与任务级画像在确认后同步落盘 |
子 Agent 出参统一使用 AgentResponseEnvelope。长任务拆分 Agent 还必须额外返回 task_profile_summary,用于在创建长目标时与长目标数据一并落盘。如果 db_action 非空,则 isNeedUser 必须为 true;否则母 AI 编排模块直接拦截为 execution.error。
| 模块 | 函数名称 | 调用方 | 入参 | 出参 | 是否写入 | 说明 |
|---|---|---|---|---|---|---|
| 全局数据模块 | 保存对话消息 | 母 AI 编排模块 |
user_id、role、modality、raw_content、source_url、metadata
|
ConversationMessageDTO |
是 | 保存用户输入、系统追问和主要结果消息 |
| 全局数据模块 | 查询最近对话记录 | 母 AI 编排模块 |
user_id、limit=20、before_message_id?
|
list[ConversationMessageDTO] |
否 | 给母 AI 和子 Agent 提供对话连续性 |
| 全局数据模块 | 查询事项列表 | 母 AI 编排模块、基础业务模块 |
user_id、time_range、item_type?、status?
|
list[TaskItemDTO] |
否 | 用于候选定位、重排、复盘和页面展示 |
| 全局数据模块 | 查询长目标及子任务 | 母 AI 编排模块、基础业务模块 |
user_id、long_goal_id
|
LongGoalDTO、subtasks
|
否 | 用于长任务拆分、重排和详情页 |
| 全局数据模块 | 查询反馈记录 | 母 AI 编排模块 |
user_id、time_range、target_item_ids?
|
list[FeedbackDTO] |
否 | 用于重排和复盘 |
| 全局数据模块 | 查询全局用户画像 | 母 AI 编排模块、全局用户画像管理模块 | user_id |
UserProfileDTO |
否 | 用于长任务拆分和重排 |
| 全局数据模块 | 查询任务级画像 | 母 AI 编排模块、任务级画像管理模块 |
user_id、long_goal_id
|
TaskProfileSnapshot? |
否 | 用于重排阶段读取已落盘快照 |
| 全局数据模块 | 创建写入请求 | 母 AI 编排模块 |
source_message_id、action、target_type、payload、preview_text
|
write_request_id、payload_hash
|
是 | 写入确认门禁 |
| 全局数据模块 | 应用已确认写入 | 母 AI 编排模块 |
write_request_id、decision、idempotency_key、confirmed_by
|
WriteDecisionResult |
是 | 校验确认后事务写入业务事实 |
| LLM 管理模块 | 调用结构化模型 | 母 AI 编排模块、子 Agent |
prompt_name、messages、output_schema、model_config?
|
structured_output、trace_id
|
否 | 所有模型调用统一入口 |
| LLM 管理模块 | 渲染子 Agent 能力提示词 | 母 AI 编排模块 | list[AgentFunctionDefinition] |
prompt_fragment |
否 | 程序维护能力表,渲染注入系统提示词 |
| 对象存储 | 上传原始媒体文件 | 前端交互与展示模块 |
user_id、file、file_type
|
source_url、object_key
|
是,对象存储 | 不做识别和业务理解 |
| OCR 识别模块 | 识别图片文本 | 母 AI 编排模块 | source_url |
recognized_text、status
|
否 | 图片到文本 |
| ASR 语音识别模块 | 识别语音文本 | 母 AI 编排模块 | source_url |
recognized_text、status
|
否 | 语音到文本 |
| 用户账号模块 | 获取当前用户 | 前端交互与展示模块、母 AI 编排模块 | 登录凭证 |
user_id、business_user_id
|
否 | 鉴权和用户归属 |
| 全局用户画像管理模块 | 更新全局用户画像 | 前端交互与展示模块 |
user_id、画像字段 |
UserProfileDTO |
是 | 用户主动维护全局画像 |
| 任务级画像管理模块 | 查询任务级画像快照 | 母 AI 编排模块、重排 Agent |
user_id、long_goal_id
|
TaskProfileSnapshot |
否 | 任务级画像只在重排时使用,作为提示词输入 |
| 任务级画像管理模块 | 保存任务级画像快照 | 母 AI 编排模块 |
user_id、long_goal_id、source_message_id、profile_summary、version
|
TaskProfileSnapshot |
是 | 长任务拆分确认落盘时,与长目标创建数据同步写入 |
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
| 表名 | 用途 | 保留原因 |
|---|---|---|
users |
用户账号 | 登录、用户归属、业务用户唯一 ID |
user_profiles |
全局用户画像 | 长任务拆分和重排需要读取 |
long_goals |
长目标 | 长任务拆分、目标复盘、任务级画像归属 |
task_items |
统一事项表 | 保存日程、待办、长目标子任务;其中只有 subtask 关联长目标 |
conversation_messages |
对话消息 | 保存文本、ASR 文本、OCR 文本和原始媒体 URL |
feedback_records |
执行反馈 | 反馈、重排、复盘的事实来源 |
task_profile_snapshots |
任务级画像 | 长任务拆分时写入的当前快照 |
write_requests |
待确认写入请求 | 所有 AI 写入候选的确认门禁 |
operation_confirmations |
用户确认记录 | 记录用户确认或拒绝 |
| 表名 | 处理方式 | 原因 |
|---|---|---|
agent_run_records |
MVP 暂不实现 | 用户明确系统审计模块先不实现 |
audit_events |
MVP 暂不实现 | 用户明确系统审计模块先不实现 |
task_profile_observations |
暂不保留 | 任务级画像只保留长任务创建时写入的当前快照,减少冗余来源表 |
replan_proposals |
暂不保留 | 重排候选可存入 write_requests.payload,确认后直接更新事项 |
replan_proposal_changes |
暂不保留 | 变更列表作为写入请求 payload 的一部分 |
replan_applications |
暂不保留 | 应用结果由 write_requests 和业务表状态体现 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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 |
是 | 更新时间 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
uuid |
是 | 主键 |
user_id |
uuid |
是 | 用户 ID |
profile_data |
jsonb |
是 | 全局画像结构化数据 |
is_deleted |
boolean |
是 | 软删除标记 |
created_at / updated_at
|
timestamptz |
是 | 时间字段 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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 |
否 | 时间字段 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
uuid |
是 | 主键 |
user_id |
uuid |
是 | 用户 ID |
long_goal_id |
uuid |
否 | 仅 subtask 允许关联长目标;schedule 和 todo 必须为空 |
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 |
否 | 时间字段 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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 采用扁平化固定键结构,不再额外拆表。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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=input,client_message_id 必填 |
| 生成反问 |
message_kind=clarification_question,question_id 必填且等于该消息的 conversation_messages.id,question_status=open,expires_at 必填 |
| 用户回答反问 |
message_kind=clarification_answer,reply_to_question_id 必填且必须指向一条 question_status=open 的 clarification_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
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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 |
否 | 时间字段 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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 |
否 | 时间字段 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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 |
是 | 时间字段 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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
'保存长任务拆分时写入的任务级画像当前快照。';