schema.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import { sql } from 'drizzle-orm'
  2. import { index, integer, primaryKey, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'
  3. import type { FileAccessPolicy, InteractionRequest, InteractionResponse, McpToolPolicy, PermissionMode, ProfileSnapshot } from '@agent-server/shared'
  4. const now = sql`(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`
  5. const createdAt = () => text('created_at').notNull().default(now)
  6. const updatedAt = () => text('updated_at').notNull().default(now)
  7. export const apiKeys = sqliteTable('api_keys', {
  8. id: text('id').primaryKey(),
  9. name: text('name').notNull(),
  10. keyHash: text('key_hash').notNull().unique(),
  11. keyPrefix: text('key_prefix').notNull(),
  12. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  13. /** 这个平台可以使用的 Profile(至少一个) */
  14. allowedProfileIds: text('allowed_profile_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  15. /** 平台创建会话时不指定 Profile 则使用它,必须在 allowedProfileIds 中 */
  16. defaultProfileId: text('default_profile_id'),
  17. createdAt: createdAt(),
  18. lastUsedAt: text('last_used_at'),
  19. })
  20. export const profiles = sqliteTable('profiles', {
  21. id: text('id').primaryKey(),
  22. name: text('name').notNull(),
  23. description: text('description').notNull().default(''),
  24. provider: text('provider', { enum: ['claude', 'codex'] }).notNull(),
  25. model: text('model').notNull(),
  26. systemPrompt: text('system_prompt').notNull().default(''),
  27. permissionMode: text('permission_mode').$type<PermissionMode>().notNull().default('ask'),
  28. allowedTools: text('allowed_tools', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  29. disallowedTools: text('disallowed_tools', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  30. skills: text('skills', { mode: 'json' }).$type<string[] | 'all'>().notNull().default(sql`'"all"'`),
  31. fileAccess: text('file_access', { mode: 'json' }).$type<FileAccessPolicy>().notNull(),
  32. knowledgeRootIds: text('knowledge_root_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  33. mcpServerIds: text('mcp_server_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  34. isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
  35. /** 内置 Profile(通用助手)不可删除 */
  36. builtin: integer('builtin', { mode: 'boolean' }).notNull().default(false),
  37. version: integer('version').notNull().default(1),
  38. createdAt: createdAt(),
  39. updatedAt: updatedAt(),
  40. })
  41. export const knowledgeRoots = sqliteTable('knowledge_roots', {
  42. id: text('id').primaryKey(),
  43. name: text('name').notNull(),
  44. path: text('path').notNull(),
  45. description: text('description').notNull().default(''),
  46. access: text('access', { enum: ['read', 'write'] }).notNull().default('read'),
  47. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  48. createdAt: createdAt(),
  49. updatedAt: updatedAt(),
  50. })
  51. export const sessions = sqliteTable('sessions', {
  52. id: text('id').primaryKey(),
  53. /** 调用来源:API Key id,或后台的 'admin' */
  54. source: text('source').notNull(),
  55. profileId: text('profile_id'),
  56. provider: text('provider', { enum: ['claude', 'codex'] }).notNull(),
  57. model: text('model').notNull(),
  58. /** 为空表示跟随 Profile;只有后台管理员可以为单个会话覆盖 */
  59. permissionMode: text('permission_mode').$type<PermissionMode>(),
  60. systemPromptAppend: text('system_prompt_append').notNull().default(''),
  61. providerSessionId: text('provider_session_id'),
  62. workspaceDir: text('workspace_dir').notNull(),
  63. title: text('title').notNull().default(''),
  64. lastSeq: integer('last_seq').notNull().default(0),
  65. archived: integer('archived', { mode: 'boolean' }).notNull().default(false),
  66. createdAt: createdAt(),
  67. updatedAt: updatedAt(),
  68. }, t => [index('sessions_source_idx').on(t.source, t.updatedAt)])
  69. export const runs = sqliteTable('runs', {
  70. id: text('id').primaryKey(),
  71. sessionId: text('session_id').notNull(),
  72. status: text('status', { enum: ['queued', 'running', 'waiting_input', 'completed', 'failed', 'interrupted'] }).notNull(),
  73. startedAt: text('started_at'),
  74. endedAt: text('ended_at'),
  75. inputTokens: integer('input_tokens'),
  76. outputTokens: integer('output_tokens'),
  77. cacheReadTokens: integer('cache_read_tokens'),
  78. cacheWriteTokens: integer('cache_write_tokens'),
  79. costUsd: real('cost_usd'),
  80. error: text('error'),
  81. createdAt: createdAt(),
  82. }, t => [index('runs_session_idx').on(t.sessionId)])
  83. export const events = sqliteTable('events', {
  84. sessionId: text('session_id').notNull(),
  85. seq: integer('seq').notNull(),
  86. runId: text('run_id'),
  87. type: text('type').notNull(),
  88. payload: text('payload', { mode: 'json' }).notNull(),
  89. ts: text('ts').notNull(),
  90. }, t => [primaryKey({ columns: [t.sessionId, t.seq] })])
  91. export const interactions = sqliteTable('interactions', {
  92. id: text('id').primaryKey(),
  93. sessionId: text('session_id').notNull(),
  94. runId: text('run_id').notNull(),
  95. kind: text('kind', { enum: ['approval', 'question'] }).notNull(),
  96. request: text('request', { mode: 'json' }).$type<InteractionRequest>().notNull(),
  97. response: text('response', { mode: 'json' }).$type<InteractionResponse>(),
  98. status: text('status', { enum: ['pending', 'allowed', 'denied', 'answered', 'timeout', 'cancelled'] }).notNull(),
  99. createdAt: createdAt(),
  100. resolvedAt: text('resolved_at'),
  101. expiresAt: text('expires_at').notNull(),
  102. }, t => [index('interactions_session_idx').on(t.sessionId, t.status)])
  103. export const files = sqliteTable('files', {
  104. id: text('id').primaryKey(),
  105. sessionId: text('session_id').notNull(),
  106. relPath: text('rel_path').notNull(),
  107. origin: text('origin', { enum: ['upload', 'agent'] }).notNull(),
  108. mime: text('mime').notNull(),
  109. size: integer('size').notNull(),
  110. mtimeMs: real('mtime_ms').notNull(),
  111. createdAt: createdAt(),
  112. updatedAt: updatedAt(),
  113. }, t => [index('files_session_path_idx').on(t.sessionId, t.relPath)])
  114. export const settings = sqliteTable('settings', {
  115. key: text('key').primaryKey(),
  116. value: text('value', { mode: 'json' }).notNull(),
  117. updatedAt: updatedAt(),
  118. })
  119. export const auditLogs = sqliteTable('audit_logs', {
  120. id: integer('id').primaryKey({ autoIncrement: true }),
  121. ts: text('ts').notNull().default(now),
  122. actor: text('actor').notNull(),
  123. sessionId: text('session_id'),
  124. kind: text('kind', { enum: ['command', 'file_access_denied', 'network_denied', 'approval', 'admin', 'gateway_call'] }).notNull(),
  125. summary: text('summary').notNull(),
  126. detail: text('detail', { mode: 'json' }),
  127. }, t => [index('audit_ts_idx').on(t.ts)])
  128. export const adminSessions = sqliteTable('admin_sessions', {
  129. id: text('id').primaryKey(),
  130. createdAt: createdAt(),
  131. expiresAt: text('expires_at').notNull(),
  132. })
  133. /** 按天汇总的用量(Run 结束时累加;删除会话不影响历史用量)。日期为服务器本地日期 */
  134. export const usageDaily = sqliteTable('usage_daily', {
  135. date: text('date').notNull(),
  136. /** API Key id,或后台的 'admin' */
  137. source: text('source').notNull(),
  138. provider: text('provider').notNull(),
  139. model: text('model').notNull(),
  140. runs: integer('runs').notNull().default(0),
  141. failed: integer('failed').notNull().default(0),
  142. interrupted: integer('interrupted').notNull().default(0),
  143. inputTokens: integer('input_tokens').notNull().default(0),
  144. outputTokens: integer('output_tokens').notNull().default(0),
  145. cacheReadTokens: integer('cache_read_tokens').notNull().default(0),
  146. cacheWriteTokens: integer('cache_write_tokens').notNull().default(0),
  147. costUsd: real('cost_usd').notNull().default(0),
  148. durationMs: integer('duration_ms').notNull().default(0),
  149. }, t => [primaryKey({ columns: [t.date, t.source, t.provider, t.model] })])
  150. /** Profile 的历史版本:每次内容变化保存一份快照(不含"是否默认"),可以查看和恢复 */
  151. export const profileVersions = sqliteTable('profile_versions', {
  152. profileId: text('profile_id').notNull(),
  153. version: integer('version').notNull(),
  154. snapshot: text('snapshot', { mode: 'json' }).$type<ProfileSnapshot>().notNull(),
  155. createdAt: createdAt(),
  156. }, t => [primaryKey({ columns: [t.profileId, t.version] })])
  157. /** 第三方 MCP 服务(经 MCP Gateway 转接,ADR-0021)。headers / env 中含密钥,只在服务端使用 */
  158. export const mcpServers = sqliteTable('mcp_servers', {
  159. id: text('id').primaryKey(),
  160. name: text('name').notNull().unique(),
  161. title: text('title').notNull(),
  162. description: text('description').notNull().default(''),
  163. transport: text('transport', { enum: ['http', 'stdio'] }).notNull(),
  164. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  165. url: text('url').notNull().default(''),
  166. headers: text('headers', { mode: 'json' }).$type<Record<string, string>>().notNull().default(sql`'{}'`),
  167. command: text('command').notNull().default(''),
  168. args: text('args', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  169. env: text('env', { mode: 'json' }).$type<Record<string, string>>().notNull().default(sql`'{}'`),
  170. allowNetwork: integer('allow_network', { mode: 'boolean' }).notNull().default(false),
  171. readPaths: text('read_paths', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  172. writePaths: text('write_paths', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  173. /** 工具策略(工具名 → auto / ask / off);没有设置的工具按默认:只读工具 auto,其余 ask */
  174. toolPolicies: text('tool_policies', { mode: 'json' }).$type<Record<string, McpToolPolicy>>().notNull().default(sql`'{}'`),
  175. /** 最近一次列出的工具(引擎列工具时直接使用,不必每次启动服务) */
  176. toolsCache: text('tools_cache', { mode: 'json' }).$type<McpCachedTool[]>().notNull().default(sql`'[]'`),
  177. toolsUpdatedAt: text('tools_updated_at'),
  178. createdAt: createdAt(),
  179. updatedAt: updatedAt(),
  180. })
  181. export interface McpCachedTool {
  182. name: string
  183. description: string
  184. inputSchema: Record<string, unknown>
  185. readOnly: boolean
  186. }
  187. /** 文件清单:相对路径 → 内容摘要与大小(内容存放在按 sha256 寻址的文件库中) */
  188. export type FileMap = Record<string, { sha: string; size: number }>
  189. /** 自定义技能(docs/design/12-custom-skills.md)。运行时只读 custom-skills/skills/<name> 下的生效版本 */
  190. export const customSkills = sqliteTable('custom_skills', {
  191. id: text('id').primaryKey(),
  192. name: text('name').notNull().unique(),
  193. description: text('description').notNull().default(''),
  194. origin: text('origin', { enum: ['authored', 'imported'] }).notNull(),
  195. sourceId: text('source_id'),
  196. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  197. includeInAll: integer('include_in_all', { mode: 'boolean' }).notNull().default(false),
  198. publishedVersion: integer('published_version'),
  199. latestVersion: integer('latest_version').notNull().default(0),
  200. /** 编辑中的文件(尚未保存为版本的改动) */
  201. workingFiles: text('working_files', { mode: 'json' }).$type<FileMap>().notNull().default(sql`'{}'`),
  202. createdAt: createdAt(),
  203. updatedAt: updatedAt(),
  204. })
  205. export const customSkillVersions = sqliteTable('custom_skill_versions', {
  206. skillId: text('skill_id').notNull(),
  207. version: integer('version').notNull(),
  208. reason: text('reason', { enum: ['create', 'edit', 'import', 'sync', 'restore'] }).notNull(),
  209. files: text('files', { mode: 'json' }).$type<FileMap>().notNull(),
  210. changed: text('changed', { mode: 'json' }).$type<{ added: string[]; modified: string[]; removed: string[] }>().notNull(),
  211. createdAt: createdAt(),
  212. }, t => [primaryKey({ columns: [t.skillId, t.version] })])
  213. /** 技能源:服务器目录或上传的 zip(解压在数据目录中) */
  214. export const skillSources = sqliteTable('skill_sources', {
  215. id: text('id').primaryKey(),
  216. name: text('name').notNull(),
  217. description: text('description').notNull().default(''),
  218. kind: text('kind', { enum: ['directory', 'upload'] }).notNull(),
  219. path: text('path').notNull(),
  220. syncMode: text('sync_mode', { enum: ['manual', 'auto'] }).notNull().default('manual'),
  221. intervalMin: integer('interval_min').notNull().default(10),
  222. lastSyncAt: text('last_sync_at'),
  223. lastSyncDigest: text('last_sync_digest'),
  224. status: text('status', { enum: ['never', 'ok', 'error'] }).notNull().default('never'),
  225. lastError: text('last_error'),
  226. createdAt: createdAt(),
  227. updatedAt: updatedAt(),
  228. })