# FFAI Agent 数据模型

> 事实源：`backend/prisma/schema/platform_ai.prisma`（model AgentXxx / StorageXxx + 12 个 enum）  
> 迁移：`backend/prisma/migrations/20260516085046_add_ffai_agent_module/migration.sql` + Phase 2 增量（agent_projects / agent_memories / agent_personas / agent_crons / agent_mcp_servers / session parentSessionId）  
> 本文档是**速查索引**，详细字段类型/约束/索引以 prisma schema 为准。

## 表结构（18 张）

### 业务核心表（含完整 DataScope 强制字段）

| 表 | 用途 | 关键字段 | 关键索引 |
|---|---|---|---|
| `AgentSession` | 对话会话主表 | `surface` (enum WEB/CLI/TEAMS/DESKTOP/IOS/ANDROID) / `status` (ACTIVE/CLOSED/ARCHIVED) / `planMode` (OFF/REQUIRED) / `permissionMode` (DEFAULT/BYPASS/READ_ONLY/ASK_EVERY_TIME) / `projectId` / `personaId` (M1-step2 激活 PERSONA-scope memory；FK→agent_personas ON DELETE SET NULL) / `parentSessionId` (Phase 2 多 Agent 拓扑：root=NULL；child 指向父 session，自引用 FK + onDelete:SetNull) / `closedAt` 软删 | `(organizationId, createdAt desc)` + `(createdById, createdAt desc)` + `(status)` + `(parentSessionId)` + `(personaId)` |
| `AgentArtifact` | agent 产出工件（表格/图表/文件/链接） | `type` (enum TABLE/CHART/FILE/LINK/TEXT) / `data` JSON / `previewUrl` / `mimeType` / `sizeBytes` | `(sessionId, createdAt desc)` + `(organizationId, type, createdAt desc)` |
| `AgentTaskTracker` | 子任务状态机（PR8 sub-agent 用） | `parentTaskId` / `status` (enum PENDING/IN_PROGRESS/COMPLETED/CANCELLED/FAILED) / `progress` 0-100 / `metadata` JSON | `(sessionId, status)` + `(organizationId, status, createdAt desc)` |
| `ModelRoutingRule` | per-org/user/project 路由规则 | `scope` (ORGANIZATION/USER/PROJECT) / `scopeRefId` / `priority` 升序 / `pattern` JSON / `primary` `{provider, model}` / `fallbacks` | `(organizationId, scope, enabled, priority)` |
| `AgentQuota` | per-user/org 月度配额 | `scope` (ORGANIZATION/USER) / `monthlyTokens` / `monthlyCostUsd` / `softLimitRatio` 0.8 / `enabled` | `unique(scope, scopeRefId)` + `(organizationId)` |
| `OrganizationAgentSettings` | per-org agent 配置 | `defaultPlanMode` / `defaultPermissionMode` / `disabledCapabilities[]` / `allowedCliTools[]` / `allowedPermissionModes[]` (Phase 2 字段 day-1 预留) | `unique(organizationId)` + `(organizationId)` |
| `StorageBinding` | per-org/user/project 存储后端绑定 | `scope` / `scopeRefId` / `kind` (LOCAL/ONEDRIVE/S3/GOOGLE_DRIVE) / `config` JSON / `encryptedSecret` (envelope encrypted refresh_token) / `keyId` (envelope keyId) | `(organizationId, scope, scopeRefId)` + `(kind, enabled)` |
| `AgentProject` *(Phase 2)* | ChatGPT-style 用户自创"项目工作区"（与 FFOA 业务项目解耦） | `name` (≤120) / `icon` (emoji/lucide) / `color` (hex/Tailwind 色组) / `instructions` (项目级 system prompt 注入) | `(organizationId, createdById, updatedAt desc)` |
| `AgentMemory` *(M1 三维度)* | per-user / per-org 持久记忆，每 turn 自动注入 system prompt | `content` / `ownerScope` (enum USER/ORG，M1-step4：USER 跟用户走 / ORG admin 配全员共享) / `scope` (enum GLOBAL/PROJECT/PERSONA，M1-step2 全部生效) / `category` (enum USER/FEEDBACK/PROJECT/REFERENCE，M1-step3 借鉴 CC/OC 四分类) / `projectId` (scope=PROJECT 必填) / `personaId` (scope=PERSONA 必填) / `source` ('user'/'ai-detected'/'system') / `createdById` NULL 当 ownerScope=ORG（DB CHECK 约束 `agent_memories_owner_consistency`） | `(organizationId, createdById, scope, updatedAt desc)` + `(organizationId, ownerScope, scope, updatedAt desc)` + `(projectId)` + `(personaId)` |
| `AgentPersona` *(Phase 2)* | 智能体 "你应该是谁" —— instructions / 工具白名单 / 风格 | `createdById` (NULL = 系统预设，org 内共享；非 NULL = 用户自创，仅自己可见) / `systemKey` (系统预设的稳定标识如 'code-reviewer'/'writer'，跨 org 唯一) / `name` (≤120) / `icon` / `description` (≤280) / `instructions` / `allowedTools[]` (空 = 不限) / `enabled` (软隐藏，系统预设删除走 enabled=false) | `unique(organizationId, systemKey)` + `(organizationId, createdById, enabled, updatedAt desc)` |
| `AgentCron` *(Phase 2)* | 定时任务 — 用户自创 "每天 9 点跑这个 prompt"；@Cron 心跳 30s 扫到期 → 走 messages.runTurn 完整 LLM pipeline | `sessionId` (必须先有 session，cron 不自建；FK→agent_sessions ON DELETE CASCADE) / `cronExpr` (POSIX 5/6 字段，UTC 解释) / `prompt` (≤4000) / `nextRunAt` (每次 fire 后用 cron-parser 算下一个) / `lastRunAt` / `runCount` / `failCount` / `lastError` (≤500) / `enabled` | `(enabled, nextRunAt)` (心跳 tick 用) + `(organizationId, createdById, updatedAt desc)` |
| `AgentMcpServer` *(Phase 2)* | MCP Server 配置 — 管理员注册的外部 MCP server；启动时 spawn 子进程 / 建 SSE 连接，工具注入 ToolRegistry | `name` (≤120) / `transport` ('stdio' \| 'sse'，service 层硬校验；schema 文本字段为 P2 扩展 streamableHttp 留口) / `endpoint` (stdio=命令路径；sse=URL) / `args` (JSON 数组字符串) / `env` (JSON map，敏感值 P2 走 envelope encryption) / `enabled` / `lastConnectedAt` / `lastError` | `(organizationId, enabled)` |

