@turingflow/agent-kit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +546 -2
  2. package/dist/agent.d.ts +27 -0
  3. package/dist/agent.js +31 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +105 -0
  6. package/dist/handlers.d.ts +92 -0
  7. package/dist/handlers.js +211 -0
  8. package/dist/index.d.ts +13 -0
  9. package/dist/index.js +10 -0
  10. package/dist/sdk.d.ts +106 -0
  11. package/dist/sdk.js +309 -0
  12. package/dist/server/agent.d.ts +27 -0
  13. package/dist/server/agent.js +31 -0
  14. package/dist/server/artifact-hooks.d.ts +6 -0
  15. package/dist/server/artifact-hooks.js +37 -0
  16. package/dist/server/artifact-tool-tracker.d.ts +24 -0
  17. package/dist/server/artifact-tool-tracker.js +347 -0
  18. package/dist/server/artifacts.d.ts +227 -0
  19. package/dist/server/artifacts.js +1061 -0
  20. package/dist/server/auth/config.d.ts +34 -0
  21. package/dist/server/auth/config.js +82 -0
  22. package/dist/server/auth/db.d.ts +159 -0
  23. package/dist/server/auth/db.js +680 -0
  24. package/dist/server/auth/handlers.d.ts +27 -0
  25. package/dist/server/auth/handlers.js +297 -0
  26. package/dist/server/auth/index.d.ts +15 -0
  27. package/dist/server/auth/index.js +14 -0
  28. package/dist/server/auth/login-options.d.ts +14 -0
  29. package/dist/server/auth/login-options.js +50 -0
  30. package/dist/server/auth/providers/dingtalk.d.ts +16 -0
  31. package/dist/server/auth/providers/dingtalk.js +177 -0
  32. package/dist/server/auth/providers/feishu.d.ts +15 -0
  33. package/dist/server/auth/providers/feishu.js +100 -0
  34. package/dist/server/auth/providers/index.d.ts +13 -0
  35. package/dist/server/auth/providers/index.js +24 -0
  36. package/dist/server/auth/session.d.ts +48 -0
  37. package/dist/server/auth/session.js +108 -0
  38. package/dist/server/auth/types.d.ts +118 -0
  39. package/dist/server/auth/types.js +7 -0
  40. package/dist/server/browser-agent-mcp.d.ts +67 -0
  41. package/dist/server/browser-agent-mcp.js +608 -0
  42. package/dist/server/cli.d.ts +6 -0
  43. package/dist/server/cli.js +112 -0
  44. package/dist/server/debug-log.d.ts +25 -0
  45. package/dist/server/debug-log.js +86 -0
  46. package/dist/server/handlers.d.ts +141 -0
  47. package/dist/server/handlers.js +523 -0
  48. package/dist/server/index.d.ts +30 -0
  49. package/dist/server/index.js +28 -0
  50. package/dist/server/kit-api.d.ts +23 -0
  51. package/dist/server/kit-api.js +153 -0
  52. package/dist/server/model-sources.d.ts +32 -0
  53. package/dist/server/model-sources.js +58 -0
  54. package/dist/server/oa.d.ts +87 -0
  55. package/dist/server/oa.js +397 -0
  56. package/dist/server/providers/openrouter.d.ts +51 -0
  57. package/dist/server/providers/openrouter.js +283 -0
  58. package/dist/server/render-markdown.d.ts +2 -0
  59. package/dist/server/render-markdown.js +50 -0
  60. package/dist/server/sdk.d.ts +259 -0
  61. package/dist/server/sdk.js +1991 -0
  62. package/dist/server/session-access.d.ts +5 -0
  63. package/dist/server/session-access.js +68 -0
  64. package/dist/server/session-fork.d.ts +10 -0
  65. package/dist/server/session-fork.js +142 -0
  66. package/dist/server/session-store.d.ts +56 -0
  67. package/dist/server/session-store.js +148 -0
  68. package/dist/server/sessions.d.ts +24 -0
  69. package/dist/server/sessions.js +541 -0
  70. package/dist/server/share.d.ts +15 -0
  71. package/dist/server/share.js +433 -0
  72. package/dist/server/tool-activity.d.ts +2 -0
  73. package/dist/server/tool-activity.js +26 -0
  74. package/dist/server/transcription/xiaomi-mimo.d.ts +8 -0
  75. package/dist/server/transcription/xiaomi-mimo.js +86 -0
  76. package/dist/server/upload.d.ts +13 -0
  77. package/dist/server/upload.js +81 -0
  78. package/dist/server/user-files.d.ts +28 -0
  79. package/dist/server/user-files.js +311 -0
  80. package/dist/server/voice.d.ts +25 -0
  81. package/dist/server/voice.js +61 -0
  82. package/dist/server/web-fetch.d.ts +71 -0
  83. package/dist/server/web-fetch.js +330 -0
  84. package/dist/styles.css +3354 -0
  85. package/dist/transcription/xiaomi-mimo.d.ts +8 -0
  86. package/dist/transcription/xiaomi-mimo.js +86 -0
  87. package/dist/ui/index.d.ts +352 -0
  88. package/dist/ui/index.js +7730 -0
  89. package/dist/upload.d.ts +13 -0
  90. package/dist/upload.js +81 -0
  91. package/dist/voice.d.ts +25 -0
  92. package/dist/voice.js +61 -0
  93. package/package.json +74 -4
