@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.
- package/README.md +546 -2
- package/dist/agent.d.ts +27 -0
- package/dist/agent.js +31 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +105 -0
- package/dist/handlers.d.ts +92 -0
- package/dist/handlers.js +211 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/sdk.d.ts +106 -0
- package/dist/sdk.js +309 -0
- package/dist/server/agent.d.ts +27 -0
- package/dist/server/agent.js +31 -0
- package/dist/server/artifact-hooks.d.ts +6 -0
- package/dist/server/artifact-hooks.js +37 -0
- package/dist/server/artifact-tool-tracker.d.ts +24 -0
- package/dist/server/artifact-tool-tracker.js +347 -0
- package/dist/server/artifacts.d.ts +227 -0
- package/dist/server/artifacts.js +1061 -0
- package/dist/server/auth/config.d.ts +34 -0
- package/dist/server/auth/config.js +82 -0
- package/dist/server/auth/db.d.ts +159 -0
- package/dist/server/auth/db.js +680 -0
- package/dist/server/auth/handlers.d.ts +27 -0
- package/dist/server/auth/handlers.js +297 -0
- package/dist/server/auth/index.d.ts +15 -0
- package/dist/server/auth/index.js +14 -0
- package/dist/server/auth/login-options.d.ts +14 -0
- package/dist/server/auth/login-options.js +50 -0
- package/dist/server/auth/providers/dingtalk.d.ts +16 -0
- package/dist/server/auth/providers/dingtalk.js +177 -0
- package/dist/server/auth/providers/feishu.d.ts +15 -0
- package/dist/server/auth/providers/feishu.js +100 -0
- package/dist/server/auth/providers/index.d.ts +13 -0
- package/dist/server/auth/providers/index.js +24 -0
- package/dist/server/auth/session.d.ts +48 -0
- package/dist/server/auth/session.js +108 -0
- package/dist/server/auth/types.d.ts +118 -0
- package/dist/server/auth/types.js +7 -0
- package/dist/server/browser-agent-mcp.d.ts +67 -0
- package/dist/server/browser-agent-mcp.js +608 -0
- package/dist/server/cli.d.ts +6 -0
- package/dist/server/cli.js +112 -0
- package/dist/server/debug-log.d.ts +25 -0
- package/dist/server/debug-log.js +86 -0
- package/dist/server/handlers.d.ts +141 -0
- package/dist/server/handlers.js +523 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.js +28 -0
- package/dist/server/kit-api.d.ts +23 -0
- package/dist/server/kit-api.js +153 -0
- package/dist/server/model-sources.d.ts +32 -0
- package/dist/server/model-sources.js +58 -0
- package/dist/server/oa.d.ts +87 -0
- package/dist/server/oa.js +397 -0
- package/dist/server/providers/openrouter.d.ts +51 -0
- package/dist/server/providers/openrouter.js +283 -0
- package/dist/server/render-markdown.d.ts +2 -0
- package/dist/server/render-markdown.js +50 -0
- package/dist/server/sdk.d.ts +259 -0
- package/dist/server/sdk.js +1991 -0
- package/dist/server/session-access.d.ts +5 -0
- package/dist/server/session-access.js +68 -0
- package/dist/server/session-fork.d.ts +10 -0
- package/dist/server/session-fork.js +142 -0
- package/dist/server/session-store.d.ts +56 -0
- package/dist/server/session-store.js +148 -0
- package/dist/server/sessions.d.ts +24 -0
- package/dist/server/sessions.js +541 -0
- package/dist/server/share.d.ts +15 -0
- package/dist/server/share.js +433 -0
- package/dist/server/tool-activity.d.ts +2 -0
- package/dist/server/tool-activity.js +26 -0
- package/dist/server/transcription/xiaomi-mimo.d.ts +8 -0
- package/dist/server/transcription/xiaomi-mimo.js +86 -0
- package/dist/server/upload.d.ts +13 -0
- package/dist/server/upload.js +81 -0
- package/dist/server/user-files.d.ts +28 -0
- package/dist/server/user-files.js +311 -0
- package/dist/server/voice.d.ts +25 -0
- package/dist/server/voice.js +61 -0
- package/dist/server/web-fetch.d.ts +71 -0
- package/dist/server/web-fetch.js +330 -0
- package/dist/styles.css +3354 -0
- package/dist/transcription/xiaomi-mimo.d.ts +8 -0
- package/dist/transcription/xiaomi-mimo.js +86 -0
- package/dist/ui/index.d.ts +352 -0
- package/dist/ui/index.js +7730 -0
- package/dist/upload.d.ts +13 -0
- package/dist/upload.js +81 -0
- package/dist/voice.d.ts +25 -0
- package/dist/voice.js +61 -0
- package/package.json +74 -4
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type SessionAccessKind = "user" | null;
|
|
2
|
+
/** 判断某个会话是否属于当前请求者(登录用户 → DB 归属校验)。 */
|
|
3
|
+
export declare function assertSessionAccess(request: Request, sessionId: string): Promise<SessionAccessKind>;
|
|
4
|
+
/** chat / answer / steer 的访问判定:登录用户 → DB 归属;未登录 → 拒绝。 */
|
|
5
|
+
export declare function assertChatAccess(request: Request, sessionId: string, resume: boolean): Promise<SessionAccessKind>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-kit · 会话访问控制(隔离核心)
|
|
3
|
+
*
|
|
4
|
+
* 目标:不同用户之间不能访问彼此的会话数据(消息 / 产物 / 常驻 CLI 会话)。
|
|
5
|
+
* 唯一路径:以会话 Cookie(agent_session)解析 user.sub(主体键 `provider:unionId`),再查 PostgreSQL
|
|
6
|
+
* agent_sessions.user_id —— 只能访问**自己**的会话。任何请求带别人的 sessionId
|
|
7
|
+
* → 拒绝。未登录一律无权限(不再支持游客模式)。
|
|
8
|
+
*
|
|
9
|
+
* 设计取舍:
|
|
10
|
+
* - 产物(iframe/img/a 无法带 header)同样走 Cookie:同源请求自动携带,
|
|
11
|
+
* 无需 URL 签名票据;
|
|
12
|
+
* - 数据库故障时放行登录用户(体验优先,风险=失去归属校验,接受并记日志)。
|
|
13
|
+
*/
|
|
14
|
+
import { verifySession } from "./auth/handlers.js";
|
|
15
|
+
import { loadAuthConfig } from "./auth/config.js";
|
|
16
|
+
import { getAgentSession, isDbEnabled } from "./auth/db.js";
|
|
17
|
+
/** 判断某个会话是否属于当前请求者(登录用户 → DB 归属校验)。 */
|
|
18
|
+
export async function assertSessionAccess(request, sessionId) {
|
|
19
|
+
const user = verifySession(request, loadAuthConfig());
|
|
20
|
+
if (!user?.sub)
|
|
21
|
+
return null;
|
|
22
|
+
if (isDbEnabled()) {
|
|
23
|
+
try {
|
|
24
|
+
const row = await getAgentSession(user.sub, sessionId);
|
|
25
|
+
return row ? "user" : null;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
// DB 查询异常:放行登录用户(体验优先,风险=数据库故障时失去归属校验,接受并记日志)
|
|
29
|
+
console.warn("[agent-kit access] DB 归属校验异常,放行登录用户:", e instanceof Error ? e.message : e);
|
|
30
|
+
return "user";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return "user"; // 数据库未启用:会话无持久化,无跨用户数据可泄露
|
|
34
|
+
}
|
|
35
|
+
/** chat / answer / steer 的访问判定:登录用户 → DB 归属;未登录 → 拒绝。 */
|
|
36
|
+
export async function assertChatAccess(request, sessionId, resume) {
|
|
37
|
+
const user = verifySession(request, loadAuthConfig());
|
|
38
|
+
if (!user?.sub)
|
|
39
|
+
return null;
|
|
40
|
+
if (isDbEnabled()) {
|
|
41
|
+
try {
|
|
42
|
+
const row = await getAgentSession(user.sub, sessionId);
|
|
43
|
+
if (row)
|
|
44
|
+
return "user";
|
|
45
|
+
// 编辑重发时前端会换一个「临时 SDK transcript id」避免 CLI 加载旧 transcript。
|
|
46
|
+
// 该 id 形如 `${本会话 id}_${随机后缀}`(会话 id 为 UUID,不含 '_')。
|
|
47
|
+
// 取 '_' 前的会话 id 校验归属——id 由受信会话 id 派生,无法借它冒用他人会话:
|
|
48
|
+
// · 直接拿他人会话 id 当 sessionId → 上面查不到,无 '_' 可选,落到此处仍拒绝;
|
|
49
|
+
// · 伪造 `${他人会话 id}_x` → 基准 id 非本人所有,拒绝。
|
|
50
|
+
const sep = sessionId.indexOf("_");
|
|
51
|
+
if (sep > 0) {
|
|
52
|
+
const base = sessionId.slice(0, sep);
|
|
53
|
+
const owner = await getAgentSession(user.sub, base);
|
|
54
|
+
if (owner)
|
|
55
|
+
return "user";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
console.warn("[agent-kit access] DB 归属校验异常,放行登录用户:", e instanceof Error ? e.message : e);
|
|
60
|
+
return "user";
|
|
61
|
+
}
|
|
62
|
+
// DB 可用但库里没有该会话:仅首轮(resume=false)放行——视为当前用户的新会话
|
|
63
|
+
//(前端发送前会先 POST /api/sessions 注册;此分支覆盖注册竞态);
|
|
64
|
+
// 续接一个"不属于自己"的会话 → 拒绝。
|
|
65
|
+
return resume ? null : "user";
|
|
66
|
+
}
|
|
67
|
+
return "user"; // 数据库未启用
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentMessageRow } from "./auth/db.js";
|
|
2
|
+
export declare function nextBranchTitle(title: string, existing: Iterable<string>): string;
|
|
3
|
+
export declare function selectBranchMessages(messages: AgentMessageRow[], index: number, messageId?: number): AgentMessageRow[];
|
|
4
|
+
/** Preserve still-visible browser steps/long text while keeping file references server-owned. */
|
|
5
|
+
export declare function mergeBranchSnapshot(messages: AgentMessageRow[], snapshot?: unknown): AgentMessageRow[];
|
|
6
|
+
/** Independent files prevent deleting/editing the source from breaking the branch. */
|
|
7
|
+
export declare function copyBranchFiles(sourceId: string, targetId: string, messages: AgentMessageRow[], workspaceDir?: string): Promise<{
|
|
8
|
+
messages: AgentMessageRow[];
|
|
9
|
+
cleanup: () => Promise<void>;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { artifactType, ensureSessionDir, readArtifact, saveRegistry, sanitizeArtifactName } from "./artifacts.js";
|
|
5
|
+
export function nextBranchTitle(title, existing) {
|
|
6
|
+
const used = new Set(existing);
|
|
7
|
+
const base = title.replace(/(\d+)$/, "");
|
|
8
|
+
for (let n = 1;; n++) {
|
|
9
|
+
const suffix = `(${n})`;
|
|
10
|
+
const candidate = base.slice(0, 250 - suffix.length) + suffix;
|
|
11
|
+
if (!used.has(candidate))
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function selectBranchMessages(messages, index, messageId) {
|
|
16
|
+
const end = messageId === undefined ? index : messages.findIndex((message) => message.id === messageId);
|
|
17
|
+
if (!Number.isSafeInteger(end) || end < 0 || end >= messages.length || messages[end].role !== "assistant") {
|
|
18
|
+
throw new Error("所选答复尚未保存或已发生变化,请刷新后重试");
|
|
19
|
+
}
|
|
20
|
+
return structuredClone(messages.slice(0, end + 1));
|
|
21
|
+
}
|
|
22
|
+
/** Preserve still-visible browser steps/long text while keeping file references server-owned. */
|
|
23
|
+
export function mergeBranchSnapshot(messages, snapshot) {
|
|
24
|
+
if (snapshot === undefined)
|
|
25
|
+
return messages;
|
|
26
|
+
if (!Array.isArray(snapshot) || snapshot.length !== messages.length)
|
|
27
|
+
throw new Error("会话内容已变化,请刷新后重试");
|
|
28
|
+
return messages.map((message, index) => {
|
|
29
|
+
const live = snapshot[index];
|
|
30
|
+
if (!live || live.role !== message.role || typeof live.content !== "string" || (live.id !== undefined && live.id !== message.id)
|
|
31
|
+
|| (live.content !== message.content && live.content.slice(0, 20000) !== message.content))
|
|
32
|
+
throw new Error("会话内容已变化,请刷新后重试");
|
|
33
|
+
return { ...message, content: live.content, images: Array.isArray(live.images) ? live.images : message.images,
|
|
34
|
+
steps: Array.isArray(live.steps) ? live.steps : message.steps };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Independent files prevent deleting/editing the source from breaking the branch. */
|
|
38
|
+
export async function copyBranchFiles(sourceId, targetId, messages, workspaceDir) {
|
|
39
|
+
const copies = [];
|
|
40
|
+
const replacements = new Map();
|
|
41
|
+
const sourceUploads = path.resolve(process.cwd(), "public", "uploads");
|
|
42
|
+
const result = structuredClone(messages);
|
|
43
|
+
let folder;
|
|
44
|
+
const cleanup = async () => {
|
|
45
|
+
await Promise.all(copies.map((file) => fs.unlink(file).catch(() => { })));
|
|
46
|
+
// Resolve and check the generated UUID directory before removing anything recursively.
|
|
47
|
+
if (folder && path.basename(path.resolve(folder)) === targetId)
|
|
48
|
+
await fs.rm(path.resolve(folder), { recursive: true, force: true });
|
|
49
|
+
if (folder)
|
|
50
|
+
await saveRegistry(targetId, [], workspaceDir);
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
// Only server-stored attachment URLs are allowed to select upload files.
|
|
54
|
+
for (const message of result) {
|
|
55
|
+
const attachment = message.attachment;
|
|
56
|
+
if (!attachment?.url?.startsWith("/uploads/"))
|
|
57
|
+
continue;
|
|
58
|
+
if (replacements.has(attachment.url))
|
|
59
|
+
continue;
|
|
60
|
+
const relative = decodeURIComponent(attachment.url.slice("/uploads/".length));
|
|
61
|
+
const source = path.resolve(sourceUploads, relative);
|
|
62
|
+
if (!source.startsWith(sourceUploads + path.sep))
|
|
63
|
+
throw new Error("附件路径无效");
|
|
64
|
+
const name = `${targetId}-${randomUUID()}${path.extname(source)}`;
|
|
65
|
+
const target = path.join(sourceUploads, name);
|
|
66
|
+
try {
|
|
67
|
+
await fs.copyFile(source, target, fs.constants.COPYFILE_EXCL);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error("附件文件不可用,未创建分支");
|
|
71
|
+
}
|
|
72
|
+
copies.push(target);
|
|
73
|
+
replacements.set(attachment.url, `/uploads/${name}`);
|
|
74
|
+
replacements.set(`/attachments/${relative}`, `/attachments/${name}`);
|
|
75
|
+
}
|
|
76
|
+
folder = await ensureSessionDir(targetId, workspaceDir);
|
|
77
|
+
const staging = path.join(workspaceDir ? path.resolve(workspaceDir) : folder, "attachments");
|
|
78
|
+
for (const url of replacements.values()) {
|
|
79
|
+
if (!url.startsWith("/uploads/"))
|
|
80
|
+
continue;
|
|
81
|
+
const name = path.basename(url);
|
|
82
|
+
await fs.mkdir(staging, { recursive: true });
|
|
83
|
+
const target = path.join(staging, name);
|
|
84
|
+
await fs.copyFile(path.join(sourceUploads, name), target, fs.constants.COPYFILE_EXCL);
|
|
85
|
+
copies.push(target);
|
|
86
|
+
}
|
|
87
|
+
const files = new Map();
|
|
88
|
+
const created = new Set();
|
|
89
|
+
for (const message of result)
|
|
90
|
+
for (const item of Array.isArray(message.artifacts) ? message.artifacts : []) {
|
|
91
|
+
if (!item || typeof item.id !== "string" || item.type === "browser")
|
|
92
|
+
continue;
|
|
93
|
+
if (item.change === "added" || !item.change)
|
|
94
|
+
created.add(item.id);
|
|
95
|
+
files.set(item.id, item);
|
|
96
|
+
}
|
|
97
|
+
const entries = [];
|
|
98
|
+
for (const [id, file] of files) {
|
|
99
|
+
if (file.change === "deleted")
|
|
100
|
+
continue;
|
|
101
|
+
const name = sanitizeArtifactName(id);
|
|
102
|
+
const target = path.resolve(folder, name);
|
|
103
|
+
if (!target.startsWith(path.resolve(folder) + path.sep))
|
|
104
|
+
throw new Error("产出物路径无效");
|
|
105
|
+
const content = typeof file.newContent === "string" ? Buffer.from(file.newContent) : (await readArtifact(sourceId, id, workspaceDir))?.buf;
|
|
106
|
+
if (!content)
|
|
107
|
+
throw new Error(`产出物 ${String(file.name ?? id)} 不可用,未创建分支`);
|
|
108
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
109
|
+
await fs.writeFile(target, content, { flag: "wx" });
|
|
110
|
+
entries.push({ id: name, name: String(file.name ?? name), absPath: target, type: artifactType(name), origin: "model", createdBySession: created.has(id), lastSeen: true, size: content.length });
|
|
111
|
+
}
|
|
112
|
+
await saveRegistry(targetId, entries, workspaceDir);
|
|
113
|
+
// Rewrite embedded upload links and artifact session URLs throughout message metadata.
|
|
114
|
+
replacements.set(`/agent-output/${sourceId}/`, `/agent-output/${targetId}/`);
|
|
115
|
+
replacements.set(`session=${encodeURIComponent(sourceId)}`, `session=${encodeURIComponent(targetId)}`);
|
|
116
|
+
const rewrite = (value) => {
|
|
117
|
+
if (typeof value === "string") {
|
|
118
|
+
for (const [before, after] of replacements)
|
|
119
|
+
value = value.split(before).join(after);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(value))
|
|
123
|
+
return value.map(rewrite);
|
|
124
|
+
if (value && typeof value === "object" && !(value instanceof Date))
|
|
125
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rewrite(item)]));
|
|
126
|
+
return value;
|
|
127
|
+
};
|
|
128
|
+
return { messages: result.map((message) => {
|
|
129
|
+
const copied = rewrite(message);
|
|
130
|
+
if (Array.isArray(copied.artifacts))
|
|
131
|
+
copied.artifacts = copied.artifacts.map((file) => {
|
|
132
|
+
const entry = entries.find((entry) => entry.id === file.id);
|
|
133
|
+
return entry ? { ...file, dir: path.dirname(entry.absPath) } : file;
|
|
134
|
+
});
|
|
135
|
+
return copied;
|
|
136
|
+
}), cleanup };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
await cleanup();
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-kit · Session 常驻注册表(多轮 + steer 支撑)
|
|
3
|
+
*
|
|
4
|
+
* 基于 @tencent-ai/agent-sdk 的 V2 Session API(unstable_v2_createSession):
|
|
5
|
+
* 一个前端 sessionId 对应一个常驻 CLI 会话进程,跨轮复用,运行中可插入消息(steer)。
|
|
6
|
+
*
|
|
7
|
+
* 为什么不用每轮 query():
|
|
8
|
+
* query() 是单发模式(一轮一 CLI 进程),中途无法插入消息;
|
|
9
|
+
* Session API 底层 CLI(>= 2.140.0,bundled 2.158.0)支持 stream-json 的
|
|
10
|
+
* steer control_request:消息进入 SteerInputBuffer,在「下一次模型请求前」注入,
|
|
11
|
+
* 不打断当前正在执行的工具循环——这正是前端「插话/steer」所需的语义。
|
|
12
|
+
*
|
|
13
|
+
* steer 通道说明(TS SDK 尚未公开,走 transport 内部通道,官方暴露后可替换):
|
|
14
|
+
* transport.sendControlRequest({
|
|
15
|
+
* subtype: "steer",
|
|
16
|
+
* session_id: <CLI 侧会话 ID>, // 从 stream 的 system 消息捕获
|
|
17
|
+
* content_blocks: [{ type: "text", text }],
|
|
18
|
+
* expected_request_id: <可选,同轮约束>,
|
|
19
|
+
* })
|
|
20
|
+
* → { steered: true } | { steered: false, reason: "stale" | "idle" }
|
|
21
|
+
*
|
|
22
|
+
* 注意:TS 的 private 在运行时只是普通属性,因此可以访问 session._transport
|
|
23
|
+
* 与 session.getSessionIdForControl();SDK 升级可能改名,调用处均有防御。
|
|
24
|
+
*/
|
|
25
|
+
import { type Session, type SessionOptions } from "@tencent-ai/agent-sdk";
|
|
26
|
+
export type SteerResult = {
|
|
27
|
+
steered: boolean;
|
|
28
|
+
reason?: string;
|
|
29
|
+
response?: unknown;
|
|
30
|
+
};
|
|
31
|
+
export interface ManagedSession {
|
|
32
|
+
/** 前端会话 ID(registry key,也是 AskUserQuestion 挂起/产物隔离用的 sid) */
|
|
33
|
+
sid: string;
|
|
34
|
+
/** SDK Session 实例(常驻 CLI 进程;Session 接口不含 interrupt/setCanUseTool,调用处断言) */
|
|
35
|
+
session: Session;
|
|
36
|
+
/** CLI 侧会话 ID(从 stream 的 system 消息捕获;steer control_request 需要它) */
|
|
37
|
+
cliSessionId: string | null;
|
|
38
|
+
lastActiveAt: number;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function getManagedSession(sid: string): ManagedSession | undefined;
|
|
42
|
+
export declare function createManagedSession(sid: string, options: SessionOptions): ManagedSession;
|
|
43
|
+
/** 首轮 stream 的 system 消息会携带 CLI 侧会话 ID,捕获后供 steer 使用(只取第一个) */
|
|
44
|
+
export declare function setCliSessionId(sid: string, cliSessionId: string): void;
|
|
45
|
+
export declare function touchSession(sid: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* 运行中插入消息(steer):走 CLI 2.140.0+ 的 stream-json steer 控制通道。
|
|
48
|
+
* 返回 { steered: true } 表示已进入缓冲,将在下一次模型请求前注入。
|
|
49
|
+
*/
|
|
50
|
+
export declare function steerSession(sid: string, text: string): Promise<SteerResult>;
|
|
51
|
+
/** 中断当前轮的模型执行(前端"停止生成"→ request.signal abort → 本函数) */
|
|
52
|
+
export declare function interruptSession(sid: string): Promise<void>;
|
|
53
|
+
export declare function closeSession(sid: string): void;
|
|
54
|
+
export declare function getSessionCount(): number;
|
|
55
|
+
/** 惰性启动空闲回收(Node 环境;.unref 让定时器不阻塞进程退出) */
|
|
56
|
+
export declare function startSessionSweeper(): void;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-kit · Session 常驻注册表(多轮 + steer 支撑)
|
|
3
|
+
*
|
|
4
|
+
* 基于 @tencent-ai/agent-sdk 的 V2 Session API(unstable_v2_createSession):
|
|
5
|
+
* 一个前端 sessionId 对应一个常驻 CLI 会话进程,跨轮复用,运行中可插入消息(steer)。
|
|
6
|
+
*
|
|
7
|
+
* 为什么不用每轮 query():
|
|
8
|
+
* query() 是单发模式(一轮一 CLI 进程),中途无法插入消息;
|
|
9
|
+
* Session API 底层 CLI(>= 2.140.0,bundled 2.158.0)支持 stream-json 的
|
|
10
|
+
* steer control_request:消息进入 SteerInputBuffer,在「下一次模型请求前」注入,
|
|
11
|
+
* 不打断当前正在执行的工具循环——这正是前端「插话/steer」所需的语义。
|
|
12
|
+
*
|
|
13
|
+
* steer 通道说明(TS SDK 尚未公开,走 transport 内部通道,官方暴露后可替换):
|
|
14
|
+
* transport.sendControlRequest({
|
|
15
|
+
* subtype: "steer",
|
|
16
|
+
* session_id: <CLI 侧会话 ID>, // 从 stream 的 system 消息捕获
|
|
17
|
+
* content_blocks: [{ type: "text", text }],
|
|
18
|
+
* expected_request_id: <可选,同轮约束>,
|
|
19
|
+
* })
|
|
20
|
+
* → { steered: true } | { steered: false, reason: "stale" | "idle" }
|
|
21
|
+
*
|
|
22
|
+
* 注意:TS 的 private 在运行时只是普通属性,因此可以访问 session._transport
|
|
23
|
+
* 与 session.getSessionIdForControl();SDK 升级可能改名,调用处均有防御。
|
|
24
|
+
*/
|
|
25
|
+
import { unstable_v2_createSession } from "@tencent-ai/agent-sdk";
|
|
26
|
+
import { slog } from "./debug-log.js";
|
|
27
|
+
const registry = new Map();
|
|
28
|
+
/** 空闲回收:会话超过该时长无任何活动则关闭 CLI 进程,避免常驻进程堆积 */
|
|
29
|
+
const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
30
|
+
const SWEEP_INTERVAL_MS = 5 * 60 * 1000;
|
|
31
|
+
let sweeper = null;
|
|
32
|
+
export function getManagedSession(sid) {
|
|
33
|
+
const entry = registry.get(sid);
|
|
34
|
+
if (entry)
|
|
35
|
+
entry.lastActiveAt = Date.now();
|
|
36
|
+
return entry;
|
|
37
|
+
}
|
|
38
|
+
export function createManagedSession(sid, options) {
|
|
39
|
+
const session = unstable_v2_createSession({ ...options });
|
|
40
|
+
const entry = {
|
|
41
|
+
sid,
|
|
42
|
+
session: session,
|
|
43
|
+
cliSessionId: null,
|
|
44
|
+
lastActiveAt: Date.now(),
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
registry.set(sid, entry);
|
|
48
|
+
console.log(`[agent-kit] Session 已创建 sid=${sid}(registry=${registry.size})`);
|
|
49
|
+
slog(sid, "session.created", { registrySize: registry.size }, "info");
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
/** 首轮 stream 的 system 消息会携带 CLI 侧会话 ID,捕获后供 steer 使用(只取第一个) */
|
|
53
|
+
export function setCliSessionId(sid, cliSessionId) {
|
|
54
|
+
const entry = registry.get(sid);
|
|
55
|
+
if (entry && !entry.cliSessionId)
|
|
56
|
+
entry.cliSessionId = cliSessionId;
|
|
57
|
+
}
|
|
58
|
+
export function touchSession(sid) {
|
|
59
|
+
const entry = registry.get(sid);
|
|
60
|
+
if (entry)
|
|
61
|
+
entry.lastActiveAt = Date.now();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 运行中插入消息(steer):走 CLI 2.140.0+ 的 stream-json steer 控制通道。
|
|
65
|
+
* 返回 { steered: true } 表示已进入缓冲,将在下一次模型请求前注入。
|
|
66
|
+
*/
|
|
67
|
+
export async function steerSession(sid, text) {
|
|
68
|
+
const entry = registry.get(sid);
|
|
69
|
+
if (!entry) {
|
|
70
|
+
slog(sid, "steer.reject", { reason: "no_session" }, "warn");
|
|
71
|
+
return { steered: false, reason: "no_session" };
|
|
72
|
+
}
|
|
73
|
+
const session = entry.session;
|
|
74
|
+
const transport = session._transport;
|
|
75
|
+
if (!transport?.sendControlRequest) {
|
|
76
|
+
slog(sid, "steer.reject", { reason: "no_transport" }, "warn");
|
|
77
|
+
return { steered: false, reason: "no_transport" };
|
|
78
|
+
}
|
|
79
|
+
const cliSessionId = entry.cliSessionId ?? session.getSessionIdForControl?.();
|
|
80
|
+
if (!cliSessionId) {
|
|
81
|
+
slog(sid, "steer.reject", { reason: "no_session_id" }, "warn");
|
|
82
|
+
return { steered: false, reason: "no_session_id" };
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
slog(sid, "steer.request", { cliSessionId, textLen: text.length, text }, "info");
|
|
86
|
+
const resp = (await transport.sendControlRequest({
|
|
87
|
+
subtype: "steer",
|
|
88
|
+
session_id: cliSessionId,
|
|
89
|
+
content_blocks: [{ type: "text", text }],
|
|
90
|
+
}));
|
|
91
|
+
entry.lastActiveAt = Date.now();
|
|
92
|
+
const steered = resp?.steered === true;
|
|
93
|
+
console.log(`[agent-kit] steer sid=${sid} steered=${steered}${steered ? "" : ` reason=${resp?.reason ?? "unknown"}`}`);
|
|
94
|
+
slog(sid, "steer.response", { steered, reason: resp?.reason ?? null, response: resp }, "info");
|
|
95
|
+
return { steered, reason: resp?.reason, response: resp };
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.error(`[agent-kit] steer 请求异常 sid=${sid}:`, err instanceof Error ? err.message : err);
|
|
99
|
+
slog(sid, "steer.error", { error: err instanceof Error ? err.message : String(err) }, "error");
|
|
100
|
+
return { steered: false, reason: "error" };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** 中断当前轮的模型执行(前端"停止生成"→ request.signal abort → 本函数) */
|
|
104
|
+
export async function interruptSession(sid) {
|
|
105
|
+
const entry = registry.get(sid);
|
|
106
|
+
if (!entry)
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
// Session 接口未声明 interrupt(SessionImpl 才有);运行时存在,防御性调用
|
|
110
|
+
await entry.session.interrupt();
|
|
111
|
+
slog(sid, "session.interrupt", {}, "info");
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.warn(`[agent-kit] interrupt sid=${sid}:`, err instanceof Error ? err.message : err);
|
|
115
|
+
slog(sid, "session.interrupt.error", { error: err instanceof Error ? err.message : String(err) }, "warn");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function closeSession(sid) {
|
|
119
|
+
const entry = registry.get(sid);
|
|
120
|
+
if (!entry)
|
|
121
|
+
return;
|
|
122
|
+
try {
|
|
123
|
+
entry.session.close();
|
|
124
|
+
}
|
|
125
|
+
catch { /* ignore */ }
|
|
126
|
+
registry.delete(sid);
|
|
127
|
+
console.log(`[agent-kit] Session 已关闭 sid=${sid}(registry=${registry.size})`);
|
|
128
|
+
slog(sid, "session.closed", { registrySize: registry.size }, "info");
|
|
129
|
+
}
|
|
130
|
+
export function getSessionCount() {
|
|
131
|
+
return registry.size;
|
|
132
|
+
}
|
|
133
|
+
/** 惰性启动空闲回收(Node 环境;.unref 让定时器不阻塞进程退出) */
|
|
134
|
+
export function startSessionSweeper() {
|
|
135
|
+
if (sweeper)
|
|
136
|
+
return;
|
|
137
|
+
sweeper = setInterval(() => {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
for (const [sid, entry] of registry) {
|
|
140
|
+
if (now - entry.lastActiveAt > IDLE_TIMEOUT_MS) {
|
|
141
|
+
console.log(`[agent-kit] 回收空闲会话 ${sid}(${Math.round((now - entry.lastActiveAt) / 1000)}s 无活动)`);
|
|
142
|
+
closeSession(sid);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}, SWEEP_INTERVAL_MS);
|
|
146
|
+
if (typeof sweeper.unref === "function")
|
|
147
|
+
sweeper.unref();
|
|
148
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** POST /api/admin/login:校验 admin/admin,成功签发登录 Cookie。 */
|
|
2
|
+
declare function adminLogin(request: Request): Promise<Response>;
|
|
3
|
+
/** POST /api/admin/logout:清除 admin 登录 Cookie。 */
|
|
4
|
+
declare function adminLogout(): Response;
|
|
5
|
+
/** 跑一次极简 LLM 调用,把用户首条消息总结成 6-12 字中文标题(fire-and-forget,失败返回空串)。
|
|
6
|
+
* 失败不打扰对话:标题保持快速占位(deriveQuickTitle)。 */
|
|
7
|
+
export declare function summarizeSessionTitle(userMessage: string): Promise<string>;
|
|
8
|
+
export declare function createSessionHandlers(): {
|
|
9
|
+
list: (request: Request) => Promise<Response>;
|
|
10
|
+
create: (request: Request) => Promise<Response>;
|
|
11
|
+
rename: (request: Request, ctx: unknown) => Promise<Response>;
|
|
12
|
+
remove: (request: Request, ctx: unknown) => Promise<Response>;
|
|
13
|
+
messages: (request: Request, ctx: unknown) => Promise<Response>;
|
|
14
|
+
appendMessage: (request: Request, ctx: unknown) => Promise<Response>;
|
|
15
|
+
autoTitle: (request: Request, ctx: unknown) => Promise<Response>;
|
|
16
|
+
truncate: (request: Request, ctx: unknown) => Promise<Response>;
|
|
17
|
+
fork: (request: Request, ctx: unknown) => Promise<Response>;
|
|
18
|
+
closeSdk: (request: Request, ctx: unknown) => Promise<Response>;
|
|
19
|
+
adminUsage: (request: Request) => Promise<Response>;
|
|
20
|
+
adminLogin: typeof adminLogin;
|
|
21
|
+
adminLogout: typeof adminLogout;
|
|
22
|
+
modelContext: (request: Request) => Promise<Response>;
|
|
23
|
+
};
|
|
24
|
+
export {};
|