### 审计/append-only/junction（`/// @skip-data-scope` 豁免，归属由父表 FK 控制）

| 表 | 用途 | 关键字段 | 关键索引 |
|---|---|---|---|
| `AgentMessage` | 消息历史（session 子表，append-only） | `turnId` / `type` (enum USER_PROMPT/ASSISTANT_TEXT/TOOL_USE/TOOL_RESULT/A2UI_COMPONENT/TURN_DONE) / `sequence` (INV-3 reconciliation) / `content` / `payload` JSON / `model` | `(sessionId, sequence)` + `(sessionId, createdAt)` + `(turnId)` |
| `AgentTrajectoryEvent` | session 审计事件流（sha256 哈希链） | `eventType` (TURN_STARTED/ROUTING_DECIDED/PROVIDER_INVOKED/MESSAGE_APPENDED/TURN_DONE/TOOL_CALL/TOOL_RESULT) / `sequenceInSession` 严格递增 / `prevEventHash` / `eventHash` / `payload` | `unique(sessionId, sequenceInSession)` + `(turnId)` + `(eventType, createdAt)` |
| `ModelRoutingDecision` | 每次路由决策审计（含 reasoning + cost） | `request` JSON / `decision` JSON / `matchSource` (RULE/LLM_ROUTED/SCOPE_OVERRIDE/DEFAULT) / `matchedRuleId` / `actualLatencyMs` / `actualCostUsd` 回填 | `(organizationId, createdAt desc)` + `(sessionId, createdAt)` + `(matchSource, createdAt)` |
| `AgentQuotaUsage` | 月度用量统计聚合（upsert） | `periodStart` (月初日期) / `tokensUsed` / `costUsdUsed` | `unique(scope, scopeRefId, periodStart)` + `(organizationId, periodStart)` |
| `AgentScratchpad` | per-session KV（跨 turn 持久化中间状态） | `key` / `value` JSON | `unique(sessionId, key)` + `(organizationId, sessionId)` |
| `StorageFile` | 文件目录（binding 子表） | `path` / `externalId` (OneDrive itemId) / `sha256` 完整性 / `encrypted` / `uploadedById` 设计语义≠createdById | `(organizationId, createdAt desc)` + `(bindingId, path)` + `(sha256)` |

## Enum 类型（12 个）

