@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,541 @@
1
+ /**
2
+ * agent-kit · 会话管理 API(侧边栏会话列表增删改查)
3
+ *
4
+ * 路由接线(宿主 app,basePath=/agent):
5
+ * // app/api/sessions/route.js -> GET 列表 / POST 注册新会话
6
+ * // app/api/sessions/[id]/route.js -> PATCH 重命名 / DELETE 删除
7
+ * // app/api/sessions/[id]/messages/route.js -> GET 消息列表 / POST 追加消息
8
+ *
9
+ * 设计要点:
10
+ * - 所有操作以登录用户(verifySession → sub,即 `provider:unionId` 主体键)为归属边界,未登录 / 游客一律 401;
11
+ * - 会话 id 即前端 sessionId(UUID 文本),与 CLI 续接 key 一致;
12
+ * - 数据库未配置时(isDbEnabled=false)返回 503,前端自动降级为纯本地会话。
13
+ */
14
+ import { verifySession } from "./auth/handlers.js";
15
+ import { loadAuthConfig } from "./auth/config.js";
16
+ import { createSessionToken, verifySessionToken, sessionCookieOptions } from "./auth/session.js";
17
+ import { createAgentSession, listAgentSessions, getAgentSession, renameAgentSession, setSessionTitleIfNotUserRenamed, updateAgentSessionWorkspace, deleteAgentSession, addAgentMessage, listAgentMessages, listSessionAttachmentUrls, truncateAgentMessages, isDbEnabled, queryAdminUsage, forkAgentSession, } from "./auth/db.js";
18
+ import { createAgentStream, getModelContextLimit } from "./sdk.js";
19
+ import { slog } from "./debug-log.js";
20
+ import { unlink } from "node:fs/promises";
21
+ import path from "node:path";
22
+ function json(data, status = 200) {
23
+ return new Response(JSON.stringify(data), {
24
+ status,
25
+ headers: { "Content-Type": "application/json; charset=utf-8" },
26
+ });
27
+ }
28
+ /** 解析请求 Cookie 中的指定键值。 */
29
+ function getCookieValue(request, name) {
30
+ const raw = request.headers.get("cookie");
31
+ if (!raw)
32
+ return undefined;
33
+ for (const part of raw.split(";")) {
34
+ const eq = part.indexOf("=");
35
+ if (eq === -1)
36
+ continue;
37
+ if (part.slice(0, eq).trim() === name)
38
+ return part.slice(eq + 1).trim();
39
+ }
40
+ return undefined;
41
+ }
42
+ // ---- admin 用量页登录保护(独立于飞书用户会话)----
43
+ // 用户名 / 密码固定为 admin / admin(本地桌面 app 的轻量保护),登录后签发
44
+ // 12 小时 httpOnly Cookie,usage 接口校验该 Cookie,未登录一律 401。
45
+ const ADMIN_COOKIE = "agent_admin";
46
+ const ADMIN_SUB = "local:admin";
47
+ const ADMIN_MAX_AGE_SECONDS = 60 * 60 * 12; // 12 小时
48
+ /** 校验请求是否携带有效的 admin 登录 Cookie。 */
49
+ function verifyAdmin(request) {
50
+ const config = loadAuthConfig();
51
+ const token = getCookieValue(request, ADMIN_COOKIE);
52
+ if (!token)
53
+ return false;
54
+ const user = verifySessionToken(token, config.sessionSecret);
55
+ return !!user && user.sub === ADMIN_SUB;
56
+ }
57
+ /** 组装 admin Cookie 的 Set-Cookie 头(复用标准会话选项,path 跟随 cookiePath)。 */
58
+ function adminCookieHeader(token) {
59
+ const config = loadAuthConfig();
60
+ const opts = sessionCookieOptions(config.cookiePath, config.secureCookie, ADMIN_MAX_AGE_SECONDS);
61
+ const parts = [`${ADMIN_COOKIE}=${token ?? ""}`, `Path=${opts.path}`];
62
+ if (token)
63
+ parts.push(`Max-Age=${opts.maxAge}`);
64
+ else
65
+ parts.push("Max-Age=0");
66
+ parts.push("HttpOnly");
67
+ parts.push(opts.sameSite === "none" ? "SameSite=None" : "SameSite=Lax");
68
+ if (opts.secure)
69
+ parts.push("Secure");
70
+ return parts.join("; ");
71
+ }
72
+ /** POST /api/admin/login:校验 admin/admin,成功签发登录 Cookie。 */
73
+ async function adminLogin(request) {
74
+ const body = (await request.json().catch(() => ({})));
75
+ const username = typeof body.username === "string" ? body.username : "";
76
+ const password = typeof body.password === "string" ? body.password : "";
77
+ if (username !== "admin" || password !== "admin") {
78
+ return json({ error: "用户名或密码错误" }, 401);
79
+ }
80
+ const config = loadAuthConfig();
81
+ const sessionUser = {
82
+ sub: ADMIN_SUB,
83
+ provider: "builtin",
84
+ unionId: "admin",
85
+ openId: "",
86
+ name: "管理员",
87
+ avatar: "",
88
+ exp: Math.floor(Date.now() / 1000) + ADMIN_MAX_AGE_SECONDS,
89
+ };
90
+ const token = createSessionToken(sessionUser, config.sessionSecret);
91
+ return new Response(JSON.stringify({ ok: true }), {
92
+ status: 200,
93
+ headers: { "Content-Type": "application/json; charset=utf-8", "Set-Cookie": adminCookieHeader(token) },
94
+ });
95
+ }
96
+ /** POST /api/admin/logout:清除 admin 登录 Cookie。 */
97
+ function adminLogout() {
98
+ return new Response(JSON.stringify({ ok: true }), {
99
+ status: 200,
100
+ headers: { "Content-Type": "application/json; charset=utf-8", "Set-Cookie": adminCookieHeader(null) },
101
+ });
102
+ }
103
+ /** 解析 Next.js 动态路由参数(Next 15 中 params 可能是 Promise)。 */
104
+ async function resolveParams(ctx) {
105
+ const params = (ctx && ctx.params) || {};
106
+ const resolved = params instanceof Promise ? await params : params;
107
+ return resolved;
108
+ }
109
+ /** 跑一次极简 LLM 调用,把用户首条消息总结成 6-12 字中文标题(fire-and-forget,失败返回空串)。
110
+ * 失败不打扰对话:标题保持快速占位(deriveQuickTitle)。 */
111
+ export async function summarizeSessionTitle(userMessage) {
112
+ let title = "";
113
+ // 单次执行(含内部重试):返回 { title, retryable }
114
+ async function runOnce() {
115
+ let t = "";
116
+ try {
117
+ console.log(`[agent-kit] summarizeSessionTitle start: "${userMessage.slice(0, 80)}"`);
118
+ const stream = createAgentStream({
119
+ messages: [
120
+ {
121
+ role: "system",
122
+ content: "你是一个标题生成器,不是对话助手。不要回答用户,不要和用户交流。你的唯一任务是:把下面这段用户输入总结成一个 6-12 字的中文会话标题,概括用户意图或主题。只输出标题本身,不要引号、不要解释、不要句号结尾、不要问候。例如用户说'帮我写个爬虫',你输出'编写爬虫';用户说'hi',你输出'打招呼'。",
123
+ },
124
+ { role: "user", content: `请为以下用户输入生成标题:\n\n${userMessage.slice(0, 500)}` },
125
+ ],
126
+ config: { sessionId: undefined, resume: false, continueSession: false, maxTurns: 1, allowedTools: [], webFetch: false },
127
+ });
128
+ const reader = stream.getReader();
129
+ const decoder = new TextDecoder();
130
+ let buf = "";
131
+ while (true) {
132
+ const { done, value } = await reader.read();
133
+ if (done)
134
+ break;
135
+ buf += decoder.decode(value, { stream: true });
136
+ let idx;
137
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
138
+ const frame = buf.slice(0, idx);
139
+ buf = buf.slice(idx + 2);
140
+ const evM = frame.match(/^event: (.+)$/m);
141
+ const dataM = frame.match(/^data: (.+)$/m);
142
+ if (evM && evM[1] === "text" && dataM) {
143
+ try {
144
+ t += JSON.parse(dataM[1]).text || "";
145
+ }
146
+ catch { /* ignore */ }
147
+ }
148
+ if (evM && evM[1] === "error" && dataM) {
149
+ const raw = dataM[1];
150
+ let detail = raw;
151
+ try {
152
+ detail = JSON.parse(raw).text || raw;
153
+ }
154
+ catch { /* ignore */ }
155
+ console.error(`[agent-kit] summarizeSessionTitle SSE error: ${detail}`);
156
+ }
157
+ }
158
+ }
159
+ console.log(`[agent-kit] summarizeSessionTitle done: "${t}"`);
160
+ return { title: t, retryable: false };
161
+ }
162
+ catch (e) {
163
+ const msg = e instanceof Error && e.message ? e.message : String(e);
164
+ // transport 竞态(CLI 子进程未就绪/已退出)与单轮超限:主对话占着 CLI 时偶发,
165
+ // 标题只是占位增强,可重试一次吸收竞态;其他错误不重试。
166
+ const retryable = msg.includes("Transport not started") ||
167
+ msg.includes("Max turns") ||
168
+ msg.includes("Connection") ||
169
+ msg.includes("ETIMEDOUT") ||
170
+ msg.includes("ECONNRESET");
171
+ console.error(`[agent-kit] auto-title 生成失败(不影响对话,标题暂用快速占位)${retryable ? ",将重试一次" : ""}: ${msg}`);
172
+ return { title: "", retryable };
173
+ }
174
+ }
175
+ const first = await runOnce();
176
+ if (!first.title && first.retryable) {
177
+ await new Promise((r) => setTimeout(r, 800));
178
+ const second = await runOnce();
179
+ title = second.title;
180
+ }
181
+ else {
182
+ title = first.title;
183
+ }
184
+ return title.trim().replace(/^["「『]|["」』。.!!??\s]+$/g, "").slice(0, 30);
185
+ }
186
+ /** 本地规则占位标题:取用户首条消息开头 6-12 字(去掉常见开场词/附件前缀/尾部标点)。
187
+ * 仅作「消息一发出标题就刷新」的立即占位,不依赖 LLM 渠道(hy3 免费渠道并发可能 429),
188
+ * 后台 LLM 标题到达后替换;空输入返回 ""(不落库,保持默认"新对话")。 */
189
+ function deriveQuickTitle(text) {
190
+ const s = (text || "").trim();
191
+ if (!s)
192
+ return "";
193
+ const cleaned = s
194
+ .replace(/^\[附件\]\s*/, "")
195
+ .replace(/^(帮我|请|麻烦|你好|嗨|hi|哈喽|hello)[,。!!??\s、,]*/i, "");
196
+ const base = (cleaned || s).replace(/[。!!?\s,,、;;]+$/g, "").trim();
197
+ if (!base)
198
+ return "";
199
+ return base.length > 12 ? base.slice(0, 12) + "…" : base;
200
+ }
201
+ export function createSessionHandlers() {
202
+ /** 401:未登录 / 游客 */
203
+ function unauth() {
204
+ return json({ error: "unauthorized" }, 401);
205
+ }
206
+ /** 503:数据库未配置,前端应降级为本地会话 */
207
+ function noDb() {
208
+ return json({ error: "database_unavailable" }, 503);
209
+ }
210
+ async function currentUserId(request) {
211
+ const user = verifySession(request, loadAuthConfig());
212
+ if (!user || !user.sub)
213
+ return null;
214
+ return user.sub;
215
+ }
216
+ /** GET /api/sessions:当前用户的会话列表 */
217
+ async function list(request) {
218
+ const userId = await currentUserId(request);
219
+ if (!userId)
220
+ return unauth();
221
+ if (!isDbEnabled())
222
+ return noDb();
223
+ const rows = await listAgentSessions(userId);
224
+ return json({ sessions: rows });
225
+ }
226
+ /** POST /api/sessions:注册新会话(body { sessionId, title?, workspaceDir? });已存在则复用 */
227
+ async function create(request) {
228
+ const userId = await currentUserId(request);
229
+ if (!userId)
230
+ return unauth();
231
+ if (!isDbEnabled())
232
+ return noDb();
233
+ const body = (await request.json().catch(() => ({})));
234
+ const sessionId = typeof body.sessionId === "string" && body.sessionId ? body.sessionId : null;
235
+ if (!sessionId)
236
+ return json({ error: "missing_session_id" }, 400);
237
+ const title = typeof body.title === "string" ? body.title : "新任务";
238
+ const workspaceDir = typeof body.workspaceDir === "string" && body.workspaceDir.trim()
239
+ ? body.workspaceDir.trim()
240
+ : null;
241
+ const row = await createAgentSession(userId, sessionId, title, workspaceDir);
242
+ return json({ session: row }, row ? 200 : 500);
243
+ }
244
+ /** PATCH /api/sessions/:id:更新会话(body { title?, workspaceDir? },至少一项;title 重命名,workspaceDir 切工作目录) */
245
+ async function rename(request, ctx) {
246
+ const userId = await currentUserId(request);
247
+ if (!userId)
248
+ return unauth();
249
+ if (!isDbEnabled())
250
+ return noDb();
251
+ const { id } = await resolveParams(ctx);
252
+ if (!id)
253
+ return json({ error: "missing_id" }, 400);
254
+ const body = (await request.json().catch(() => ({})));
255
+ const title = typeof body.title === "string" ? body.title.trim() : "";
256
+ const hasWorkspace = Object.prototype.hasOwnProperty.call(body, "workspaceDir");
257
+ const workspaceDir = typeof body.workspaceDir === "string" && body.workspaceDir.trim()
258
+ ? body.workspaceDir.trim()
259
+ : null;
260
+ if (!title && !hasWorkspace)
261
+ return json({ error: "missing_title_or_workspace" }, 400);
262
+ let ok = true;
263
+ if (title)
264
+ ok = (await renameAgentSession(userId, id, title, true)) && ok;
265
+ if (hasWorkspace)
266
+ ok = (await updateAgentSessionWorkspace(userId, id, workspaceDir)) && ok;
267
+ return json({ ok }, ok ? 200 : 404);
268
+ }
269
+ /** 按附件 url 清理磁盘文件:uploads 持久源 + attachments 派生副本(路径安全:只取 basename)。 */
270
+ async function removeAttachmentFiles(urls) {
271
+ const prefix = "/uploads";
272
+ const realDir = path.join(process.cwd(), "public", "uploads");
273
+ const virtualDir = path.join(process.cwd(), "attachments");
274
+ for (const url of urls) {
275
+ if (typeof url !== "string" || !url.startsWith(prefix))
276
+ continue;
277
+ const rel = url.slice(prefix.length).replace(/\\/g, "/").replace(/^\/+/, "");
278
+ const name = path.basename(rel).trim();
279
+ if (!name || name === "." || name === ".." || name.includes("\0"))
280
+ continue;
281
+ for (const dir of [realDir, virtualDir]) {
282
+ try {
283
+ await unlink(path.join(dir, name));
284
+ }
285
+ catch {
286
+ // 文件不存在 / 无权限:忽略(attachments 是派生的缓存副本,本来就可能没有)
287
+ }
288
+ }
289
+ }
290
+ }
291
+ /** DELETE /api/sessions/:id:删除会话(消息级联删除),并清理该会话上传的磁盘文件 */
292
+ async function remove(request, ctx) {
293
+ const userId = await currentUserId(request);
294
+ if (!userId)
295
+ return unauth();
296
+ if (!isDbEnabled())
297
+ return noDb();
298
+ const { id } = await resolveParams(ctx);
299
+ if (!id)
300
+ return json({ error: "missing_id" }, 400);
301
+ // 删除会话前先收集该会话全部上传附件 url(消息随后级联删除,无法再查)
302
+ let attachmentUrls = [];
303
+ try {
304
+ attachmentUrls = await listSessionAttachmentUrls(userId, id);
305
+ }
306
+ catch (e) {
307
+ console.warn(`[agent-kit] 收集会话附件失败(不影响删除)session=${id}:`, e instanceof Error ? e.message : e);
308
+ }
309
+ const ok = await deleteAgentSession(userId, id);
310
+ if (!ok)
311
+ return json({ ok }, 404);
312
+ // 磁盘清理:uploads(持久源)+ attachments(模型读取副本)
313
+ await removeAttachmentFiles(attachmentUrls);
314
+ return json({ ok }, 200);
315
+ }
316
+ /** GET /api/sessions/:id/messages:会话内消息(时间正序) */
317
+ async function messages(request, ctx) {
318
+ const userId = await currentUserId(request);
319
+ if (!userId)
320
+ return unauth();
321
+ if (!isDbEnabled())
322
+ return noDb();
323
+ const { id } = await resolveParams(ctx);
324
+ if (!id)
325
+ return json({ error: "missing_id" }, 400);
326
+ const rows = await listAgentMessages(userId, id);
327
+ return json({ messages: rows });
328
+ }
329
+ /** POST /api/sessions/:id/messages:追加消息(body { role, content, images?, modelInfo? }) */
330
+ async function appendMessage(request, ctx) {
331
+ const userId = await currentUserId(request);
332
+ if (!userId)
333
+ return unauth();
334
+ if (!isDbEnabled())
335
+ return noDb();
336
+ const { id } = await resolveParams(ctx);
337
+ if (!id)
338
+ return json({ error: "missing_id" }, 400);
339
+ // 会话必须归属当前用户(校验存在性)
340
+ const session = await getAgentSession(userId, id);
341
+ if (!session)
342
+ return json({ error: "not_found" }, 404);
343
+ const body = (await request.json().catch(() => ({})));
344
+ const role = body.role === "assistant" ? "assistant" : body.role === "user" ? "user" : "";
345
+ const content = typeof body.content === "string" ? body.content : "";
346
+ if (!role || (!content && !(role === "assistant" && Array.isArray(body.steps) && body.steps.length)))
347
+ return json({ error: "missing_role_or_content" }, 400);
348
+ const thinkingSecs = typeof body.thinkingSecs === "number" ? body.thinkingSecs : body.thinkingSecs === null ? null : undefined;
349
+ const elapsedSecs = typeof body.elapsedSecs === "number" ? body.elapsedSecs : body.elapsedSecs === null ? null : undefined;
350
+ try {
351
+ await addAgentMessage(id, role, content, body.images ?? null, body.steps ?? null, thinkingSecs, body.modelInfo ?? null, body.artifacts ?? null, body.attachment ?? null, elapsedSecs, body.costInfo ?? null);
352
+ }
353
+ catch (e) {
354
+ // 落库失败必须可见:前端会因 HTTP 非 2xx 在控制台告警,避免"历史凭空消失"无法排查
355
+ console.error(`[agent-kit] appendMessage 落库失败 session=${id} role=${role}:`, e instanceof Error ? e.message : e);
356
+ return json({ error: "message_save_failed" }, 500);
357
+ }
358
+ return json({ ok: true }, 200);
359
+ }
360
+ /** POST /api/sessions/:id/auto-title:首条消息生成会话标题(双轨)。
361
+ * ① 立即:本地规则占位标题落库并返回——任何网络/渠道状况下标题都即刻刷新,不停在"新对话";
362
+ * ② LLM 标题:等待生成后随本响应直接返回更精确标题(标题摘要为单轮短 prompt,通常数秒),
363
+ * 前端拿到即刷新——满足"拿到 title 就立即刷新",即使主对话还在思考也能即时显示;
364
+ * LLM 渠道失败/返回空则回退占位标题,答复流中 SDK 的 ai-title(topic)继续兜底。
365
+ * 说明:标题请求与主对话是两个独立 SDK query(各自新建 transport),并发互不阻塞。 */
366
+ async function autoTitle(request, ctx) {
367
+ const userId = await currentUserId(request);
368
+ if (!userId) {
369
+ console.log("[auto-title] unauth");
370
+ return unauth();
371
+ }
372
+ if (!isDbEnabled()) {
373
+ console.log("[auto-title] db disabled");
374
+ return noDb();
375
+ }
376
+ const { id } = await resolveParams(ctx);
377
+ if (!id)
378
+ return json({ error: "missing_id" }, 400);
379
+ const session = await getAgentSession(userId, id);
380
+ if (!session) {
381
+ console.log(`[auto-title] session not found: ${id}`);
382
+ return json({ error: "not_found" }, 404);
383
+ }
384
+ // 只对首条消息生成标题:会话已有真实标题(非默认"新对话"/"新任务")时直接跳过,不再消耗 LLM
385
+ const hasRealTitle = typeof session.title === "string" &&
386
+ session.title.trim() !== "" &&
387
+ session.title !== "新对话" &&
388
+ session.title !== "新任务";
389
+ if (hasRealTitle) {
390
+ console.log(`[auto-title] skip (already titled): ${id}`);
391
+ return json({ skipped: true, title: session.title });
392
+ }
393
+ const body = (await request.json().catch(() => ({})));
394
+ const userMessage = typeof body.userMessage === "string" ? body.userMessage.trim() : "";
395
+ if (!userMessage)
396
+ return json({ error: "missing_user_message" }, 400);
397
+ // ① 立即落库本地规则占位标题:消息刚发出标题就刷新(不再显示"新对话")
398
+ // 兜底:即使派生为空(纯附件/特殊字符)也截取原文前 12 字,保证返回 title 非空、前端可刷新
399
+ const quick = deriveQuickTitle(userMessage) || userMessage.replace(/\s+/g, " ").trim().slice(0, 12);
400
+ if (quick) {
401
+ try {
402
+ const saved = await setSessionTitleIfNotUserRenamed(userId, id, quick);
403
+ if (saved)
404
+ console.log(`[auto-title] quick title="${quick}"`);
405
+ else
406
+ console.warn(`[auto-title] quick title 未落库(行不存在或 user_renamed=true)title="${quick}"`);
407
+ }
408
+ catch (e) {
409
+ // DB 异常不得 500:前端仍拿 title 刷新界面;DB 由后续重试 / SDK ai-title 补齐
410
+ console.error(`[auto-title] quick title 落库异常(不阻塞响应):`, e instanceof Error ? e.message : e);
411
+ }
412
+ }
413
+ // ② LLM 标题:等待生成(单轮短 prompt,通常数秒)后随响应返回,前端拿到即刷新;
414
+ // 失败/返回空则回退占位标题,并由答复流中 SDK 的 ai-title 兜底。
415
+ // 这样即便 SDK 不为该模型回传 title 事件,首条消息的精确标题也能在模型还在思考时即时显示。
416
+ let improved = "";
417
+ try {
418
+ improved = String(await summarizeSessionTitle(userMessage)).trim();
419
+ }
420
+ catch (e) {
421
+ console.error("[auto-title] LLM 标题生成失败(回退占位):", e instanceof Error ? e.message : e);
422
+ }
423
+ if (improved) {
424
+ try {
425
+ await setSessionTitleIfNotUserRenamed(userId, id, improved);
426
+ console.log(`[auto-title] llm title="${improved}"`);
427
+ }
428
+ catch (e) {
429
+ console.error("[auto-title] LLM 标题落库异常:", e instanceof Error ? e.message : e);
430
+ }
431
+ }
432
+ return json({ title: improved || quick, improving: true });
433
+ }
434
+ /** POST /api/sessions/:id/truncate:删除 upToId 之后的所有消息(编辑重发时调用)。 */
435
+ async function truncate(request, ctx) {
436
+ const userId = await currentUserId(request);
437
+ if (!userId)
438
+ return unauth();
439
+ if (!isDbEnabled())
440
+ return noDb();
441
+ const { id } = await resolveParams(ctx);
442
+ if (!id)
443
+ return json({ error: "missing_id" }, 400);
444
+ const session = await getAgentSession(userId, id);
445
+ if (!session)
446
+ return json({ error: "not_found" }, 404);
447
+ const body = (await request.json().catch(() => ({})));
448
+ const upToId = typeof body.upToId === "number" ? body.upToId : -1;
449
+ if (upToId < 0)
450
+ return json({ error: "missing_upToId" }, 400);
451
+ slog(id, "http.truncate", { userId, upToId }, "info");
452
+ await truncateAgentMessages(userId, id, upToId);
453
+ return json({ ok: true }, 200);
454
+ }
455
+ async function fork(request, ctx) {
456
+ const userId = await currentUserId(request);
457
+ if (!userId)
458
+ return unauth();
459
+ if (!isDbEnabled())
460
+ return noDb();
461
+ const { id } = await resolveParams(ctx);
462
+ if (!id)
463
+ return json({ error: "missing_id" }, 400);
464
+ if (!await getAgentSession(userId, id))
465
+ return json({ error: "会话不存在或无权访问" }, 404);
466
+ const body = await request.json().catch(() => ({}));
467
+ if (!Number.isSafeInteger(body.messageIndex) || body.messageIndex < 0 || (body.messageId !== undefined && (!Number.isSafeInteger(body.messageId) || body.messageId <= 0)))
468
+ return json({ error: "分支位置无效" }, 400);
469
+ try {
470
+ return json({ session: await forkAgentSession(userId, id, body.messageIndex, body.messageId, body.snapshot) }, 201);
471
+ }
472
+ catch (error) {
473
+ console.error("[agent-kit] fork failed", error);
474
+ return json({ error: error instanceof Error ? error.message : "创建分支失败" }, 409);
475
+ }
476
+ }
477
+ /** POST /api/sessions/:id/close-sdk:关闭指定 sdkSid 的常驻 CLI Session(编辑重发时清旧 transcript)。 */
478
+ async function closeSdk(request, ctx) {
479
+ const body = (await request.json().catch(() => ({})));
480
+ const sdkSid = typeof body.sdkSid === "string" ? body.sdkSid : "";
481
+ if (sdkSid) {
482
+ const { closeSession } = await import("./session-store.js");
483
+ closeSession(sdkSid);
484
+ slog(sdkSid, "http.closeSdk", {}, "info");
485
+ }
486
+ return json({ ok: true }, 200);
487
+ }
488
+ /** GET /api/admin/usage:token 用量聚合(需 admin 登录 Cookie,未登录 401)。
489
+ * summary 总量 + 按会话分组明细;数据源为消息表 cost_info。
490
+ * 没有 cost_info 数据时返回 200 空聚合(前端展示「暂无用量数据」),不报错。 */
491
+ async function adminUsage(request) {
492
+ if (!verifyAdmin(request))
493
+ return json({ error: "未登录或登录已过期,请先登录" }, 401);
494
+ if (!isDbEnabled())
495
+ return noDb();
496
+ try {
497
+ const data = await queryAdminUsage(200);
498
+ if (!data)
499
+ return noDb();
500
+ return json(data);
501
+ }
502
+ catch (e) {
503
+ // 数据库未迁移(如缺 cost_info 列)等异常:返回带可读 message 的 500,
504
+ // 前端优先展示服务端错误信息,便于定位;空数据不走到这里。
505
+ const msg = e instanceof Error ? e.message : String(e);
506
+ console.error("[agent-kit] admin usage 查询失败:", e);
507
+ return json({ error: `用量数据查询失败:${msg}` }, 500);
508
+ }
509
+ }
510
+ /** GET /api/models/context?model=&sid=:当前模型上下文窗口上限(token)。
511
+ * 容量百分比 = 前端最新一轮 cost_info.usage.input_tokens / maxTokens;
512
+ * 会话创建后服务端已后台预取(getAvailableModelsRaw,无需先 send),
513
+ * 拿不到时返回 null(前端按「无数据不显示」策略隐藏圆环)。 */
514
+ async function modelContext(request) {
515
+ const userId = await currentUserId(request);
516
+ if (!userId)
517
+ return unauth();
518
+ const url = new URL(request.url);
519
+ const model = url.searchParams.get("model") || undefined;
520
+ const sid = url.searchParams.get("sid") || undefined;
521
+ const maxTokens = await getModelContextLimit(model || undefined, sid || undefined);
522
+ slog(sid ?? "", "modelContext.route", { model: model ?? null, maxTokens }, "info");
523
+ return json({ model: model ?? null, maxTokens });
524
+ }
525
+ return {
526
+ list,
527
+ create,
528
+ rename,
529
+ remove,
530
+ messages,
531
+ appendMessage,
532
+ autoTitle,
533
+ truncate,
534
+ fork,
535
+ closeSdk,
536
+ adminUsage,
537
+ adminLogin,
538
+ adminLogout,
539
+ modelContext,
540
+ };
541
+ }
@@ -0,0 +1,15 @@
1
+ export interface ShareHandlers {
2
+ create: {
3
+ POST: (request: Request) => Promise<Response>;
4
+ };
5
+ jsapiSign: {
6
+ POST: (request: Request) => Promise<Response>;
7
+ };
8
+ view: {
9
+ GET: (request: Request, ctx: unknown) => Promise<Response>;
10
+ };
11
+ send: {
12
+ POST: (request: Request, ctx: unknown) => Promise<Response>;
13
+ };
14
+ }
15
+ export declare function createShareHandlers(): ShareHandlers;