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 } 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), 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'), 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(), })