| Enum | 取值 | 用于 |
|---|---|---|
| `AgentSurface` | WEB / CLI / TEAMS / DESKTOP / IOS / ANDROID | session.surface（DB 列）；**routing/tool 层用 AgentSurfaceLabel 小写**，由 `normalizeSurfaceLabel()` 翻译 |
| `AgentSessionStatus` | ACTIVE / CLOSED / ARCHIVED | session.status |
| `AgentMessageType` | USER_PROMPT / ASSISTANT_TEXT / TOOL_USE / TOOL_RESULT / A2UI_COMPONENT / TURN_DONE | message.type |
| `AgentPlanMode` | OFF / REQUIRED | session.planMode + org default |
| `AgentPermissionMode` | DEFAULT / BYPASS / READ_ONLY / ASK_EVERY_TIME | session.permissionMode + org default |
| `AgentArtifactType` | TABLE / CHART / FILE / LINK / TEXT | artifact.type |
| `AgentTaskStatus` | PENDING / IN_PROGRESS / COMPLETED / CANCELLED / FAILED | task tracker |
| `AgentTrajectoryEventType` | TURN_STARTED / ROUTING_DECIDED / PROVIDER_INVOKED / MESSAGE_APPENDED / TURN_DONE / TOOL_CALL / TOOL_RESULT | trajectory.eventType |
| `ModelRoutingScope` | ORGANIZATION / USER / PROJECT | routing rule scope |
| `ModelRoutingMatchSource` | RULE / LLM_ROUTED / SCOPE_OVERRIDE / DEFAULT | decision.matchSource |
| `AgentQuotaScope` | ORGANIZATION / USER | quota scope |
| `StorageBindingScope` | ORGANIZATION / USER / PROJECT | storage binding scope |
| `StorageBackendKind` | LOCAL / ONEDRIVE / S3 / GOOGLE_DRIVE | binding.kind |
| `MemoryScope` *(M1 step2)* | GLOBAL / PROJECT / PERSONA | memory.scope（M1 全部生效；激活条件见 service.buildSystemPromptSection 文档注释） |
| `MemoryOwnerScope` *(M1 step4)* | USER / ORG | memory.ownerScope；USER=私有，跟用户走；ORG=admin 配全 org 共享 |
| `MemoryCategory` *(M1 step3)* | USER / FEEDBACK / PROJECT / REFERENCE | memory.category；指导"何时写 / 何时读"，前端 4 段折叠分组 |

## DataScope 合规

- **业务核心表**（12 张）满足 CLAUDE.md「标准字段」：id / createdAt(Timestamptz 3) / updatedAt(Timestamptz 3) / createdById / organizationId
  - 例外：`AgentPersona.createdById` 允许 NULL（系统预设语义，org 共享），CRUD 入口在 service 层用 `assertOwn(..., { allowSystemOwner: true })` 处理
  - 例外：`AgentMemory.createdById` 允许 NULL（当 ownerScope=ORG 时），由 DB CHECK 约束 `agent_memories_owner_consistency` 强制 `(ownerScope=USER ∧ createdById NOT NULL) OR (ownerScope=ORG ∧ createdById IS NULL)`；admin 入口走独立 controller `AgentAdminMemoriesController`
- **审计/append-only/junction 表**（6 张）用 `/// @skip-data-scope <原因>` 豁免，原因在 prisma schema 行内注释

## INV-* 不变量映射

| 不变量（02-architecture.md） | 落实位置 |
|---|---|
| INV-1 跨 org 写隔离 | DataScope 强制字段 + `appendMessage` 内 session.organizationId 比对 + `@SkipAssertAccess('...')` 行内豁免说明 |
| INV-3 turn-level reconciliation | `AgentMessage.sequence` per-session 自增；`AgentTrajectoryEvent.sequenceInSession` unique 约束 |
| INV-4 trajectory append-only | sha256 哈希链 + `(sessionId, sequenceInSession)` unique 约束；`verifyChain()` API |
| INV-5 quota 不可被绕过 | `messages.service.runTurn` 入口 `quota.assertAllowed(...)` 硬上限 → ForbiddenException |

## 待补字段（GA 前）

- ~~AgentMemory（per-user + per-org）~~ — M1 已落地（三维度：ownerScope × scope × category）
- AgentMemoryEmbedding（M4 相关性 top-K 召回，embedding 列 + ANN 索引，待选 provider）
- AgentCredential（OAuth refresh token 等敏感凭据，envelope encrypted）
- AgentSkill / AgentHook（skill registry 持久化；hook registry 当前仍内置）
- AgentMcpServer.env / args envelope encryption（Phase 2 明文存，P2 走 envelope）
- AgentTrajectoryAnchor（S3 / TSA 锚定记录表，PR4c 完整版）
- AgentTrajectoryEvent 时间×org 二级分区（PR4c v0.5 reviewer 要求）
