@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,330 @@
1
+ /**
2
+ * agent-kit · web-fetch MCP 工具(方案B:真实浏览器抓取,正文 + 图片)
3
+ *
4
+ * 背景:CodeBuddy 内置网页抓取工具能渲染正文,但内容清洗管线会剥离 <img>;
5
+ * 而今日头条等反爬站图片 CDN 对冷 HTTP 请求一律 403,图片必须在页面会话内抓取。
6
+ *
7
+ * 本模块用 Playwright 持久浏览器上下文(launchPersistentContext)实现:
8
+ * - 真实 Chromium 执行页面 JS(自动过 __ac_signature 类签名挑战);
9
+ * - 图片在页面上下文内 fetch(携带会话),绕开图片 CDN 403;
10
+ * - 持久 userDataDir 复用登录态/会话,降低反复建连开销。
11
+ *
12
+ * 对外暴露一个 MCP 工具:fetch_webpage(url, maxImages?, maxTextChars?)
13
+ * 返回:{ title, text, images: [{ url, mediaType, bytes, base64 }] }
14
+ */
15
+ import { chromium } from "playwright";
16
+ import { z } from "zod";
17
+ import { createSdkMcpServer, tool, } from "@tencent-ai/agent-sdk";
18
+ import { mkdirSync } from "node:fs";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ /**
22
+ * 持久浏览器用户数据目录(保留会话 cookie)。
23
+ * 解析顺序:1) 环境变量 WEB_FETCH_PROFILE_DIR;2) monorepo 仓库根/.web-fetch-profile
24
+ * (由本文件位置上溯 4 级定位,src 与 dist 结构一致);3) 兜底当前工作目录。
25
+ * 统一固定位置,避免各进程(agent-kit CLI / web Next.js)各自散落一份。
26
+ */
27
+ function defaultProfileDir() {
28
+ if (process.env.WEB_FETCH_PROFILE_DIR?.trim()) {
29
+ return path.resolve(process.env.WEB_FETCH_PROFILE_DIR.trim());
30
+ }
31
+ try {
32
+ const here = path.dirname(fileURLToPath(import.meta.url));
33
+ const repoRoot = path.resolve(here, "..", "..", "..", "..");
34
+ return path.join(repoRoot, ".web-fetch-profile");
35
+ }
36
+ catch {
37
+ return path.join(process.cwd(), ".web-fetch-profile");
38
+ }
39
+ }
40
+ /** 是否头条系图片 URL(仅用于头条页图片去重范围;普通页面取 article 内全部图片) */
41
+ export function isMediaUrl(u) {
42
+ return (u.startsWith("http") &&
43
+ /\.(jpe?g|png|webp|gif)([?#]|$)/i.test(u) ||
44
+ /toutiaoimg\.com|byteimg\.com|toutiaostatic\.com/.test(u));
45
+ }
46
+ /**
47
+ * 页面内收集真实图片 URL(处理懒加载 data-src / currentSrc / background-image)。
48
+ * 注意:以字符串(IIFE 表达式)形式传给 page.evaluate —— tsx/esbuild 会把回调
49
+ * 转换成带模块级 __name helper 的形式,浏览器上下文无该 helper 会 ReferenceError;
50
+ * 而裸箭头函数字符串会被 evaluate 当作表达式求值(返回函数对象,不执行),
51
+ * 因此必须写成立即执行的 IIFE。
52
+ */
53
+ const COLLECT_IMAGES_JS = `(() => {
54
+ const seen = new Set();
55
+ const add = (u) => { if (u && u.startsWith("http")) seen.add(u); };
56
+ const scope = document.querySelector("article") || document.querySelector(".article-content") || document.body;
57
+ const nodes = scope ? scope.querySelectorAll("img, [data-src], [style]") : [];
58
+ for (const el of Array.from(nodes)) {
59
+ add(el.currentSrc || el.src || "");
60
+ add((el.dataset && el.dataset.src) || el.getAttribute("data-src") || "");
61
+ const bg = (el.style && el.style.backgroundImage) || "";
62
+ const m = bg.match(/url\\(['"]?([^'")]+)['"]?\\)/);
63
+ if (m) add(m[1]);
64
+ }
65
+ return [...seen];
66
+ })()`;
67
+ export function collectImageUrls(page) {
68
+ return page.evaluate(COLLECT_IMAGES_JS);
69
+ }
70
+ /** 由 URL 扩展名 + 文件魔数推断图片 media type */
71
+ function sniffMediaType(url, buf) {
72
+ const ext = (url.match(/\.(jpe?g|png|webp|gif)([?#]|$)/i) || [])[1]?.toLowerCase();
73
+ if (ext === "png")
74
+ return "image/png";
75
+ if (ext === "webp")
76
+ return "image/webp";
77
+ if (ext === "gif")
78
+ return "image/gif";
79
+ if (ext && ext.startsWith("jpe"))
80
+ return "image/jpeg";
81
+ if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
82
+ return "image/png";
83
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
84
+ return "image/jpeg";
85
+ if (buf.length >= 4 && buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46)
86
+ return "image/webp";
87
+ return "image/jpeg";
88
+ }
89
+ /** 滚动触发懒加载(IIFE 字符串形式) */
90
+ const SCROLL_JS = `(async () => {
91
+ for (let i = 0; i < 6; i++) {
92
+ window.scrollBy(0, 900);
93
+ await new Promise((r) => setTimeout(r, 250));
94
+ }
95
+ window.scrollTo(0, 0);
96
+ })()`;
97
+ /** 提取正文(IIFE 字符串形式,参数内联) */
98
+ function extractTextJs(maxLen) {
99
+ return `((maxLen) => {
100
+ const article = document.querySelector("article") || document.querySelector(".article-content");
101
+ const raw = article ? article.innerText : document.body.innerText;
102
+ return { raw: raw.slice(0, maxLen), truncated: raw.length > maxLen };
103
+ })(${maxLen})`;
104
+ }
105
+ /**
106
+ * WebFetchService:单例浏览器(持久上下文),串行处理请求。
107
+ * 浏览器实例跨请求复用;每个请求独立 page,处理完即关。
108
+ */
109
+ export class WebFetchService {
110
+ static instance = null;
111
+ contextPromise = null;
112
+ queue = Promise.resolve();
113
+ opts;
114
+ constructor(opts = {}) {
115
+ this.opts = {
116
+ maxImages: opts.maxImages ?? 3,
117
+ maxTextChars: opts.maxTextChars ?? 60000,
118
+ maxBytesPerImage: opts.maxBytesPerImage ?? 800 * 1024,
119
+ navigationTimeoutMs: opts.navigationTimeoutMs ?? 25_000,
120
+ profileDir: opts.profileDir ?? defaultProfileDir(),
121
+ headless: opts.headless ?? true,
122
+ };
123
+ try {
124
+ mkdirSync(this.opts.profileDir, { recursive: true });
125
+ }
126
+ catch {
127
+ /* 目录已存在等情况忽略 */
128
+ }
129
+ }
130
+ static get(opts) {
131
+ if (!WebFetchService.instance)
132
+ WebFetchService.instance = new WebFetchService(opts);
133
+ return WebFetchService.instance;
134
+ }
135
+ context() {
136
+ if (!this.contextPromise) {
137
+ this.contextPromise = chromium
138
+ .launchPersistentContext(this.opts.profileDir, {
139
+ headless: this.opts.headless,
140
+ viewport: { width: 1280, height: 900 },
141
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
142
+ })
143
+ .catch((err) => {
144
+ this.contextPromise = null;
145
+ throw new Error(`浏览器启动失败:${err instanceof Error ? err.message : err}`);
146
+ });
147
+ }
148
+ return this.contextPromise;
149
+ }
150
+ /** 串行执行(同一浏览器上下文同一时刻只跑一个请求) */
151
+ async run(fn) {
152
+ const p = this.queue.then(fn);
153
+ this.queue = p.then(() => undefined, () => undefined);
154
+ return p;
155
+ }
156
+ /** 抓取单个网页:正文 + 图片(base64) */
157
+ fetchWebpage(url, opts = {}) {
158
+ return this.run(() => this.doFetch(url, opts));
159
+ }
160
+ async doFetch(url, opts) {
161
+ const maxImages = opts.maxImages ?? this.opts.maxImages;
162
+ const maxTextChars = opts.maxTextChars ?? this.opts.maxTextChars;
163
+ const maxBytes = opts.maxBytesPerImage ?? this.opts.maxBytesPerImage;
164
+ const navTimeout = opts.navigationTimeoutMs ?? this.opts.navigationTimeoutMs;
165
+ const context = await this.context();
166
+ const page = await context.newPage();
167
+ // 页面图片真实加载的响应体(走浏览器网络栈,绕开冷请求 403 与跨域 CORS)
168
+ const imageBodies = new Map();
169
+ const onResponse = (resp) => {
170
+ if (resp.request().resourceType() !== "image")
171
+ return;
172
+ const u = resp.url();
173
+ if (!isMediaUrl(u))
174
+ return;
175
+ resp
176
+ .body()
177
+ .then((b) => {
178
+ if (b.length <= maxBytes)
179
+ imageBodies.set(u, b);
180
+ })
181
+ .catch(() => undefined);
182
+ };
183
+ page.on("response", onResponse);
184
+ try {
185
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: navTimeout });
186
+ // 头条等页面先出签名挑战 JS、随后 reload 渲染正文:等待 article 出现
187
+ try {
188
+ await page.waitForSelector("article, .article-content", { timeout: 20_000 });
189
+ }
190
+ catch {
191
+ /* 非文章页:不阻塞,走 body 提取 */
192
+ }
193
+ // 滚动触发懒加载
194
+ try {
195
+ await page.evaluate(SCROLL_JS);
196
+ }
197
+ catch {
198
+ /* 忽略 */
199
+ }
200
+ // 再等图片加载落定
201
+ await new Promise((r) => setTimeout(r, 800));
202
+ const title = await page.title().catch(() => "");
203
+ let text = "";
204
+ let truncated = false;
205
+ try {
206
+ const out = (await page.evaluate(extractTextJs(maxTextChars)));
207
+ text = out?.raw ?? "";
208
+ truncated = !!out?.truncated;
209
+ }
210
+ catch {
211
+ /* 页面结构异常时 text 保持空 */
212
+ }
213
+ let imageSkipped = 0;
214
+ const images = [];
215
+ if (maxImages > 0) {
216
+ let urls = await collectImageUrls(page).catch(() => []);
217
+ urls = [...new Set(urls)].filter(isMediaUrl);
218
+ for (const u of urls) {
219
+ if (images.length >= maxImages) {
220
+ imageSkipped += urls.length - images.length;
221
+ break;
222
+ }
223
+ // 1) 响应监听缓存;2) 兜底:共享会话 cookie 的 Node 侧请求
224
+ let buf = imageBodies.get(u) ?? null;
225
+ if (!buf) {
226
+ try {
227
+ const r = await context.request.get(u);
228
+ if (r.ok())
229
+ buf = Buffer.from(await r.body());
230
+ }
231
+ catch {
232
+ buf = null;
233
+ }
234
+ }
235
+ if (!buf || buf.length === 0 || buf.length > maxBytes) {
236
+ imageSkipped += 1;
237
+ continue;
238
+ }
239
+ images.push({
240
+ url: u,
241
+ mediaType: sniffMediaType(u, buf),
242
+ bytes: buf.length,
243
+ base64: buf.toString("base64"),
244
+ });
245
+ }
246
+ }
247
+ return {
248
+ url,
249
+ title: title.replace(/\s*-\s*今日头条\s*$/, "").trim(),
250
+ text,
251
+ images,
252
+ imageSkipped,
253
+ truncated,
254
+ };
255
+ }
256
+ finally {
257
+ page.removeListener("response", onResponse);
258
+ await page.close().catch(() => undefined);
259
+ }
260
+ }
261
+ /** 释放浏览器(进程退出时调用) */
262
+ async close() {
263
+ if (this.contextPromise) {
264
+ const ctx = await this.contextPromise.catch(() => null);
265
+ if (ctx)
266
+ await ctx.close().catch(() => undefined);
267
+ this.contextPromise = null;
268
+ }
269
+ }
270
+ }
271
+ /** 每次调用创建独立 McpServer 实例(绝不共享):agent-sdk 每个 query 的 transport 都会对
272
+ * 传入实例调用 server.connect(),而 MCP Protocol 实例只允许 connect 一次;共享实例在并发
273
+ * 或先后复用时抛 "Already connected to a transport"。薄壳开销可忽略(WebFetchService 为单例)。 */
274
+ export function createWebFetchMcpServer(opts) {
275
+ const service = WebFetchService.get(opts);
276
+ return createSdkMcpServer({
277
+ name: "web-fetch",
278
+ version: "1.0.0",
279
+ tools: [
280
+ tool("fetch_webpage", "只读抓取并展示一个网页的完整内容:返回页面标题、正文文本,以及页面内图片的 base64 数据(可直接内联到回复或 HTML 中展示)。用真实浏览器(Playwright 持久上下文)渲染,能处理 JS 动态渲染与反爬签名校验页面(如今日头条),图片在页面会话内获取(冷请求会被 CDN 拒绝 403)。这是「只读展示网页内容」的标准工具——当用户要求「查看 / 显示 / 读取 / 翻译 / 总结某篇文章或网页的内容(含图片)」时,应优先使用本工具,而不是去「操作」浏览器。适合新闻文章、博客、帖子等文章型页面。", {
281
+ url: z.string().describe("要抓取的网页完整 URL(https://)"),
282
+ maxImages: z
283
+ .number()
284
+ .int()
285
+ .min(0)
286
+ .max(10)
287
+ .optional()
288
+ .describe("最多返回图片数,默认 3;0 表示不抓图片"),
289
+ maxTextChars: z
290
+ .number()
291
+ .int()
292
+ .min(0)
293
+ .max(100000)
294
+ .optional()
295
+ .describe("正文最大字符数,默认 60000"),
296
+ }, async ({ url, maxImages, maxTextChars }) => {
297
+ try {
298
+ const result = await service.fetchWebpage(url, { maxImages, maxTextChars });
299
+ const totalBytes = result.images.reduce((s, i) => s + (i.bytes || 0), 0);
300
+ console.log(`[agent-kit web-fetch] 抓取成功 url=${url} 正文=${result.text.length}字 图片=${result.images.length}张 图片字节=${totalBytes}${result.truncated ? " (正文截断)" : ""}${result.imageSkipped ? " (图片超限跳过)" : ""}`);
301
+ const summary = {
302
+ url: result.url,
303
+ title: result.title,
304
+ text: result.text,
305
+ images: result.images.map((i) => ({
306
+ url: i.url,
307
+ mediaType: i.mediaType,
308
+ bytes: i.bytes,
309
+ base64: i.base64,
310
+ })),
311
+ imageSkipped: result.imageSkipped,
312
+ truncated: result.truncated,
313
+ };
314
+ return { content: [{ type: "text", text: JSON.stringify(summary) }] };
315
+ }
316
+ catch (err) {
317
+ console.error(`[agent-kit web-fetch] 抓取失败 url=${url}:`, err instanceof Error ? err.stack || err.message : err);
318
+ return {
319
+ content: [
320
+ {
321
+ type: "text",
322
+ text: `抓取失败:${err instanceof Error ? err.message : String(err)}`,
323
+ },
324
+ ],
325
+ };
326
+ }
327
+ }),
328
+ ],
329
+ });
330
+ }