@@ -0,0 +1,680 @@
1
+ /**
2
+ * agent-kit · 用户数据库层(PostgreSQL)
3
+ *
4
+ * 职责:
5
+ * - 从 env(POSTGRES_*)懒加载连接池(进程级单例);
6
+ * - 幂等执行 schema.sql 建表(migrate);
7
+ * - 提供用户仓储:upsertUser(认证后落库/更新)、findUserByProvider、
8
+ * touchLastLogin。
9
+ *
10
+ * 仅放"公共、可复用"的代码;具体调用方(web 的路由)在 app 侧。
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { fileURLToPath } from "node:url";
14
+ import path from "node:path";
15
+ import { Pool } from "pg";
16
+ import { parseSessionKey } from "./session.js";
17
+ import { randomUUID } from "node:crypto";
18
+ import { copyBranchFiles, mergeBranchSnapshot, nextBranchTitle, selectBranchMessages } from "../session-fork.js";
19
+ /** 运行期可用的 DDL(与 schema.sql 内容一致,作为读不到文件时的兜底)。 */
20
+ const SCHEMA_FALLBACK = `
21
+ CREATE TABLE IF NOT EXISTS users (
22
+ id BIGSERIAL PRIMARY KEY,
23
+ provider VARCHAR(16) NOT NULL,
24
+ union_id VARCHAR(128) NOT NULL,
25
+ open_id VARCHAR(128),
26
+ user_code VARCHAR(128),
27
+ name VARCHAR(128) NOT NULL DEFAULT '',
28
+ avatar TEXT,
29
+ email VARCHAR(256),
30
+ mobile VARCHAR(64),
31
+ dept_name VARCHAR(256),
32
+ raw_profile JSONB,
33
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
34
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
35
+ last_login_at TIMESTAMPTZ
36
+ );
37
+ DO $$ BEGIN
38
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uq_users_provider_union_id') THEN
39
+ ALTER TABLE users ADD CONSTRAINT uq_users_provider_union_id UNIQUE (provider, union_id);
40
+ END IF;
41
+ END $$;
42
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS fs_user_token TEXT;
43
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS fs_refresh_token TEXT;
44
+ CREATE INDEX IF NOT EXISTS idx_users_provider_open_id ON users (provider, open_id);
45
+ CREATE INDEX IF NOT EXISTS idx_users_provider_user_code ON users (provider, user_code);
46
+
47
+ CREATE TABLE IF NOT EXISTS agent_sessions (
48
+ id TEXT PRIMARY KEY,
49
+ user_id VARCHAR(160) NOT NULL,
50
+ title VARCHAR(256) NOT NULL DEFAULT '新对话',
51
+ workspace_dir TEXT,
52
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
53
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
54
+ );
55
+ ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS workspace_dir TEXT;
56
+ ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS user_renamed BOOLEAN NOT NULL DEFAULT false;
57
+ DO $$ BEGIN
58
+ IF EXISTS (
59
+ SELECT 1 FROM information_schema.columns
60
+ WHERE table_name = 'agent_sessions' AND column_name = 'user_id' AND data_type = 'bigint'
61
+ ) THEN
62
+ ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS user_key VARCHAR(160);
63
+ UPDATE agent_sessions s SET user_key = u.provider || ':' || u.union_id
64
+ FROM users u WHERE s.user_id = u.id AND s.user_key IS NULL;
65
+ DELETE FROM agent_sessions WHERE user_key IS NULL; -- 孤儿会话(FK ON DELETE CASCADE 理论无残留)安全删除,避免 SET NOT NULL 失败
66
+ ALTER TABLE agent_sessions DROP COLUMN user_id;
67
+ ALTER TABLE agent_sessions RENAME COLUMN user_key TO user_id;
68
+ ALTER TABLE agent_sessions ALTER COLUMN user_id SET NOT NULL;
69
+ END IF;
70
+ END $$;
71
+ CREATE INDEX IF NOT EXISTS idx_agent_sessions_user ON agent_sessions (user_id, updated_at DESC);
72
+ CREATE TABLE IF NOT EXISTS agent_messages (
73
+ id BIGSERIAL PRIMARY KEY,
74
+ session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
75
+ role VARCHAR(16) NOT NULL,
76
+ content TEXT NOT NULL DEFAULT '',
77
+ images JSONB,
78
+ steps JSONB,
79
+ thinking_secs INTEGER,
80
+ elapsed_secs INTEGER,
81
+ model_info JSONB,
82
+ artifacts JSONB,
83
+ attachment JSONB,
84
+ cost_info JSONB,
85
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
86
+ );
87
+ ALTER TABLE agent_messages ADD COLUMN IF NOT EXISTS steps JSONB;
88
+ ALTER TABLE agent_messages ADD COLUMN IF NOT EXISTS thinking_secs INTEGER;
89
+ ALTER TABLE agent_messages ADD COLUMN IF NOT EXISTS elapsed_secs INTEGER;
90
+ ALTER TABLE agent_messages ADD COLUMN IF NOT EXISTS model_info JSONB;
91
+ ALTER TABLE agent_messages ADD COLUMN IF NOT EXISTS artifacts JSONB;
92
+ ALTER TABLE agent_messages ADD COLUMN IF NOT EXISTS attachment JSONB;
93
+ ALTER TABLE agent_messages ADD COLUMN IF NOT EXISTS cost_info JSONB;
94
+ CREATE INDEX IF NOT EXISTS idx_agent_messages_session ON agent_messages (session_id, id);
95
+
96
+ CREATE TABLE IF NOT EXISTS agent_shares (
97
+ token TEXT PRIMARY KEY,
98
+ session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
99
+ created_by VARCHAR(160) NOT NULL,
100
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
101
+ );
102
+ DO $$ BEGIN
103
+ IF EXISTS (
104
+ SELECT 1 FROM information_schema.columns
105
+ WHERE table_name = 'agent_shares' AND column_name = 'created_by' AND data_type = 'bigint'
106
+ ) THEN
107
+ ALTER TABLE agent_shares ADD COLUMN IF NOT EXISTS created_by_key VARCHAR(160);
108
+ UPDATE agent_shares s SET created_by_key = u.provider || ':' || u.union_id
109
+ FROM users u WHERE s.created_by = u.id AND s.created_by_key IS NULL;
110
+ DELETE FROM agent_shares WHERE created_by_key IS NULL; -- 孤儿分享安全删除,避免 SET NOT NULL 失败
111
+ ALTER TABLE agent_shares DROP COLUMN created_by;
112
+ ALTER TABLE agent_shares RENAME COLUMN created_by_key TO created_by;
113
+ ALTER TABLE agent_shares ALTER COLUMN created_by SET NOT NULL;
114
+ END IF;
115
+ END $$;
116
+ CREATE INDEX IF NOT EXISTS idx_agent_shares_session ON agent_shares (session_id);
117
+ `;
118
+ let pool = null;
119
+ function buildPool() {
120
+ const host = process.env.POSTGRES_HOST;
121
+ const user = process.env.POSTGRES_USER;
122
+ const db = process.env.POSTGRES_DB;
123
+ if (!host || !user || !db)
124
+ return null; // 未配置数据库:认证仍可仅存于会话
125
+ return new Pool({
126
+ host,
127
+ port: Number(process.env.POSTGRES_PORT || 5432),
128
+ user,
129
+ password: process.env.POSTGRES_PASSWORD || "",
130
+ database: db,
131
+ max: 5,
132
+ idleTimeoutMillis: 30_000,
133
+ connectionTimeoutMillis: 10_000,
134
+ });
135
+ }
136
+ /**
137
+ * 进程级一次性自动迁移(幂等 schema.sql):
138
+ * 首次使用数据库时后台执行一次,避免「schema.sql / db.ts 新增列后忘记手动跑
139
+ * npm run db:migrate,导致 INSERT 引用不存在的列而静默失败、会话历史丢空」。
140
+ * 幂等(全部 IF NOT EXISTS)可安全重复;失败只记日志,不影响本次连接使用。
141
+ */
142
+ let migrateOnce = null;
143
+ export function ensureMigrated() {
144
+ if (!migrateOnce) {
145
+ migrateOnce = migrate().catch((e) => {
146
+ console.error("[agent-kit auth/db] 自动迁移失败(不影响本次连接):", e instanceof Error ? e.message : e);
147
+ });
148
+ }
149
+ return migrateOnce;
150
+ }
151
+ // 进程启动即触发迁移(不等首次 DB 调用):新列/新表在第一个请求到达前大概率就绪,
152
+ // 避免「首次 INSERT 与后台迁移竞态 → 列缺失 → 消息落库失败、历史丢空」。
153
+ if (isDbEnabled())
154
+ void ensureMigrated();
155
+ /** 进程级连接池单例;未配置 Postgres 时返回 null。 */
156
+ export function getPool() {
157
+ if (pool)
158
+ return pool;
159
+ pool = buildPool();
160
+ if (pool)
161
+ void ensureMigrated();
162
+ return pool;
163
+ }
164
+ /** 是否已启用数据库(配置了 Postgres 即启用;无开关,数据强制落库)。 */
165
+ export function isDbEnabled() {
166
+ return Boolean(process.env.POSTGRES_HOST && process.env.POSTGRES_DB);
167
+ }
168
+ function loadSchemaSql() {
169
+ try {
170
+ const here = path.dirname(fileURLToPath(import.meta.url));
171
+ return readFileSync(path.join(here, "schema.sql"), "utf8");
172
+ }
173
+ catch {
174
+ return SCHEMA_FALLBACK;
175
+ }
176
+ }
177
+ /** 幂等建表;无数据库配置时静默跳过。 */
178
+ export async function migrate() {
179
+ const p = getPool();
180
+ if (!p) {
181
+ console.warn("[agent-kit auth/db] 未配置 Postgres,跳过 migrate(用户仅存于会话)。");
182
+ return;
183
+ }
184
+ const client = await p.connect();
185
+ try {
186
+ // 幂等建表(全部 IF NOT EXISTS),成功属预期常态,不打日志;
187
+ // 仅失败时由 ensureMigrated 的 catch 打印错误(见下方 console.error)
188
+ await client.query(loadSchemaSql());
189
+ }
190
+ finally {
191
+ client.release();
192
+ }
193
+ }
194
+ function rowToDbUser(row) {
195
+ return {
196
+ id: Number(row.id),
197
+ provider: String(row.provider),
198
+ unionId: String(row.union_id),
199
+ openId: row.open_id ?? null,
200
+ userCode: row.user_code ?? null,
201
+ name: String(row.name ?? ""),
202
+ avatar: row.avatar ?? null,
203
+ email: row.email ?? null,
204
+ mobile: row.mobile ?? null,
205
+ deptName: row.dept_name ?? null,
206
+ createdAt: row.created_at,
207
+ updatedAt: row.updated_at,
208
+ lastLoginAt: row.last_login_at ?? null,
209
+ };
210
+ }
211
+ /**
212
+ * 将认证用户 upsert 到 users 表:按 (provider, union_id) 匹配,存在则更新档案与
213
+ * last_login_at,不存在则插入。返回数据库行(含内部 id)。
214
+ */
215
+ export async function upsertUser(user, client) {
216
+ const p = client ?? getPool();
217
+ if (!p)
218
+ return null;
219
+ const sql = `
220
+ INSERT INTO users
221
+ (provider, union_id, open_id, user_code, name, avatar, email, mobile, dept_name, raw_profile,
222
+ fs_user_token, fs_refresh_token, last_login_at, updated_at)
223
+ VALUES
224
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), now())
225
+ ON CONFLICT (provider, union_id) DO UPDATE SET
226
+ open_id = COALESCE(EXCLUDED.open_id, users.open_id),
227
+ user_code = COALESCE(EXCLUDED.user_code, users.user_code),
228
+ name = COALESCE(NULLIF(EXCLUDED.name, ''), users.name),
229
+ avatar = COALESCE(EXCLUDED.avatar, users.avatar),
230
+ email = COALESCE(EXCLUDED.email, users.email),
231
+ mobile = COALESCE(EXCLUDED.mobile, users.mobile),
232
+ dept_name = COALESCE(EXCLUDED.dept_name, users.dept_name),
233
+ raw_profile = COALESCE(EXCLUDED.raw_profile, users.raw_profile),
234
+ fs_user_token = COALESCE(EXCLUDED.fs_user_token, users.fs_user_token),
235
+ fs_refresh_token = COALESCE(EXCLUDED.fs_refresh_token, users.fs_refresh_token),
236
+ last_login_at = now(),
237
+ updated_at = now()
238
+ RETURNING *;
239
+ `;
240
+ const values = [
241
+ user.provider,
242
+ user.unionId,
243
+ user.openId || null,
244
+ user.userCode || null,
245
+ user.name || "",
246
+ user.avatar || null,
247
+ user.email || null,
248
+ user.mobile || null,
249
+ user.deptName || null,
250
+ user.raw ? JSON.parse(JSON.stringify(user.raw)) : null,
251
+ user.oauthToken?.accessToken || null,
252
+ user.oauthToken?.refreshToken || null,
253
+ ];
254
+ const runner = client ?? p;
255
+ const result = await runner.query(sql, values);
256
+ return result.rows[0] ? rowToDbUser(result.rows[0]) : null;
257
+ }
258
+ /** 按 (provider, union_id) 查用户;不存在返回 null。 */
259
+ export async function findUserByProvider(provider, unionId, client) {
260
+ const p = client ?? getPool();
261
+ if (!p)
262
+ return null;
263
+ const runner = client ?? p;
264
+ const result = await runner.query("SELECT * FROM users WHERE provider = $1 AND union_id = $2 LIMIT 1", [provider, unionId]);
265
+ return result.rows[0] ? rowToDbUser(result.rows[0]) : null;
266
+ }
267
+ /** 仅刷新 last_login_at(登录成功时调用)。 */
268
+ export async function touchLastLogin(id, client) {
269
+ const p = client ?? getPool();
270
+ if (!p)
271
+ return;
272
+ const runner = client ?? p;
273
+ await runner.query("UPDATE users SET last_login_at = now() WHERE id = $1", [id]);
274
+ }
275
+ /** 取飞书 OAuth 令牌(用户身份发消息用);无令牌或非飞书用户返回 null。 */
276
+ export async function getUserOauthTokenByKey(userId) {
277
+ const p = getPool();
278
+ if (!p)
279
+ return null;
280
+ const { provider, unionId } = parseSessionKey(userId);
281
+ if (!provider || !unionId)
282
+ return null;
283
+ const result = await p.query("SELECT provider, fs_user_token, fs_refresh_token FROM users WHERE provider = $1 AND union_id = $2 LIMIT 1", [provider, unionId]);
284
+ const row = result.rows[0];
285
+ if (!row || row.provider !== "feishu")
286
+ return null;
287
+ return {
288
+ accessToken: row.fs_user_token || null,
289
+ refreshToken: row.fs_refresh_token || null,
290
+ };
291
+ }
292
+ /** 回写飞书 OAuth 令牌(刷新续期后调用;refreshToken 为空时保留原值)。 */
293
+ export async function updateUserOauthToken(userId, token) {
294
+ const p = getPool();
295
+ if (!p)
296
+ return;
297
+ const { provider, unionId } = parseSessionKey(userId);
298
+ if (!provider || !unionId)
299
+ return;
300
+ await p.query("UPDATE users SET fs_user_token = $3, fs_refresh_token = COALESCE($4, fs_refresh_token), updated_at = now() WHERE provider = $1 AND union_id = $2", [provider, unionId, token.accessToken, token.refreshToken || null]);
301
+ }
302
+ function rowToSession(row) {
303
+ return {
304
+ id: String(row.id),
305
+ userId: String(row.user_id),
306
+ title: String(row.title ?? "新任务"),
307
+ workspaceDir: row.workspace_dir || null,
308
+ createdAt: row.created_at,
309
+ updatedAt: row.updated_at,
310
+ messageCount: Number(row.message_count ?? 0),
311
+ lastPreview: row.last_preview ?? null,
312
+ };
313
+ }
314
+ function rowToMessage(row) {
315
+ return {
316
+ id: Number(row.id),
317
+ sessionId: String(row.session_id),
318
+ role: String(row.role),
319
+ content: String(row.content ?? ""),
320
+ images: row.images ?? null,
321
+ steps: row.steps ?? null,
322
+ thinkingSecs: row.thinking_secs != null ? Number(row.thinking_secs) : null,
323
+ elapsedSecs: row.elapsed_secs != null ? Number(row.elapsed_secs) : null,
324
+ modelInfo: row.model_info ?? null,
325
+ artifacts: row.artifacts ?? null,
326
+ attachment: row.attachment ?? null,
327
+ costInfo: row.cost_info ?? null,
328
+ createdAt: row.created_at,
329
+ };
330
+ }
331
+ /** 注册/复用会话:已存在则返回现有行,不存在则插入。workspaceDir 仅首建时生效(复用不改)。 */
332
+ export async function createAgentSession(userId, sessionId, title = "新任务", workspaceDir, client) {
333
+ const p = client ?? getPool();
334
+ if (!p)
335
+ return null;
336
+ const runner = client ?? p;
337
+ const result = await runner.query(`INSERT INTO agent_sessions (id, user_id, title, workspace_dir)
338
+ VALUES ($1, $2, $3, $4)
339
+ ON CONFLICT (id) DO UPDATE SET updated_at = now()
340
+ RETURNING *`, [sessionId, userId, title.slice(0, 250), workspaceDir ? workspaceDir.slice(0, 1024) : null]);
341
+ return result.rows[0] ? rowToSession(result.rows[0]) : null;
342
+ }
343
+ /** 当前用户的会话列表(按最近更新倒序),附带消息数与最后一条消息预览。 */
344
+ export async function listAgentSessions(userId, client) {
345
+ const p = client ?? getPool();
346
+ if (!p)
347
+ return [];
348
+ const runner = client ?? p;
349
+ const result = await runner.query(`SELECT s.*,
350
+ (SELECT count(*) FROM agent_messages m WHERE m.session_id = s.id) AS message_count,
351
+ (SELECT m.content FROM agent_messages m WHERE m.session_id = s.id ORDER BY m.id DESC LIMIT 1) AS last_preview
352
+ FROM agent_sessions s
353
+ WHERE s.user_id = $1
354
+ ORDER BY s.updated_at DESC
355
+ LIMIT 200`, [userId]);
356
+ return result.rows.map(rowToSession);
357
+ }
358
+ /** 校验会话归属并返回该会话;不属于当前用户或不存在时返回 null。 */
359
+ export async function getAgentSession(userId, sessionId, client) {
360
+ const p = client ?? getPool();
361
+ if (!p)
362
+ return null;
363
+ const runner = client ?? p;
364
+ const result = await runner.query(`SELECT s.*,
365
+ (SELECT count(*) FROM agent_messages m WHERE m.session_id = s.id) AS message_count,
366
+ (SELECT m.content FROM agent_messages m WHERE m.session_id = s.id ORDER BY m.id DESC LIMIT 1) AS last_preview
367
+ FROM agent_sessions s
368
+ WHERE s.user_id = $1 AND s.id = $2
369
+ LIMIT 1`, [userId, sessionId]);
370
+ return result.rows[0] ? rowToSession(result.rows[0]) : null;
371
+ }
372
+ /** 重命名会话(仅限归属用户)。 */
373
+ export async function renameAgentSession(userId, sessionId, title, userRenamed = false, client) {
374
+ const p = client ?? getPool();
375
+ if (!p)
376
+ return false;
377
+ const runner = client ?? p;
378
+ const result = await runner.query(`UPDATE agent_sessions SET title = $3, user_renamed = $4, updated_at = now()
379
+ WHERE user_id = $1 AND id = $2`, [userId, sessionId, title.slice(0, 250), userRenamed]);
380
+ return (result.rowCount ?? 0) > 0;
381
+ }
382
+ /** SDK 自动命名(topic / ai-title)落库:仅当用户从未手动改过标题时覆盖(user_renamed=false),
383
+ * 否则跳过,保护用户手动编辑的标题不被自动命名替换。 */
384
+ export async function setSessionTitleIfNotUserRenamed(userId, sessionId, title, client) {
385
+ const p = client ?? getPool();
386
+ if (!p)
387
+ return false;
388
+ const runner = client ?? p;
389
+ const result = await runner.query(`UPDATE agent_sessions SET title = $3, updated_at = now()
390
+ WHERE user_id = $1 AND id = $2 AND NOT user_renamed`, [userId, sessionId, title.slice(0, 250)]);
391
+ return (result.rowCount ?? 0) > 0;
392
+ }
393
+ /** 更新会话工作目录(沙箱范围;传 null 表示清除限定)。仅限归属用户。 */
394
+ export async function updateAgentSessionWorkspace(userId, sessionId, workspaceDir, client) {
395
+ const p = client ?? getPool();
396
+ if (!p)
397
+ return false;
398
+ const runner = client ?? p;
399
+ const result = await runner.query(`UPDATE agent_sessions SET workspace_dir = $3, updated_at = now()
400
+ WHERE user_id = $1 AND id = $2`, [userId, sessionId, workspaceDir ? workspaceDir.slice(0, 1024) : null]);
401
+ return (result.rowCount ?? 0) > 0;
402
+ }
403
+ /** 删除会话(消息级联删除)。 */
404
+ export async function deleteAgentSession(userId, sessionId, client) {
405
+ const p = client ?? getPool();
406
+ if (!p)
407
+ return false;
408
+ const runner = client ?? p;
409
+ const result = await runner.query("DELETE FROM agent_sessions WHERE user_id = $1 AND id = $2", [userId, sessionId]);
410
+ return (result.rowCount ?? 0) > 0;
411
+ }
412
+ /** admin 用量聚合(暂不鉴权):summary 总量 + 按模型 / 按会话 / 单条消息 Top 消耗。
413
+ * 数据源:agent_messages.cost_info JSONB { costUsd, usage: { input_tokens, output_tokens } }。 */
414
+ export async function queryAdminUsage(limit = 100) {
415
+ const p = getPool();
416
+ if (!p)
417
+ return null;
418
+ const [sum, byModel, bySession, details] = await Promise.all([
419
+ p.query(`SELECT
420
+ COALESCE(SUM((m.cost_info->'usage'->>'input_tokens')::bigint), 0)::bigint AS input_tokens,
421
+ COALESCE(SUM((m.cost_info->'usage'->>'output_tokens')::bigint), 0)::bigint AS output_tokens,
422
+ COALESCE(SUM((m.cost_info->>'costUsd')::double precision), 0)::double precision AS cost_usd,
423
+ COUNT(*) FILTER (WHERE m.cost_info IS NOT NULL) AS costed_msgs,
424
+ COUNT(DISTINCT m.session_id) AS sessions
425
+ FROM agent_messages m WHERE (m.cost_info->>'inherited') IS DISTINCT FROM 'true'`),
426
+ p.query(`SELECT
427
+ COALESCE(m.model_info->'models'->>0, '?') AS model,
428
+ COALESCE(SUM((m.cost_info->'usage'->>'input_tokens')::bigint), 0)::bigint AS input_tokens,
429
+ COALESCE(SUM((m.cost_info->'usage'->>'output_tokens')::bigint), 0)::bigint AS output_tokens,
430
+ COUNT(*) AS msgs
431
+ FROM agent_messages m
432
+ WHERE m.cost_info IS NOT NULL AND (m.cost_info->>'inherited') IS DISTINCT FROM 'true'
433
+ GROUP BY 1
434
+ ORDER BY input_tokens DESC`),
435
+ p.query(`SELECT
436
+ s.id AS session_id, s.title, s.created_at,
437
+ COALESCE(SUM((m.cost_info->'usage'->>'input_tokens')::bigint), 0)::bigint AS input_tokens,
438
+ COALESCE(SUM((m.cost_info->'usage'->>'output_tokens')::bigint), 0)::bigint AS output_tokens,
439
+ COALESCE(SUM((m.cost_info->>'costUsd')::double precision), 0)::double precision AS cost_usd,
440
+ COUNT(*) FILTER (WHERE m.cost_info IS NOT NULL) AS costed_msgs
441
+ FROM agent_sessions s
442
+ JOIN agent_messages m ON m.session_id = s.id
443
+ WHERE m.cost_info IS NOT NULL AND (m.cost_info->>'inherited') IS DISTINCT FROM 'true'
444
+ GROUP BY s.id, s.title, s.created_at
445
+ ORDER BY input_tokens DESC`),
446
+ p.query(`SELECT
447
+ m.id, m.session_id, s.title AS session_title, s.user_id,
448
+ u.name AS user_name,
449
+ m.created_at,
450
+ (m.cost_info->'usage'->>'input_tokens')::bigint AS input_tokens,
451
+ (m.cost_info->'usage'->>'output_tokens')::bigint AS output_tokens,
452
+ (m.cost_info->>'costUsd')::double precision AS cost_usd,
453
+ COALESCE(m.model_info->'models'->>0, '?') AS model,
454
+ left(m.content, 200) AS content_preview,
455
+ -- 该轮的用户提问:同一会话中、本条消息之前最近的一条 user 消息内容
456
+ (
457
+ SELECT left(u2.content, 200)
458
+ FROM agent_messages u2
459
+ WHERE u2.session_id = m.session_id AND u2.role = 'user' AND u2.id < m.id
460
+ ORDER BY u2.id DESC
461
+ LIMIT 1
462
+ ) AS user_content
463
+ FROM agent_messages m
464
+ JOIN agent_sessions s ON s.id = m.session_id
465
+ LEFT JOIN users u ON u.provider || ':' || u.union_id = s.user_id
466
+ WHERE m.cost_info IS NOT NULL AND (m.cost_info->>'inherited') IS DISTINCT FROM 'true'
467
+ ORDER BY (m.cost_info->'usage'->>'input_tokens')::bigint DESC
468
+ LIMIT $1`, [Math.max(1, Math.min(500, limit))]),
469
+ ]);
470
+ const num = (v) => Number(v ?? 0);
471
+ const summary = {
472
+ inputTokens: num(sum.rows[0]?.input_tokens),
473
+ outputTokens: num(sum.rows[0]?.output_tokens),
474
+ costUsd: num(sum.rows[0]?.cost_usd),
475
+ costedMsgs: num(sum.rows[0]?.costed_msgs),
476
+ sessions: num(sum.rows[0]?.sessions),
477
+ };
478
+ return {
479
+ summary,
480
+ byModel: byModel.rows.map((r) => ({
481
+ model: String(r.model ?? "?"),
482
+ inputTokens: num(r.input_tokens),
483
+ outputTokens: num(r.output_tokens),
484
+ msgs: num(r.msgs),
485
+ })),
486
+ bySession: bySession.rows.map((r) => ({
487
+ sessionId: String(r.session_id),
488
+ title: String(r.title ?? ""),
489
+ createdAt: r.created_at,
490
+ inputTokens: num(r.input_tokens),
491
+ outputTokens: num(r.output_tokens),
492
+ costUsd: num(r.cost_usd),
493
+ costedMsgs: num(r.costed_msgs),
494
+ })),
495
+ details: details.rows.map((r) => ({
496
+ id: num(r.id),
497
+ sessionId: String(r.session_id),
498
+ sessionTitle: String(r.session_title ?? ""),
499
+ createdAt: r.created_at,
500
+ inputTokens: num(r.input_tokens),
501
+ outputTokens: num(r.output_tokens),
502
+ costUsd: num(r.cost_usd),
503
+ model: String(r.model ?? "?"),
504
+ contentPreview: String(r.content_preview ?? ""),
505
+ userContent: r.user_content != null ? String(r.user_content) : "",
506
+ user: r.user_name != null ? String(r.user_name) : (r.user_id != null ? String(r.user_id) : ""),
507
+ })),
508
+ };
509
+ }
510
+ /** 追加一条消息并刷新会话更新时间。 */
511
+ export async function addAgentMessage(sessionId, role, content, images, steps, thinkingSecs, modelInfo, artifacts, attachment, elapsedSecs, costInfo, client) {
512
+ const p = client ?? getPool();
513
+ if (!p)
514
+ return false;
515
+ // 写入前等待迁移完成(幂等,毫秒级;防止新列在引擎刚启动的首条消息上缺失)
516
+ await ensureMigrated();
517
+ const runner = client ?? p;
518
+ const result = await runner.query(`INSERT INTO agent_messages (session_id, role, content, images, steps, thinking_secs, model_info, artifacts, attachment, elapsed_secs, cost_info)
519
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
520
+ ON CONFLICT DO NOTHING`, [
521
+ sessionId,
522
+ role,
523
+ content,
524
+ images ? JSON.stringify(images) : null,
525
+ steps ? JSON.stringify(steps) : null,
526
+ thinkingSecs != null ? Math.round(thinkingSecs) : null,
527
+ modelInfo ? JSON.stringify(modelInfo) : null,
528
+ artifacts && Array.isArray(artifacts) && artifacts.length ? JSON.stringify(artifacts) : null,
529
+ attachment && typeof attachment === "object" ? JSON.stringify(attachment) : null,
530
+ elapsedSecs != null ? Math.round(elapsedSecs) : null,
531
+ costInfo && typeof costInfo === "object" ? JSON.stringify(costInfo) : null,
532
+ ]);
533
+ await runner.query("UPDATE agent_sessions SET updated_at = now() WHERE id = $1", [sessionId]);
534
+ return true;
535
+ }
536
+ /** 会话内全部消息(按时间正序);不属于当前用户返回空数组。 */
537
+ export async function listAgentMessages(userId, sessionId, client) {
538
+ const p = client ?? getPool();
539
+ if (!p)
540
+ return [];
541
+ const runner = client ?? p;
542
+ const result = await runner.query(`SELECT m.*
543
+ FROM agent_messages m
544
+ JOIN agent_sessions s ON s.id = m.session_id
545
+ WHERE s.user_id = $1 AND m.session_id = $2
546
+ ORDER BY m.id ASC`, [userId, sessionId]);
547
+ return result.rows.map(rowToMessage);
548
+ }
549
+ /** Copy a bounded history in one transaction; serialize suffix allocation per owner. */
550
+ export async function forkAgentSession(userId, sourceId, messageIndex, messageId, snapshot) {
551
+ const pool = getPool();
552
+ if (!pool)
553
+ throw new Error("数据库不可用");
554
+ await ensureMigrated();
555
+ const client = await pool.connect();
556
+ let cleanup;
557
+ try {
558
+ await client.query("BEGIN");
559
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [userId]);
560
+ const source = await client.query("SELECT * FROM agent_sessions WHERE id = $1 AND user_id = $2 FOR UPDATE", [sourceId, userId]);
561
+ if (!source.rows[0])
562
+ throw new Error("会话不存在或无权访问");
563
+ const history = await listAgentMessages(userId, sourceId, client);
564
+ const selected = mergeBranchSnapshot(selectBranchMessages(history, messageIndex, messageId), snapshot);
565
+ const titles = await client.query("SELECT title FROM agent_sessions WHERE user_id = $1", [userId]);
566
+ const title = nextBranchTitle(String(source.rows[0].title), titles.rows.map((row) => String(row.title)));
567
+ const id = randomUUID();
568
+ const workspace = source.rows[0].workspace_dir;
569
+ const prepared = await copyBranchFiles(sourceId, id, selected, workspace);
570
+ cleanup = prepared.cleanup;
571
+ const inserted = await client.query("INSERT INTO agent_sessions (id, user_id, title, workspace_dir, user_renamed) VALUES ($1, $2, $3, $4, true) RETURNING *", [id, userId, title, workspace ?? null]);
572
+ for (const message of prepared.messages) {
573
+ await client.query(`INSERT INTO agent_messages (session_id, role, content, images, steps, thinking_secs, elapsed_secs, model_info, artifacts, attachment, cost_info, created_at)
574
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)`, [id, message.role, message.content,
575
+ JSON.stringify(message.images), JSON.stringify(message.steps), message.thinkingSecs, message.elapsedSecs,
576
+ JSON.stringify(message.modelInfo), JSON.stringify(message.artifacts), JSON.stringify(message.attachment), JSON.stringify(message.costInfo && typeof message.costInfo === "object" ? { ...message.costInfo, inherited: true } : null), message.createdAt]);
577
+ }
578
+ await client.query("COMMIT");
579
+ return { ...rowToSession(inserted.rows[0]), messageCount: selected.length, lastPreview: selected.at(-1)?.content ?? null };
580
+ }
581
+ catch (error) {
582
+ await client.query("ROLLBACK").catch(() => { });
583
+ await cleanup?.();
584
+ throw error;
585
+ }
586
+ finally {
587
+ client.release();
588
+ }
589
+ }
590
+ /** 会话内所有用户消息的附件 url 列表(删会话清理磁盘文件前收集,须在删除会话前调用)。 */
591
+ export async function listSessionAttachmentUrls(userId, sessionId, client) {
592
+ const p = client ?? getPool();
593
+ if (!p)
594
+ return [];
595
+ const runner = client ?? p;
596
+ const result = await runner.query(`SELECT m.attachment
597
+ FROM agent_messages m
598
+ JOIN agent_sessions s ON s.id = m.session_id
599
+ WHERE s.user_id = $1 AND m.session_id = $2 AND m.role = 'user' AND m.attachment IS NOT NULL`, [userId, sessionId]);
600
+ const urls = [];
601
+ for (const row of result.rows) {
602
+ const att = row.attachment;
603
+ if (att && typeof att.url === "string" && att.url)
604
+ urls.push(att.url);
605
+ }
606
+ return urls;
607
+ }
608
+ /** 截断会话:删除指定 id(含)之后的所有消息(编辑重发时调用,让刷新后不出现旧消息)。 */
609
+ export async function truncateAgentMessages(userId, sessionId, upToId) {
610
+ const p = getPool();
611
+ if (!p)
612
+ return;
613
+ await p.query(`DELETE FROM agent_messages m
614
+ USING agent_sessions s
615
+ WHERE s.id = m.session_id AND s.user_id = $1 AND m.session_id = $2 AND m.id > $3`, [userId, sessionId, upToId]);
616
+ }
617
+ /** 按主键查分享记录;不存在返回 null。 */
618
+ export async function getShareByToken(token) {
619
+ const p = getPool();
620
+ if (!p)
621
+ return null;
622
+ const result = await p.query(`SELECT token, session_id, created_by, created_at FROM agent_shares WHERE token = $1 LIMIT 1`, [token]);
623
+ const row = result.rows[0];
624
+ if (!row)
625
+ return null;
626
+ return {
627
+ token: String(row.token),
628
+ sessionId: String(row.session_id),
629
+ createdBy: String(row.created_by),
630
+ createdAt: row.created_at instanceof Date ? row.created_at : new Date(String(row.created_at)),
631
+ };
632
+ }
633
+ /** 创建分享记录;会话必须已存在(外键保证归属校验由调用方完成)。 */
634
+ export async function createShareRecord(token, sessionId, userId) {
635
+ const p = getPool();
636
+ if (!p)
637
+ return false;
638
+ await ensureMigrated();
639
+ try {
640
+ await p.query(`INSERT INTO agent_shares (token, session_id, created_by) VALUES ($1, $2, $3)
641
+ ON CONFLICT (token) DO NOTHING`, [token, sessionId, userId]);
642
+ return true;
643
+ }
644
+ catch (e) {
645
+ console.error(`[agent-kit auth/db] createShareRecord 失败:`, e instanceof Error ? e.message : e);
646
+ return false;
647
+ }
648
+ }
649
+ /** 按 session_id 查会话标题(公开分享页展示用;不校验归属)。 */
650
+ export async function getSessionTitleBySessionId(sessionId) {
651
+ const p = getPool();
652
+ if (!p)
653
+ return "";
654
+ const result = await p.query(`SELECT title FROM agent_sessions WHERE id = $1 LIMIT 1`, [sessionId]);
655
+ const row = result.rows[0];
656
+ return row && typeof row.title === "string" ? row.title : "";
657
+ }
658
+ /** 公开读取会话消息(分享页用;不校验用户归属,依赖分享 token 不可枚举)。 */
659
+ export async function listAgentMessagesPublic(sessionId) {
660
+ const p = getPool();
661
+ if (!p)
662
+ return [];
663
+ const result = await p.query(`SELECT m.*
664
+ FROM agent_messages m
665
+ WHERE m.session_id = $1
666
+ ORDER BY m.id ASC`, [sessionId]);
667
+ return result.rows.map(rowToMessage);
668
+ }
669
+ /** 按主体键 `${provider}:${unionId}` 查钉钉 userid / 飞书 employee_id(OA 发送消息时作为 sender 使用)。 */
670
+ export async function getUserCodeByKey(userId) {
671
+ const p = getPool();
672
+ if (!p)
673
+ return "";
674
+ const { provider, unionId } = parseSessionKey(userId);
675
+ if (!provider || !unionId)
676
+ return "";
677
+ const result = await p.query(`SELECT user_code FROM users WHERE provider = $1 AND union_id = $2 LIMIT 1`, [provider, unionId]);
678
+ const row = result.rows[0];
679
+ return row && typeof row.user_code === "string" ? row.user_code : "";
680
+ }