import { sql } from 'drizzle-orm' import { index, integer, primaryKey, real, sqliteTable, text } from 'drizzle-orm/sqlite-core' import type { FileAccessPolicy, InteractionRequest, InteractionResponse, 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`'[]'`), 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').notNull(), kind: text('kind', { enum: ['approval', 'question'] }).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`'{}'`), /** 最近一次列出的工具(引擎列工具时直接使用,不必每次启动服务) */ toolsCache: text('tools_cache', { mode: 'json' }).$type().notNull().default(sql`'[]'`), toolsUpdatedAt: text('tools_updated_at'), createdAt: createdAt(), updatedAt: updatedAt(), }) export interface McpCachedTool { name: string description: string inputSchema: Record readOnly: boolean } /** 文件清单:相对路径 → 内容摘要与大小(内容存放在按 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', 'completed', 'failed', 'cancelled'] }).notNull(), 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)])