schema.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. import { sql } from 'drizzle-orm'
  2. import { index, integer, primaryKey, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'
  3. import type {
  4. FileAccessPolicy, InteractionRequest, InteractionResponse, McpToolHints, McpToolPolicy, PermissionMode, ProfileSnapshot, ScheduleOutcome, ScheduleSpec, TeamDebug, TeamFileRef, TeamLimitKey, TeamLimits,
  5. } from '@agent-server/shared'
  6. const now = sql`(strftime('%Y-%m-%dT%H:%M:%fZ','now'))`
  7. const createdAt = () => text('created_at').notNull().default(now)
  8. const updatedAt = () => text('updated_at').notNull().default(now)
  9. export const apiKeys = sqliteTable('api_keys', {
  10. id: text('id').primaryKey(),
  11. name: text('name').notNull(),
  12. keyHash: text('key_hash').notNull().unique(),
  13. keyPrefix: text('key_prefix').notNull(),
  14. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  15. /** 这个平台可以使用的 Profile(至少一个) */
  16. allowedProfileIds: text('allowed_profile_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  17. /** 平台创建会话时不指定 Profile 则使用它,必须在 allowedProfileIds 中 */
  18. defaultProfileId: text('default_profile_id'),
  19. createdAt: createdAt(),
  20. lastUsedAt: text('last_used_at'),
  21. })
  22. export const profiles = sqliteTable('profiles', {
  23. id: text('id').primaryKey(),
  24. name: text('name').notNull(),
  25. description: text('description').notNull().default(''),
  26. provider: text('provider', { enum: ['claude', 'codex'] }).notNull(),
  27. model: text('model').notNull(),
  28. systemPrompt: text('system_prompt').notNull().default(''),
  29. permissionMode: text('permission_mode').$type<PermissionMode>().notNull().default('ask'),
  30. allowedTools: text('allowed_tools', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  31. disallowedTools: text('disallowed_tools', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  32. skills: text('skills', { mode: 'json' }).$type<string[] | 'all'>().notNull().default(sql`'"all"'`),
  33. fileAccess: text('file_access', { mode: 'json' }).$type<FileAccessPolicy>().notNull(),
  34. knowledgeRootIds: text('knowledge_root_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  35. mcpServerIds: text('mcp_server_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  36. codexPluginIds: text('codex_plugin_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  37. isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
  38. /** 内置 Profile(通用助手、组长)不可删除 */
  39. builtin: integer('builtin', { mode: 'boolean' }).notNull().default(false),
  40. /** 内置 Profile 的种类:空为通用助手,lead 为组长(docs/design/13-bot-teams.md §17) */
  41. builtinKey: text('builtin_key').$type<'lead'>(),
  42. version: integer('version').notNull().default(1),
  43. createdAt: createdAt(),
  44. updatedAt: updatedAt(),
  45. })
  46. export const knowledgeRoots = sqliteTable('knowledge_roots', {
  47. id: text('id').primaryKey(),
  48. name: text('name').notNull(),
  49. path: text('path').notNull(),
  50. description: text('description').notNull().default(''),
  51. access: text('access', { enum: ['read', 'write'] }).notNull().default('read'),
  52. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  53. createdAt: createdAt(),
  54. updatedAt: updatedAt(),
  55. })
  56. export const sessions = sqliteTable('sessions', {
  57. id: text('id').primaryKey(),
  58. /** 调用来源:API Key id,或后台的 'admin' */
  59. source: text('source').notNull(),
  60. profileId: text('profile_id'),
  61. provider: text('provider', { enum: ['claude', 'codex'] }).notNull(),
  62. model: text('model').notNull(),
  63. /** 为空表示跟随 Profile;只有后台管理员可以为单个会话覆盖 */
  64. permissionMode: text('permission_mode').$type<PermissionMode>(),
  65. systemPromptAppend: text('system_prompt_append').notNull().default(''),
  66. providerSessionId: text('provider_session_id'),
  67. workspaceDir: text('workspace_dir').notNull(),
  68. title: text('title').notNull().default(''),
  69. lastSeq: integer('last_seq').notNull().default(0),
  70. archived: integer('archived', { mode: 'boolean' }).notNull().default(false),
  71. createdAt: createdAt(),
  72. updatedAt: updatedAt(),
  73. }, t => [index('sessions_source_idx').on(t.source, t.updatedAt)])
  74. export const runs = sqliteTable('runs', {
  75. id: text('id').primaryKey(),
  76. sessionId: text('session_id').notNull(),
  77. status: text('status', { enum: ['queued', 'running', 'waiting_input', 'completed', 'failed', 'interrupted'] }).notNull(),
  78. startedAt: text('started_at'),
  79. endedAt: text('ended_at'),
  80. inputTokens: integer('input_tokens'),
  81. outputTokens: integer('output_tokens'),
  82. cacheReadTokens: integer('cache_read_tokens'),
  83. cacheWriteTokens: integer('cache_write_tokens'),
  84. costUsd: real('cost_usd'),
  85. /** 最后一次模型调用的上下文大小(判断工作组机器人的会话是否过长) */
  86. contextTokens: integer('context_tokens'),
  87. error: text('error'),
  88. createdAt: createdAt(),
  89. }, t => [index('runs_session_idx').on(t.sessionId)])
  90. export const events = sqliteTable('events', {
  91. sessionId: text('session_id').notNull(),
  92. seq: integer('seq').notNull(),
  93. runId: text('run_id'),
  94. type: text('type').notNull(),
  95. payload: text('payload', { mode: 'json' }).notNull(),
  96. ts: text('ts').notNull(),
  97. }, t => [primaryKey({ columns: [t.sessionId, t.seq] })])
  98. export const interactions = sqliteTable('interactions', {
  99. id: text('id').primaryKey(),
  100. sessionId: text('session_id').notNull(),
  101. /** 不属于某一轮的交互(后台长任务中服务发来的询问)为空 */
  102. runId: text('run_id'),
  103. kind: text('kind', { enum: ['approval', 'question', 'form'] }).notNull(),
  104. request: text('request', { mode: 'json' }).$type<InteractionRequest>().notNull(),
  105. response: text('response', { mode: 'json' }).$type<InteractionResponse>(),
  106. status: text('status', { enum: ['pending', 'allowed', 'denied', 'answered', 'timeout', 'cancelled'] }).notNull(),
  107. createdAt: createdAt(),
  108. resolvedAt: text('resolved_at'),
  109. expiresAt: text('expires_at').notNull(),
  110. }, t => [index('interactions_session_idx').on(t.sessionId, t.status)])
  111. export const files = sqliteTable('files', {
  112. id: text('id').primaryKey(),
  113. sessionId: text('session_id').notNull(),
  114. relPath: text('rel_path').notNull(),
  115. origin: text('origin', { enum: ['upload', 'agent'] }).notNull(),
  116. mime: text('mime').notNull(),
  117. size: integer('size').notNull(),
  118. mtimeMs: real('mtime_ms').notNull(),
  119. createdAt: createdAt(),
  120. updatedAt: updatedAt(),
  121. }, t => [index('files_session_path_idx').on(t.sessionId, t.relPath)])
  122. export const settings = sqliteTable('settings', {
  123. key: text('key').primaryKey(),
  124. value: text('value', { mode: 'json' }).notNull(),
  125. updatedAt: updatedAt(),
  126. })
  127. export const auditLogs = sqliteTable('audit_logs', {
  128. id: integer('id').primaryKey({ autoIncrement: true }),
  129. ts: text('ts').notNull().default(now),
  130. actor: text('actor').notNull(),
  131. sessionId: text('session_id'),
  132. kind: text('kind', { enum: ['command', 'file_access_denied', 'network_denied', 'approval', 'admin', 'gateway_call'] }).notNull(),
  133. summary: text('summary').notNull(),
  134. detail: text('detail', { mode: 'json' }),
  135. }, t => [index('audit_ts_idx').on(t.ts)])
  136. export const adminSessions = sqliteTable('admin_sessions', {
  137. id: text('id').primaryKey(),
  138. createdAt: createdAt(),
  139. expiresAt: text('expires_at').notNull(),
  140. })
  141. /** 按天汇总的用量(Run 结束时累加;删除会话不影响历史用量)。日期为服务器本地日期 */
  142. export const usageDaily = sqliteTable('usage_daily', {
  143. date: text('date').notNull(),
  144. /** API Key id,或后台的 'admin' */
  145. source: text('source').notNull(),
  146. provider: text('provider').notNull(),
  147. model: text('model').notNull(),
  148. runs: integer('runs').notNull().default(0),
  149. failed: integer('failed').notNull().default(0),
  150. interrupted: integer('interrupted').notNull().default(0),
  151. inputTokens: integer('input_tokens').notNull().default(0),
  152. outputTokens: integer('output_tokens').notNull().default(0),
  153. cacheReadTokens: integer('cache_read_tokens').notNull().default(0),
  154. cacheWriteTokens: integer('cache_write_tokens').notNull().default(0),
  155. costUsd: real('cost_usd').notNull().default(0),
  156. durationMs: integer('duration_ms').notNull().default(0),
  157. }, t => [primaryKey({ columns: [t.date, t.source, t.provider, t.model] })])
  158. /** Profile 的历史版本:每次内容变化保存一份快照(不含"是否默认"),可以查看和恢复 */
  159. export const profileVersions = sqliteTable('profile_versions', {
  160. profileId: text('profile_id').notNull(),
  161. version: integer('version').notNull(),
  162. snapshot: text('snapshot', { mode: 'json' }).$type<ProfileSnapshot>().notNull(),
  163. createdAt: createdAt(),
  164. }, t => [primaryKey({ columns: [t.profileId, t.version] })])
  165. /** 第三方 MCP 服务(经 MCP Gateway 转接,ADR-0021)。headers / env 中含密钥,只在服务端使用 */
  166. export const mcpServers = sqliteTable('mcp_servers', {
  167. id: text('id').primaryKey(),
  168. name: text('name').notNull().unique(),
  169. title: text('title').notNull(),
  170. description: text('description').notNull().default(''),
  171. transport: text('transport', { enum: ['http', 'stdio'] }).notNull(),
  172. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  173. url: text('url').notNull().default(''),
  174. headers: text('headers', { mode: 'json' }).$type<Record<string, string>>().notNull().default(sql`'{}'`),
  175. command: text('command').notNull().default(''),
  176. args: text('args', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  177. env: text('env', { mode: 'json' }).$type<Record<string, string>>().notNull().default(sql`'{}'`),
  178. allowNetwork: integer('allow_network', { mode: 'boolean' }).notNull().default(false),
  179. readPaths: text('read_paths', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  180. writePaths: text('write_paths', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  181. /** 工具策略(工具名 → auto / ask / off);没有设置的工具按默认:只读工具 auto,其余 ask */
  182. toolPolicies: text('tool_policies', { mode: 'json' }).$type<Record<string, McpToolPolicy>>().notNull().default(sql`'{}'`),
  183. /** 总是在后台执行的工具(docs/design/15-background-calls.md §3) */
  184. backgroundTools: text('background_tools', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  185. /** 管理员覆盖的工具提示(工具名 → 只读 / 破坏性 / 幂等,docs/design/16-mcp-support.md §2.1) */
  186. toolHints: text('tool_hints', { mode: 'json' }).$type<Record<string, McpToolHints>>().notNull().default(sql`'{}'`),
  187. /** OAuth 授权(配置、动态注册的客户端信息、令牌、授权过程中的临时值),不出现在 DTO 中 */
  188. oauth: text('oauth', { mode: 'json' }).$type<McpOAuthState>(),
  189. /** 最近一次列出的工具(引擎列工具时直接使用,不必每次启动服务) */
  190. toolsCache: text('tools_cache', { mode: 'json' }).$type<McpCachedTool[]>().notNull().default(sql`'[]'`),
  191. toolsUpdatedAt: text('tools_updated_at'),
  192. /** 服务声明的能力(最近一次连接时记录):资源、提示模板 */
  193. features: text('features', { mode: 'json' }).$type<McpFeatures>().notNull().default(sql`'{}'`),
  194. createdAt: createdAt(),
  195. updatedAt: updatedAt(),
  196. })
  197. export interface McpFeatures {
  198. resources?: boolean
  199. prompts?: boolean
  200. }
  201. export interface McpOAuthState {
  202. enabled: boolean
  203. clientId: string
  204. clientSecret: string
  205. scope: string
  206. /** 回调地址(发起授权时访问后台的地址) */
  207. redirectUrl?: string
  208. /** 动态注册得到的客户端信息 */
  209. clientInfo?: Record<string, unknown>
  210. tokens?: Record<string, unknown>
  211. codeVerifier?: string
  212. /** 授权进行中:state 与过期时间 */
  213. state?: string
  214. stateExpiresAt?: string
  215. authorizedAt?: string
  216. lastError?: string
  217. }
  218. export interface McpCachedTool {
  219. name: string
  220. description: string
  221. inputSchema: Record<string, unknown>
  222. /** 服务声明的只读提示(annotations.readOnlyHint === true) */
  223. readOnly: boolean
  224. title?: string
  225. /** 服务声明的其他提示(未声明的不出现) */
  226. annotations?: { destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; readOnlyHint?: boolean }
  227. outputSchema?: Record<string, unknown>
  228. taskSupport?: 'required' | 'optional'
  229. }
  230. /** 文件清单:相对路径 → 内容摘要与大小(内容存放在按 sha256 寻址的文件库中) */
  231. export type FileMap = Record<string, { sha: string; size: number }>
  232. /** 自定义技能(docs/design/12-custom-skills.md)。运行时只读 custom-skills/skills/<name> 下的生效版本 */
  233. export const customSkills = sqliteTable('custom_skills', {
  234. id: text('id').primaryKey(),
  235. name: text('name').notNull().unique(),
  236. description: text('description').notNull().default(''),
  237. origin: text('origin', { enum: ['authored', 'imported'] }).notNull(),
  238. sourceId: text('source_id'),
  239. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  240. includeInAll: integer('include_in_all', { mode: 'boolean' }).notNull().default(false),
  241. publishedVersion: integer('published_version'),
  242. latestVersion: integer('latest_version').notNull().default(0),
  243. /** 编辑中的文件(尚未保存为版本的改动) */
  244. workingFiles: text('working_files', { mode: 'json' }).$type<FileMap>().notNull().default(sql`'{}'`),
  245. createdAt: createdAt(),
  246. updatedAt: updatedAt(),
  247. })
  248. export const customSkillVersions = sqliteTable('custom_skill_versions', {
  249. skillId: text('skill_id').notNull(),
  250. version: integer('version').notNull(),
  251. reason: text('reason', { enum: ['create', 'edit', 'import', 'sync', 'restore'] }).notNull(),
  252. files: text('files', { mode: 'json' }).$type<FileMap>().notNull(),
  253. changed: text('changed', { mode: 'json' }).$type<{ added: string[]; modified: string[]; removed: string[] }>().notNull(),
  254. createdAt: createdAt(),
  255. }, t => [primaryKey({ columns: [t.skillId, t.version] })])
  256. /** 技能源:服务器目录或上传的 zip(解压在数据目录中) */
  257. export const skillSources = sqliteTable('skill_sources', {
  258. id: text('id').primaryKey(),
  259. name: text('name').notNull(),
  260. description: text('description').notNull().default(''),
  261. kind: text('kind', { enum: ['directory', 'upload'] }).notNull(),
  262. path: text('path').notNull(),
  263. syncMode: text('sync_mode', { enum: ['manual', 'auto'] }).notNull().default('manual'),
  264. intervalMin: integer('interval_min').notNull().default(10),
  265. lastSyncAt: text('last_sync_at'),
  266. lastSyncDigest: text('last_sync_digest'),
  267. status: text('status', { enum: ['never', 'ok', 'error'] }).notNull().default('never'),
  268. lastError: text('last_error'),
  269. createdAt: createdAt(),
  270. updatedAt: updatedAt(),
  271. })
  272. /** 机器人(docs/design/13-bot-teams.md):身份 + 关联的 Profile */
  273. export const bots = sqliteTable('bots', {
  274. id: text('id').primaryKey(),
  275. name: text('name').notNull().unique(),
  276. avatar: text('avatar').notNull().default('🤖'),
  277. kind: text('kind', { enum: ['lead', 'worker'] }).notNull(),
  278. description: text('description').notNull().default(''),
  279. profileId: text('profile_id').notNull(),
  280. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  281. createdAt: createdAt(),
  282. updatedAt: updatedAt(),
  283. })
  284. /** Bot API 的密钥(ADR-0024),与 api_keys 互不通用 */
  285. export const botApiKeys = sqliteTable('bot_api_keys', {
  286. id: text('id').primaryKey(),
  287. name: text('name').notNull(),
  288. keyHash: text('key_hash').notNull().unique(),
  289. keyPrefix: text('key_prefix').notNull(),
  290. enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
  291. allowedBotIds: text('allowed_bot_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  292. createdAt: createdAt(),
  293. lastUsedAt: text('last_used_at'),
  294. })
  295. export const teams = sqliteTable('teams', {
  296. id: text('id').primaryKey(),
  297. /** Bot Key id,或后台的 'admin' */
  298. source: text('source').notNull(),
  299. title: text('title').notNull().default(''),
  300. human: text('human', { mode: 'json' }).$type<{ id: string; name: string }>().notNull(),
  301. status: text('status', { enum: ['active', 'paused', 'closed'] }).notNull().default('active'),
  302. /** 共享目录:组内所有机器人会话的工作目录 */
  303. workspaceDir: text('workspace_dir').notNull(),
  304. /** 限额:创建时以系统设置为准,可以调低 */
  305. limits: text('limits', { mode: 'json' }).$type<Partial<TeamLimits>>(),
  306. /** 调试设置(仅 admin 的工作组) */
  307. debug: text('debug', { mode: 'json' }).$type<TeamDebug>().notNull().default(sql`'{"stepMode":false,"stubBotIds":[]}'`),
  308. lastSeq: integer('last_seq').notNull().default(0),
  309. /** 由回归用例重放创建时的用例 id */
  310. caseId: text('case_id'),
  311. createdAt: createdAt(),
  312. updatedAt: updatedAt(),
  313. }, t => [index('teams_source_idx').on(t.source, t.updatedAt), index('teams_case_idx').on(t.caseId)])
  314. export const teamBots = sqliteTable('team_bots', {
  315. teamId: text('team_id').notNull(),
  316. botId: text('bot_id').notNull(),
  317. role: text('role', { enum: ['lead', 'worker'] }).notNull(),
  318. joinedAt: text('joined_at').notNull().default(now),
  319. }, t => [primaryKey({ columns: [t.teamId, t.botId] }), index('team_bots_bot_idx').on(t.botId)])
  320. /** 每个"机器人 × 工作组"一个引擎会话;离开工作组后保留,再加入时续接 */
  321. export const teamBotSessions = sqliteTable('team_bot_sessions', {
  322. teamId: text('team_id').notNull(),
  323. botId: text('bot_id').notNull(),
  324. sessionId: text('session_id').notNull().unique(),
  325. /** 已交给这个机器人的工作组事件 seq */
  326. lastReadSeq: integer('last_read_seq').notNull().default(0),
  327. /** 成员要求开始新的会话:下一轮不再续接 */
  328. resetRequested: integer('reset_requested', { mode: 'boolean' }).notNull().default(false),
  329. }, t => [primaryKey({ columns: [t.teamId, t.botId] })])
  330. export const teamEvents = sqliteTable('team_events', {
  331. teamId: text('team_id').notNull(),
  332. seq: integer('seq').notNull(),
  333. type: text('type').notNull(),
  334. payload: text('payload', { mode: 'json' }).notNull(),
  335. ts: text('ts').notNull(),
  336. }, t => [primaryKey({ columns: [t.teamId, t.seq] })])
  337. /** 任务:成员交给组长的一项复杂工作,同一工作组内依次执行 */
  338. export const jobs = sqliteTable('jobs', {
  339. id: text('id').primaryKey(),
  340. teamId: text('team_id').notNull(),
  341. no: integer('no').notNull(),
  342. title: text('title').notNull(),
  343. plan: text('plan').notNull().default(''),
  344. status: text('status', { enum: ['running', 'blocked', 'completed', 'failed', 'cancelled'] }).notNull().default('running'),
  345. summary: text('summary'),
  346. files: text('files', { mode: 'json' }).$type<TeamFileRef[]>().notNull().default(sql`'[]'`),
  347. blockedBy: text('blocked_by').$type<TeamLimitKey>(),
  348. /** 本任务生效的限额(继续后加倍) */
  349. limits: text('limits', { mode: 'json' }).$type<Record<TeamLimitKey, number | null>>().notNull(),
  350. leadRuns: integer('lead_runs').notNull().default(0),
  351. /** 本任务产生的全部 Run(组长与子任务),用于统计成本 */
  352. runIds: text('run_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  353. createdAt: createdAt(),
  354. endedAt: text('ended_at'),
  355. }, t => [index('jobs_team_idx').on(t.teamId, t.no)])
  356. /** 子任务:组长分派给某个组员的单项工作 */
  357. export const tasks = sqliteTable('tasks', {
  358. id: text('id').primaryKey(),
  359. jobId: text('job_id').notNull(),
  360. teamId: text('team_id').notNull(),
  361. no: integer('no').notNull(),
  362. botId: text('bot_id').notNull(),
  363. title: text('title').notNull(),
  364. instructions: text('instructions').notNull(),
  365. dependsOn: text('depends_on', { mode: 'json' }).$type<number[]>().notNull().default(sql`'[]'`),
  366. status: text('status', { enum: ['waiting', 'queued', 'running', 'stub', 'background', 'completed', 'failed', 'cancelled'] }).notNull(),
  367. /** 等待的后台调用结束后,组员下一轮收到的后续输入(docs/design/15-background-calls.md §4) */
  368. followUp: text('follow_up'),
  369. result: text('result'),
  370. files: text('files', { mode: 'json' }).$type<TeamFileRef[]>().notNull().default(sql`'[]'`),
  371. runId: text('run_id'),
  372. sessionId: text('session_id'),
  373. createdAt: createdAt(),
  374. startedAt: text('started_at'),
  375. endedAt: text('ended_at'),
  376. }, t => [index('tasks_team_idx').on(t.teamId, t.no), index('tasks_job_idx').on(t.jobId)])
  377. /** 回归用例(docs/design/13-bot-teams.md §12.2):成员组成 + 第一条命令,批量重放后人工比对结果 */
  378. export const teamCases = sqliteTable('team_cases', {
  379. id: text('id').primaryKey(),
  380. name: text('name').notNull(),
  381. leadBotId: text('lead_bot_id'),
  382. workerBotIds: text('worker_bot_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  383. stubBotIds: text('stub_bot_ids', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  384. limits: text('limits', { mode: 'json' }).$type<Partial<TeamLimits>>(),
  385. humanName: text('human_name').notNull(),
  386. text: text('text').notNull(),
  387. mentions: text('mentions', { mode: 'json' }).$type<string[]>().notNull().default(sql`'[]'`),
  388. /** 第一条命令的附件,复制到 <dataDir>/data/team-cases/<id>/ 下 */
  389. files: text('files', { mode: 'json' }).$type<{ name: string; size: number }[]>().notNull().default(sql`'[]'`),
  390. createdAt: createdAt(),
  391. updatedAt: updatedAt(),
  392. })
  393. /** 定时任务(docs/design/14-schedules.md):属于一个会话或一个工作组 */
  394. export const schedules = sqliteTable('schedules', {
  395. id: text('id').primaryKey(),
  396. sessionId: text('session_id'),
  397. teamId: text('team_id'),
  398. /** 工作组中创建它的机器人(到点的消息 @ 组员时使用) */
  399. createdByBotId: text('created_by_bot_id'),
  400. title: text('title').notNull(),
  401. prompt: text('prompt').notNull(),
  402. spec: text('spec', { mode: 'json' }).$type<ScheduleSpec>().notNull(),
  403. timezone: text('timezone').notNull(),
  404. status: text('status', { enum: ['active', 'paused', 'ended'] }).notNull().default('active'),
  405. statusNote: text('status_note'),
  406. nextRunAt: text('next_run_at'),
  407. lastRunAt: text('last_run_at'),
  408. lastOutcome: text('last_outcome').$type<ScheduleOutcome>(),
  409. runCount: integer('run_count').notNull().default(0),
  410. createdAt: createdAt(),
  411. updatedAt: updatedAt(),
  412. }, t => [index('schedules_due_idx').on(t.status, t.nextRunAt), index('schedules_session_idx').on(t.sessionId), index('schedules_team_idx').on(t.teamId)])
  413. export const scheduleRuns = sqliteTable('schedule_runs', {
  414. id: integer('id').primaryKey({ autoIncrement: true }),
  415. scheduleId: text('schedule_id').notNull(),
  416. ts: text('ts').notNull().default(now),
  417. outcome: text('outcome').$type<ScheduleOutcome>().notNull(),
  418. runId: text('run_id'),
  419. costUsd: real('cost_usd'),
  420. note: text('note'),
  421. }, t => [index('schedule_runs_idx').on(t.scheduleId, t.id)])
  422. /** 后台调用(docs/design/15-background-calls.md):外部工具的长调用,由服务端等待,完成后通知发起方 */
  423. export const backgroundCalls = sqliteTable('background_calls', {
  424. id: text('id').primaryKey(),
  425. sessionId: text('session_id').notNull(),
  426. teamId: text('team_id'),
  427. botId: text('bot_id'),
  428. taskId: text('task_id'),
  429. serverId: text('server_id').notNull(),
  430. serverName: text('server_name').notNull(),
  431. serverTitle: text('server_title').notNull(),
  432. tool: text('tool').notNull(),
  433. args: text('args', { mode: 'json' }).notNull(),
  434. status: text('status', { enum: ['running', 'completed', 'failed', 'cancelled'] }).notNull().default('running'),
  435. startedAt: text('started_at').notNull().default(now),
  436. endedAt: text('ended_at'),
  437. result: text('result'),
  438. error: text('error'),
  439. delivered: integer('delivered', { mode: 'boolean' }).notNull().default(false),
  440. /** 等待方式:wait 保持连接等待;task 以长任务方式调用、按任务 id 查询(docs/design/16-mcp-support.md §3) */
  441. mode: text('mode', { enum: ['wait', 'task'] }).notNull().default('wait'),
  442. /** 长任务:服务返回的任务 id、调用时使用的连接(会话)、下次查询时间、查询连续出错的起始时间 */
  443. mcpTaskId: text('mcp_task_id'),
  444. connScope: text('conn_scope'),
  445. pollAfter: text('poll_after'),
  446. pollErrorSince: text('poll_error_since'),
  447. statusMessage: text('status_message'),
  448. }, 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)])