import { sql } from 'drizzle-orm' import { index, integer, primaryKey, real, sqliteTable, text } from 'drizzle-orm/sqlite-core' import type { FileAccessPolicy, InteractionRequest, InteractionResponse, McpToolHints, McpToolPolicy, PermissionMode, ProfileSnapshot, ScheduleOutcome, ScheduleSpec, TeamDebug, TeamFileRef, TeamLimitKey, TeamLimits, } from '@agent-server/shared' const now = sql`(strftime('%Y-%m-%dT%H:%M:%fZ','now'))` const createdAt = () => text('created_at').notNull().default(now) const updatedAt = () => text('updated_at').notNull().default(now) export const apiKeys = sqliteTable('api_keys', { id: text('id').primaryKey(), name: text('name').notNull(), keyHash: text('key_hash').notNull().unique(), keyPrefix: text('key_prefix').notNull(), enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), /** 这个平台可以使用的 Profile(至少一个) */ allowedProfileIds: text('allowed_profile_ids', { mode: 'json' }).$type().notNull().default(sql`'[]'`), /** 平台创建会话时不指定 Profile 则使用它,必须在 allowedProfileIds 中 */ defaultProfileId: text('default_profile_id'), createdAt: createdAt(), lastUsedAt: text('last_used_at'), }) export const profiles = sqliteTable('profiles', { id: text('id').primaryKey(), name: text('name').notNull(), description: text('description').notNull().default(''), provider: text('provider', { enum: ['claude', 'codex'] }).notNull(), model: text('model').notNull(), systemPrompt: text('system_prompt').notNull().default(''), permissionMode: text('permission_mode').$type().notNull().default('ask'), allowedTools: text('allowed_tools', { mode: 'json' }).$type().notNull().default(sql`'[]'`), disallowedTools: text('disallowed_tools', { mode: 'json' }).$type().notNull().default(sql`'[]'`), skills: text('skills', { mode: 'json' }).$type().notNull().default(sql`'"all"'`), fileAccess: text('file_access', { mode: 'json' }).$type().notNull(), knowledgeRootIds: text('knowledge_root_ids', { mode: 'json' }).$type().notNull().default(sql`'[]'`), mcpServerIds: text('mcp_server_ids', { mode: 'json' }).$type().notNull().default(sql`'[]'`), codexPluginIds: text('codex_plugin_ids', { mode: 'json' }).$type().notNull().default(sql`'[]'`), isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false), /** 内置 Profile(通用助手、组长)不可删除 */ builtin: integer('builtin', { mode: 'boolean' }).notNull().default(false), /** 内置 Profile 的种类:空为通用助手,lead 为组长(docs/design/13-bot-teams.md §17) */ builtinKey: text('builtin_key').$type<'lead'>(), version: integer('version').notNull().default(1), createdAt: createdAt(), updatedAt: updatedAt(), }) export const knowledgeRoots = sqliteTable('knowledge_roots', { id: text('id').primaryKey(), name: text('name').notNull(), path: text('path').notNull(), description: text('description').notNull().default(''), access: text('access', { enum: ['read', 'write'] }).notNull().default('read'), enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), createdAt: createdAt(), updatedAt: updatedAt(), }) export const sessions = sqliteTable('sessions', { id: text('id').primaryKey(), /** 调用来源:API Key id,或后台的 'admin' */ source: text('source').notNull(), profileId: text('profile_id'), provider: text('provider', { enum: ['claude', 'codex'] }).notNull(), model: text('model').notNull(), /** 为空表示跟随 Profile;只有后台管理员可以为单个会话覆盖 */ permissionMode: text('permission_mode').$type(), systemPromptAppend: text('system_prompt_append').notNull().default(''), providerSessionId: text('provider_session_id'), workspaceDir: text('workspace_dir').notNull(), title: text('title').notNull().default(''), lastSeq: integer('last_seq').notNull().default(0), archived: integer('archived', { mode: 'boolean' }).notNull().default(false), createdAt: createdAt(), updatedAt: updatedAt(), }, t => [index('sessions_source_idx').on(t.source, t.updatedAt)]) export const runs = sqliteTable('runs', { id: text('id').primaryKey(), sessionId: text('session_id').notNull(), status: text('status', { enum: ['queued', 'running', 'waiting_input', 'completed', 'failed', 'interrupted'] }).notNull(), startedAt: text('started_at'), endedAt: text('ended_at'), inputTokens: integer('input_tokens'), outputTokens: integer('output_tokens'), cacheReadTokens: integer('cache_read_tokens'), cacheWriteTokens: integer('cache_write_tokens'), costUsd: real('cost_usd'), /** 最后一次模型调用的上下文大小(判断工作组机器人的会话是否过长) */ contextTokens: integer('context_tokens'), error: text('error'), createdAt: createdAt(), }, t => [index('runs_session_idx').on(t.sessionId)]) export const events = sqliteTable('events', { sessionId: text('session_id').notNull(), seq: integer('seq').notNull(), runId: text('run_id'), type: text('type').notNull(), payload: text('payload', { mode: 'json' }).notNull(), ts: text('ts').notNull(), }, t => [primaryKey({ columns: [t.sessionId, t.seq] })]) export const interactions = sqliteTable('interactions', { id: text('id').primaryKey(), sessionId: text('session_id').notNull(), /** 不属于某一轮的交互(后台长任务中服务发来的询问)为空 */ runId: text('run_id'), kind: text('kind', { enum: ['approval', 'question', 'form'] }).notNull(), request: text('request', { mode: 'json' }).$type().notNull(), response: text('response', { mode: 'json' }).$type(), status: text('status', { enum: ['pending', 'allowed', 'denied', 'answered', 'timeout', 'cancelled'] }).notNull(), createdAt: createdAt(), resolvedAt: text('resolved_at'), expiresAt: text('expires_at').notNull(), }, t => [index('interactions_session_idx').on(t.sessionId, t.status)]) export const files = sqliteTable('files', { id: text('id').primaryKey(), sessionId: text('session_id').notNull(), relPath: text('rel_path').notNull(), origin: text('origin', { enum: ['upload', 'agent'] }).notNull(), mime: text('mime').notNull(), size: integer('size').notNull(), mtimeMs: real('mtime_ms').notNull(), createdAt: createdAt(), updatedAt: updatedAt(), }, t => [index('files_session_path_idx').on(t.sessionId, t.relPath)]) export const settings = sqliteTable('settings', { key: text('key').primaryKey(), value: text('value', { mode: 'json' }).notNull(), updatedAt: updatedAt(), }) export const auditLogs = sqliteTable('audit_logs', { id: integer('id').primaryKey({ autoIncrement: true }), ts: text('ts').notNull().default(now), actor: text('actor').notNull(), sessionId: text('session_id'), kind: text('kind', { enum: ['command', 'file_access_denied', 'network_denied', 'approval', 'admin', 'gateway_call'] }).notNull(), summary: text('summary').notNull(), detail: text('detail', { mode: 'json' }), }, t => [index('audit_ts_idx').on(t.ts)]) export const adminSessions = sqliteTable('admin_sessions', { id: text('id').primaryKey(), createdAt: createdAt(), expiresAt: text('expires_at').notNull(), }) /** 按天汇总的用量(Run 结束时累加;删除会话不影响历史用量)。日期为服务器本地日期 */ export const usageDaily = sqliteTable('usage_daily', { date: text('date').notNull(), /** API Key id,或后台的 'admin' */ source: text('source').notNull(), provider: text('provider').notNull(), model: text('model').notNull(), runs: integer('runs').notNull().default(0), failed: integer('failed').notNull().default(0), interrupted: integer('interrupted').notNull().default(0), inputTokens: integer('input_tokens').notNull().default(0), outputTokens: integer('output_tokens').notNull().default(0), cacheReadTokens: integer('cache_read_tokens').notNull().default(0), cacheWriteTokens: integer('cache_write_tokens').notNull().default(0), costUsd: real('cost_usd').notNull().default(0), durationMs: integer('duration_ms').notNull().default(0), }, t => [primaryKey({ columns: [t.date, t.source, t.provider, t.model] })]) /** Profile 的历史版本:每次内容变化保存一份快照(不含"是否默认"),可以查看和恢复 */ export const profileVersions = sqliteTable('profile_versions', { profileId: text('profile_id').notNull(), version: integer('version').notNull(), snapshot: text('snapshot', { mode: 'json' }).$type().notNull(), createdAt: createdAt(), }, t => [primaryKey({ columns: [t.profileId, t.version] })]) /** 第三方 MCP 服务(经 MCP Gateway 转接,ADR-0021)。headers / env 中含密钥,只在服务端使用 */ export const mcpServers = sqliteTable('mcp_servers', { id: text('id').primaryKey(), name: text('name').notNull().unique(), title: text('title').notNull(), description: text('description').notNull().default(''), transport: text('transport', { enum: ['http', 'stdio'] }).notNull(), enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), url: text('url').notNull().default(''), headers: text('headers', { mode: 'json' }).$type>().notNull().default(sql`'{}'`), command: text('command').notNull().default(''), args: text('args', { mode: 'json' }).$type().notNull().default(sql`'[]'`), env: text('env', { mode: 'json' }).$type>().notNull().default(sql`'{}'`), allowNetwork: integer('allow_network', { mode: 'boolean' }).notNull().default(false), readPaths: text('read_paths', { mode: 'json' }).$type().notNull().default(sql`'[]'`), writePaths: text('write_paths', { mode: 'json' }).$type().notNull().default(sql`'[]'`), /** 工具策略(工具名 → auto / ask / off);没有设置的工具按默认:只读工具 auto,其余 ask */ toolPolicies: text('tool_policies', { mode: 'json' }).$type>().notNull().default(sql`'{}'`), /** 总是在后台执行的工具(docs/design/15-background-calls.md §3) */ backgroundTools: text('background_tools', { mode: 'json' }).$type().notNull().default(sql`'[]'`), /** 管理员覆盖的工具提示(工具名 → 只读 / 破坏性 / 幂等,docs/design/16-mcp-support.md §2.1) */ toolHints: text('tool_hints', { mode: 'json' }).$type>().notNull().default(sql`'{}'`), /** OAuth 授权(配置、动态注册的客户端信息、令牌、授权过程中的临时值),不出现在 DTO 中 */ oauth: text('oauth', { mode: 'json' }).$type(), /** 最近一次列出的工具(引擎列工具时直接使用,不必每次启动服务) */ toolsCache: text('tools_cache', { mode: 'json' }).$type().notNull().default(sql`'[]'`), toolsUpdatedAt: text('tools_updated_at'), /** 服务声明的能力(最近一次连接时记录):资源、提示模板 */ features: text('features', { mode: 'json' }).$type().notNull().default(sql`'{}'`), createdAt: createdAt(), updatedAt: updatedAt(), }) export interface McpFeatures { resources?: boolean prompts?: boolean } export interface McpOAuthState { enabled: boolean clientId: string clientSecret: string scope: string /** 回调地址(发起授权时访问后台的地址) */ redirectUrl?: string /** 动态注册得到的客户端信息 */ clientInfo?: Record tokens?: Record codeVerifier?: string /** 授权进行中:state 与过期时间 */ state?: string stateExpiresAt?: string authorizedAt?: string lastError?: string } export interface McpCachedTool { name: string description: string inputSchema: Record /** 服务声明的只读提示(annotations.readOnlyHint === true) */ readOnly: boolean title?: string /** 服务声明的其他提示(未声明的不出现) */ annotations?: { destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; readOnlyHint?: boolean } outputSchema?: Record taskSupport?: 'required' | 'optional' } /** 文件清单:相对路径 → 内容摘要与大小(内容存放在按 sha256 寻址的文件库中) */ export type FileMap = Record /** 自定义技能(docs/design/12-custom-skills.md)。运行时只读 custom-skills/skills/ 下的生效版本 */ export const customSkills = sqliteTable('custom_skills', { id: text('id').primaryKey(), name: text('name').notNull().unique(), description: text('description').notNull().default(''), origin: text('origin', { enum: ['authored', 'imported'] }).notNull(), sourceId: text('source_id'), enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), includeInAll: integer('include_in_all', { mode: 'boolean' }).notNull().default(false), publishedVersion: integer('published_version'), latestVersion: integer('latest_version').notNull().default(0), /** 编辑中的文件(尚未保存为版本的改动) */ workingFiles: text('working_files', { mode: 'json' }).$type().notNull().default(sql`'{}'`), createdAt: createdAt(), updatedAt: updatedAt(), }) export const customSkillVersions = sqliteTable('custom_skill_versions', { skillId: text('skill_id').notNull(), version: integer('version').notNull(), reason: text('reason', { enum: ['create', 'edit', 'import', 'sync', 'restore'] }).notNull(), files: text('files', { mode: 'json' }).$type().notNull(), changed: text('changed', { mode: 'json' }).$type<{ added: string[]; modified: string[]; removed: string[] }>().notNull(), createdAt: createdAt(), }, t => [primaryKey({ columns: [t.skillId, t.version] })]) /** 技能源:服务器目录或上传的 zip(解压在数据目录中) */ export const skillSources = sqliteTable('skill_sources', { id: text('id').primaryKey(), name: text('name').notNull(), description: text('description').notNull().default(''), kind: text('kind', { enum: ['directory', 'upload'] }).notNull(), path: text('path').notNull(), syncMode: text('sync_mode', { enum: ['manual', 'auto'] }).notNull().default('manual'), intervalMin: integer('interval_min').notNull().default(10), lastSyncAt: text('last_sync_at'), lastSyncDigest: text('last_sync_digest'), status: text('status', { enum: ['never', 'ok', 'error'] }).notNull().default('never'), lastError: text('last_error'), createdAt: createdAt(), updatedAt: updatedAt(), }) /** 机器人(docs/design/13-bot-teams.md):身份 + 关联的 Profile */ export const bots = sqliteTable('bots', { id: text('id').primaryKey(), name: text('name').notNull().unique(), avatar: text('avatar').notNull().default('🤖'), kind: text('kind', { enum: ['lead', 'worker'] }).notNull(), description: text('description').notNull().default(''), profileId: text('profile_id').notNull(), enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), createdAt: createdAt(), updatedAt: updatedAt(), }) /** Bot API 的密钥(ADR-0024),与 api_keys 互不通用 */ export const botApiKeys = sqliteTable('bot_api_keys', { id: text('id').primaryKey(), name: text('name').notNull(), keyHash: text('key_hash').notNull().unique(), keyPrefix: text('key_prefix').notNull(), enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true), allowedBotIds: text('allowed_bot_ids', { mode: 'json' }).$type().notNull().default(sql`'[]'`), createdAt: createdAt(), lastUsedAt: text('last_used_at'), }) export const teams = sqliteTable('teams', { id: text('id').primaryKey(), /** Bot Key id,或后台的 'admin' */ source: text('source').notNull(), title: text('title').notNull().default(''), human: text('human', { mode: 'json' }).$type<{ id: string; name: string }>().notNull(), status: text('status', { enum: ['active', 'paused', 'closed'] }).notNull().default('active'), /** 共享目录:组内所有机器人会话的工作目录 */ workspaceDir: text('workspace_dir').notNull(), /** 限额:创建时以系统设置为准,可以调低 */ limits: text('limits', { mode: 'json' }).$type>(), /** 调试设置(仅 admin 的工作组) */ debug: text('debug', { mode: 'json' }).$type().notNull().default(sql`'{"stepMode":false,"stubBotIds":[]}'`), lastSeq: integer('last_seq').notNull().default(0), /** 由回归用例重放创建时的用例 id */ caseId: text('case_id'), createdAt: createdAt(), updatedAt: updatedAt(), }, t => [index('teams_source_idx').on(t.source, t.updatedAt), index('teams_case_idx').on(t.caseId)]) export const teamBots = sqliteTable('team_bots', { teamId: text('team_id').notNull(), botId: text('bot_id').notNull(), role: text('role', { enum: ['lead', 'worker'] }).notNull(), joinedAt: text('joined_at').notNull().default(now), }, t => [primaryKey({ columns: [t.teamId, t.botId] }), index('team_bots_bot_idx').on(t.botId)]) /** 每个"机器人 × 工作组"一个引擎会话;离开工作组后保留,再加入时续接 */ export const teamBotSessions = sqliteTable('team_bot_sessions', { teamId: text('team_id').notNull(), botId: text('bot_id').notNull(), sessionId: text('session_id').notNull().unique(), /** 已交给这个机器人的工作组事件 seq */ lastReadSeq: integer('last_read_seq').notNull().default(0), /** 成员要求开始新的会话:下一轮不再续接 */ resetRequested: integer('reset_requested', { mode: 'boolean' }).notNull().default(false), }, t => [primaryKey({ columns: [t.teamId, t.botId] })]) export const teamEvents = sqliteTable('team_events', { teamId: text('team_id').notNull(), seq: integer('seq').notNull(), type: text('type').notNull(), payload: text('payload', { mode: 'json' }).notNull(), ts: text('ts').notNull(), }, t => [primaryKey({ columns: [t.teamId, t.seq] })]) /** 任务:成员交给组长的一项复杂工作,同一工作组内依次执行 */ export const jobs = sqliteTable('jobs', { id: text('id').primaryKey(), teamId: text('team_id').notNull(), no: integer('no').notNull(), title: text('title').notNull(), plan: text('plan').notNull().default(''), status: text('status', { enum: ['running', 'blocked', 'completed', 'failed', 'cancelled'] }).notNull().default('running'), summary: text('summary'), files: text('files', { mode: 'json' }).$type().notNull().default(sql`'[]'`), blockedBy: text('blocked_by').$type(), /** 本任务生效的限额(继续后加倍) */ limits: text('limits', { mode: 'json' }).$type>().notNull(), leadRuns: integer('lead_runs').notNull().default(0), /** 本任务产生的全部 Run(组长与子任务),用于统计成本 */ runIds: text('run_ids', { mode: 'json' }).$type().notNull().default(sql`'[]'`), createdAt: createdAt(), endedAt: text('ended_at'), }, t => [index('jobs_team_idx').on(t.teamId, t.no)]) /** 子任务:组长分派给某个组员的单项工作 */ export const tasks = sqliteTable('tasks', { id: text('id').primaryKey(), jobId: text('job_id').notNull(), teamId: text('team_id').notNull(), no: integer('no').notNull(), botId: text('bot_id').notNull(), title: text('title').notNull(), instructions: text('instructions').notNull(), dependsOn: text('depends_on', { mode: 'json' }).$type().notNull().default(sql`'[]'`), status: text('status', { enum: ['waiting', 'queued', 'running', 'stub', 'background', 'completed', 'failed', 'cancelled'] }).notNull(), /** 等待的后台调用结束后,组员下一轮收到的后续输入(docs/design/15-background-calls.md §4) */ followUp: text('follow_up'), result: text('result'), files: text('files', { mode: 'json' }).$type().notNull().default(sql`'[]'`), runId: text('run_id'), sessionId: text('session_id'), createdAt: createdAt(), startedAt: text('started_at'), endedAt: text('ended_at'), }, t => [index('tasks_team_idx').on(t.teamId, t.no), index('tasks_job_idx').on(t.jobId)]) /** 回归用例(docs/design/13-bot-teams.md §12.2):成员组成 + 第一条命令,批量重放后人工比对结果 */ export const teamCases = sqliteTable('team_cases', { id: text('id').primaryKey(), name: text('name').notNull(), leadBotId: text('lead_bot_id'), workerBotIds: text('worker_bot_ids', { mode: 'json' }).$type().notNull().default(sql`'[]'`), stubBotIds: text('stub_bot_ids', { mode: 'json' }).$type().notNull().default(sql`'[]'`), limits: text('limits', { mode: 'json' }).$type>(), humanName: text('human_name').notNull(), text: text('text').notNull(), mentions: text('mentions', { mode: 'json' }).$type().notNull().default(sql`'[]'`), /** 第一条命令的附件,复制到 /data/team-cases// 下 */ files: text('files', { mode: 'json' }).$type<{ name: string; size: number }[]>().notNull().default(sql`'[]'`), createdAt: createdAt(), updatedAt: updatedAt(), }) /** 定时任务(docs/design/14-schedules.md):属于一个会话或一个工作组 */ export const schedules = sqliteTable('schedules', { id: text('id').primaryKey(), sessionId: text('session_id'), teamId: text('team_id'), /** 工作组中创建它的机器人(到点的消息 @ 组员时使用) */ createdByBotId: text('created_by_bot_id'), title: text('title').notNull(), prompt: text('prompt').notNull(), spec: text('spec', { mode: 'json' }).$type().notNull(), timezone: text('timezone').notNull(), status: text('status', { enum: ['active', 'paused', 'ended'] }).notNull().default('active'), statusNote: text('status_note'), nextRunAt: text('next_run_at'), lastRunAt: text('last_run_at'), lastOutcome: text('last_outcome').$type(), runCount: integer('run_count').notNull().default(0), createdAt: createdAt(), updatedAt: updatedAt(), }, t => [index('schedules_due_idx').on(t.status, t.nextRunAt), index('schedules_session_idx').on(t.sessionId), index('schedules_team_idx').on(t.teamId)]) export const scheduleRuns = sqliteTable('schedule_runs', { id: integer('id').primaryKey({ autoIncrement: true }), scheduleId: text('schedule_id').notNull(), ts: text('ts').notNull().default(now), outcome: text('outcome').$type().notNull(), runId: text('run_id'), costUsd: real('cost_usd'), note: text('note'), }, t => [index('schedule_runs_idx').on(t.scheduleId, t.id)]) /** 后台调用(docs/design/15-background-calls.md):外部工具的长调用,由服务端等待,完成后通知发起方 */ export const backgroundCalls = sqliteTable('background_calls', { id: text('id').primaryKey(), sessionId: text('session_id').notNull(), teamId: text('team_id'), botId: text('bot_id'), taskId: text('task_id'), serverId: text('server_id').notNull(), serverName: text('server_name').notNull(), serverTitle: text('server_title').notNull(), tool: text('tool').notNull(), args: text('args', { mode: 'json' }).notNull(), status: text('status', { enum: ['running', 'completed', 'failed', 'cancelled'] }).notNull().default('running'), startedAt: text('started_at').notNull().default(now), endedAt: text('ended_at'), result: text('result'), error: text('error'), delivered: integer('delivered', { mode: 'boolean' }).notNull().default(false), /** 等待方式:wait 保持连接等待;task 以长任务方式调用、按任务 id 查询(docs/design/16-mcp-support.md §3) */ mode: text('mode', { enum: ['wait', 'task'] }).notNull().default('wait'), /** 长任务:服务返回的任务 id、调用时使用的连接(会话)、下次查询时间、查询连续出错的起始时间 */ mcpTaskId: text('mcp_task_id'), connScope: text('conn_scope'), pollAfter: text('poll_after'), pollErrorSince: text('poll_error_since'), statusMessage: text('status_message'), }, t => [index('background_calls_session_idx').on(t.sessionId), index('background_calls_team_idx').on(t.teamId), index('background_calls_status_idx').on(t.status, t.delivered)])