From 506a551438aad6aea57613e404a424d846bbaad3 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 3 Jul 2026 00:34:03 +0800 Subject: [PATCH] feat: polish mindspace decoupling flows --- .runtime/portal/RUNBOOK.txt | 10 + .runtime/portal/mindspace-public-links.mjs | 151 + .runtime/portal/mindspace-sandbox-mcp.mjs | 156 +- .runtime/portal/package.json | 1 + .runtime/portal/public/brand/tkmind-icon.png | Bin 0 -> 62745 bytes .../portal/public/dev/wechat-share-demo.html | 50 + .../public/dev/wechat-share-preview.html | 41 + .runtime/portal/schema.sql | 52 + .runtime/portal/scripts/agent-run-guard.mjs | 346 + .runtime/portal/scripts/agent-run-worker.mjs | 2864 +++++++- .../scripts/check-agent-code-run-entry.mjs | 2 + .../portal/scripts/check-agent-run-worker.mjs | 229 + .../scripts/check-mindspace-public-links.mjs | 60 + .../portal/scripts/check-tool-runtime.mjs | 13 + .../scripts/install-agent-run-guard-agent.sh | 86 + .../scripts/install-agent-run-worker-agent.sh | 111 + .../install-runtime-heartbeat-agent.sh | 76 + .../install-runtime-slo-report-agent.sh | 5 + .../scripts/install-runtime-slo-soak-agent.sh | 79 + .runtime/portal/scripts/load-env.mjs | 20 + .../portal/scripts/memind-portal-tunnel.sh | 14 + .../portal/scripts/runtime-slo-report.mjs | 132 +- .../scripts/runtime-worker-heartbeat.mjs | 164 + .runtime/portal/scripts/wechat-mp-menu.mjs | 138 + .runtime/portal/server.mjs | 6453 ++++++++++++----- .runtime/portal/skills/docx-generate/SKILL.md | 2 +- .../skills/docx-generate/generate_docx.py | 1 - .../skills/long-image-download/SKILL.md | 36 + .../skills/static-page-publish/SKILL.md | 25 +- .runtime/portal/skills/web/SKILL.md | 6 + mindspace-conversation-package-routes.mjs | 15 +- ...space-conversation-package-routes.test.mjs | 13 +- mindspace-conversation-package.mjs | 5 + mindspace-conversation-package.test.mjs | 7 + scripts/build-portal-runtime.mjs | 5 + src/components/ChatPanel.tsx | 309 +- src/components/ChatView.tsx | 238 +- src/components/ConversationPackagePanel.tsx | 288 + src/index.css | 203 +- src/types.ts | 1 + 40 files changed, 10037 insertions(+), 2370 deletions(-) create mode 100644 .runtime/portal/mindspace-public-links.mjs create mode 100644 .runtime/portal/public/brand/tkmind-icon.png create mode 100644 .runtime/portal/public/dev/wechat-share-demo.html create mode 100644 .runtime/portal/public/dev/wechat-share-preview.html create mode 100755 .runtime/portal/scripts/agent-run-guard.mjs create mode 100755 .runtime/portal/scripts/check-agent-run-worker.mjs create mode 100644 .runtime/portal/scripts/check-mindspace-public-links.mjs create mode 100755 .runtime/portal/scripts/install-agent-run-guard-agent.sh create mode 100755 .runtime/portal/scripts/install-agent-run-worker-agent.sh create mode 100755 .runtime/portal/scripts/install-runtime-heartbeat-agent.sh create mode 100755 .runtime/portal/scripts/install-runtime-slo-soak-agent.sh create mode 100644 .runtime/portal/scripts/load-env.mjs create mode 100755 .runtime/portal/scripts/memind-portal-tunnel.sh create mode 100755 .runtime/portal/scripts/runtime-worker-heartbeat.mjs create mode 100755 .runtime/portal/scripts/wechat-mp-menu.mjs create mode 100644 .runtime/portal/skills/long-image-download/SKILL.md create mode 100644 src/components/ConversationPackagePanel.tsx diff --git a/.runtime/portal/RUNBOOK.txt b/.runtime/portal/RUNBOOK.txt index 6070dfb..478b5fc 100644 --- a/.runtime/portal/RUNBOOK.txt +++ b/.runtime/portal/RUNBOOK.txt @@ -41,9 +41,14 @@ Streaming runtime operations: node scripts/runtime-worker-metrics.mjs status node scripts/runtime-worker-metrics.mjs sample bash scripts/install-runtime-metrics-agent.sh + node scripts/runtime-worker-heartbeat.mjs once + node scripts/runtime-worker-heartbeat.mjs serve + bash scripts/install-runtime-heartbeat-agent.sh node scripts/runtime-slo-report.mjs node scripts/runtime-slo-report.mjs --write-report + node scripts/runtime-slo-report.mjs --write-report --prune --retention-days 30 bash scripts/install-runtime-slo-report-agent.sh + bash scripts/install-runtime-slo-soak-agent.sh node scripts/runtime-worker-drain.mjs drain goosed-3 node scripts/runtime-worker-drain.mjs undrain goosed-3 node scripts/check-tool-runtime.mjs @@ -51,3 +56,8 @@ Streaming runtime operations: curl -sk https://mm.tkmind.cn/api/runtime/status # includes toolRuntime.queue node scripts/agent-run-worker.mjs --status node scripts/agent-run-worker.mjs --once + bash scripts/install-agent-run-worker-agent.sh # installs disabled by default + node scripts/check-agent-run-worker.mjs # read-only LaunchAgent/queue check + node scripts/agent-run-guard.mjs # dry-run auto-pause guard check + node scripts/agent-run-guard.mjs --apply # stop worker and disable code-run gate when thresholds trip + bash scripts/install-agent-run-guard-agent.sh # installs guard LaunchAgent diff --git a/.runtime/portal/mindspace-public-links.mjs b/.runtime/portal/mindspace-public-links.mjs new file mode 100644 index 0000000..ce459dc --- /dev/null +++ b/.runtime/portal/mindspace-public-links.mjs @@ -0,0 +1,151 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const SKIP_REFERENCE = /^(?:https?:|mailto:|javascript:|data:|tel:|#|\/\/)/i; +const DOWNLOAD_EXTENSIONS = /\.(?:docx|doc|pdf|xlsx|xls|pptx|ppt|zip|rar|7z|csv)$/i; + +export function isFilesystemPublicReference(raw) { + const value = String(raw ?? '').trim(); + if (!value) return false; + if (SKIP_REFERENCE.test(value)) return false; + // /api/... and /MindSpace/... are server routes, not on-disk siblings of public HTML. + if (value.startsWith('/')) return false; + return true; +} + +/** @deprecated use isFilesystemPublicReference */ +export function isRelativePublicReference(raw) { + return isFilesystemPublicReference(raw); +} + +export function isDownloadLikeReference(raw, source = '') { + if (!isFilesystemPublicReference(raw)) return false; + const ref = normalizeReferencePath(raw); + if (DOWNLOAD_EXTENSIONS.test(ref)) return true; + return /\bdownload\b/i.test(String(source ?? '')); +} + +export function normalizeReferencePath(raw) { + const trimmed = String(raw ?? '') + .trim() + .split('#')[0] + .split('?')[0] + .trim(); + if (!trimmed) return ''; + try { + return decodeURIComponent(trimmed); + } catch { + return trimmed; + } +} + +export function resolveReferencePath(htmlDir, raw) { + const ref = normalizeReferencePath(raw); + if (!ref) return null; + return path.resolve(htmlDir, ref); +} + +function parseMindspaceCoverPaths(contentAttr) { + const paths = []; + if (!contentAttr) return paths; + let parsed = null; + try { + parsed = JSON.parse(contentAttr); + } catch { + for (const key of ['cover', 'image']) { + const match = contentAttr.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`)); + if (match?.[1]) paths.push(match[1]); + } + return paths; + } + for (const key of ['cover', 'image']) { + if (parsed?.[key]) paths.push(String(parsed[key])); + } + return paths; +} + +export function collectPublicHtmlReferences(html, htmlDir, { downloadsOnly = false } = {}) { + const refs = []; + const seen = new Set(); + + const add = (raw, source) => { + if (downloadsOnly && !isDownloadLikeReference(raw, source)) return; + if (!downloadsOnly && !isFilesystemPublicReference(raw)) return; + const ref = normalizeReferencePath(raw); + if (!ref) return; + const key = `${source}\0${ref}`; + if (seen.has(key)) return; + seen.add(key); + refs.push({ + ref, + source, + resolvedPath: resolveReferencePath(htmlDir, ref), + }); + }; + + for (const match of String(html ?? '').matchAll(/\b(?:href|src)\s*=\s*["']([^"']+)["']/gi)) { + add(match[1], match[0]); + } + + if (!downloadsOnly) { + for (const match of String(html ?? '').matchAll( + /]*\bname\s*=\s*["']mindspace-cover["'][^>]*>/gi, + )) { + const tag = match[0]; + const contentMatch = + tag.match(/\bcontent\s*=\s*"([^"]*)"/i) ?? tag.match(/\bcontent\s*=\s*'([^']*)'/i); + for (const coverPath of parseMindspaceCoverPaths(contentMatch?.[1])) { + add(coverPath, 'mindspace-cover'); + } + } + } + + return refs; +} + +export function findMissingPublicHtmlReferences(htmlPath, html = null, options = {}) { + const resolvedHtmlPath = path.resolve(htmlPath); + const content = html ?? fs.readFileSync(resolvedHtmlPath, 'utf8'); + const htmlDir = path.dirname(resolvedHtmlPath); + const missing = []; + const seen = new Set(); + + for (const item of collectPublicHtmlReferences(content, htmlDir, options)) { + const key = item.ref; + if (seen.has(key)) continue; + seen.add(key); + if (!item.resolvedPath || !fs.existsSync(item.resolvedPath)) { + missing.push({ htmlPath: resolvedHtmlPath, ...item }); + } + } + + return missing; +} + +export function listPublicHtmlFiles(publishRoot, { userId = null } = {}) { + const root = path.resolve(publishRoot); + if (!fs.existsSync(root)) return []; + + const userDirs = userId + ? [path.join(root, userId)] + : fs.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path.join(root, d.name)); + + const files = []; + for (const userDir of userDirs) { + const publicDir = path.join(userDir, 'public'); + if (!fs.existsSync(publicDir) || !fs.statSync(publicDir).isDirectory()) continue; + for (const name of fs.readdirSync(publicDir)) { + if (!name.toLowerCase().endsWith('.html')) continue; + files.push(path.join(publicDir, name)); + } + } + return files.sort(); +} + +export function scanPublicHtmlLinks(publishRoot, options = {}) { + const issues = []; + for (const htmlPath of listPublicHtmlFiles(publishRoot, options)) { + issues.push(...findMissingPublicHtmlReferences(htmlPath, null, options)); + } + return issues; +} diff --git a/.runtime/portal/mindspace-sandbox-mcp.mjs b/.runtime/portal/mindspace-sandbox-mcp.mjs index 3f59298..f190dd5 100755 --- a/.runtime/portal/mindspace-sandbox-mcp.mjs +++ b/.runtime/portal/mindspace-sandbox-mcp.mjs @@ -2,8 +2,8 @@ import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url); // mindspace-sandbox-mcp.mjs -import path from "node:path"; -import fs from "node:fs"; +import path2 from "node:path"; +import fs2 from "node:fs"; import readline from "node:readline"; import { execFileSync } from "node:child_process"; import mysql from "mysql2/promise"; @@ -1132,15 +1132,102 @@ function createScheduleService(pool, options = {}) { }; } +// mindspace-long-image.mjs +import fs from "node:fs"; +import fsPromises from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +var DEFAULT_VIEWPORT_WIDTH = 1280; +var DEFAULT_VIEWPORT_HEIGHT = 720; +var MAX_LONG_IMAGE_HEIGHT = 2e4; +function longImagePathForHtml(htmlPath, outputPath = null) { + return outputPath || String(htmlPath).replace(/\.html$/i, ".long.png"); +} +function clampDimension(value, fallback, max) { + const number = Math.ceil(Number(value) || fallback); + return Math.max(320, Math.min(number, max)); +} +async function launchChromium(chromium) { + const base = { + headless: true, + args: ["--disable-dev-shm-usage", "--hide-scrollbars"] + }; + if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) { + return chromium.launch({ + ...base, + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH + }); + } + if (process.env.PLAYWRIGHT_CHROMIUM_CHANNEL) { + return chromium.launch({ ...base, channel: process.env.PLAYWRIGHT_CHROMIUM_CHANNEL }); + } + try { + return await chromium.launch(base); + } catch (error) { + if (process.platform === "darwin") { + return chromium.launch({ ...base, channel: "chrome" }); + } + throw error; + } +} +async function renderLongImage({ + htmlPath = null, + url = null, + outputPath = null, + viewportWidth = DEFAULT_VIEWPORT_WIDTH, + viewportHeight = DEFAULT_VIEWPORT_HEIGHT +} = {}) { + if (!htmlPath && !url) throw new Error("\u7F3A\u5C11 htmlPath \u6216 url"); + const targetUrl = url || pathToFileURL(path.resolve(htmlPath)).toString(); + const destination = outputPath ? path.resolve(outputPath) : longImagePathForHtml(path.resolve(htmlPath)); + const { chromium } = await import("playwright"); + let browser = null; + try { + browser = await launchChromium(chromium); + const page = await browser.newPage({ + viewport: { + width: clampDimension(viewportWidth, DEFAULT_VIEWPORT_WIDTH, 2400), + height: clampDimension(viewportHeight, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT) + }, + deviceScaleFactor: 2 + }); + await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 3e4 }); + await page.evaluate(() => document.fonts?.ready).catch(() => null); + const size = await page.evaluate(() => ({ + width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, 320), + height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, 320) + })); + await page.setViewportSize({ + width: clampDimension(size.width, DEFAULT_VIEWPORT_WIDTH, 2400), + height: Math.min(clampDimension(size.height, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT), 2400) + }); + await fsPromises.mkdir(path.dirname(destination), { recursive: true }); + await page.screenshot({ + path: destination, + fullPage: true, + type: "png", + animations: "disabled", + caret: "hide" + }); + return { + outputPath: destination, + bytes: fs.statSync(destination).size + }; + } finally { + await browser?.close().catch(() => { + }); + } +} + // mindspace-sandbox-mcp.mjs var SANDBOX_ROOT = process.argv[2]?.trim() || process.env.SANDBOX_ROOT?.trim(); if (!SANDBOX_ROOT) { process.stderr.write("[mindspace-sandbox-mcp] SANDBOX_ROOT is not set \u2014 refusing to start\n"); process.exit(1); } -var SANDBOX = path.resolve(SANDBOX_ROOT); -var PRIVATE_DATA_DIR = path.join(SANDBOX, ".mindspace"); -var PRIVATE_DATA_DB = path.join(PRIVATE_DATA_DIR, "private-data.sqlite"); +var SANDBOX = path2.resolve(SANDBOX_ROOT); +var PRIVATE_DATA_DIR = path2.join(SANDBOX, ".mindspace"); +var PRIVATE_DATA_DB = path2.join(PRIVATE_DATA_DIR, "private-data.sqlite"); var SQLITE_BIN = process.env.SQLITE_BIN?.trim() || "sqlite3"; var PRIVATE_DATA_MAX_BYTES = Number(process.env.PRIVATE_DATA_MAX_BYTES ?? 20 * 1024 * 1024); var PRIVATE_DATA_QUERY_TIMEOUT_MS = Number(process.env.PRIVATE_DATA_QUERY_TIMEOUT_MS ?? 5e3); @@ -1150,11 +1237,11 @@ var allowedToolsEnv = process.env.ALLOWED_TOOLS?.trim(); var ALLOWED_TOOLS = allowedToolsEnv ? new Set(allowedToolsEnv.split(",").map((s) => s.trim())) : null; function resolveSandboxed(p) { if (!p || typeof p !== "string") throw new Error("\u8DEF\u5F84\u53C2\u6570\u65E0\u6548"); - const resolved = path.isAbsolute(p) ? path.resolve(p) : path.resolve(SANDBOX, p); - if (resolved !== SANDBOX && !resolved.startsWith(SANDBOX + path.sep)) { + const resolved = path2.isAbsolute(p) ? path2.resolve(p) : path2.resolve(SANDBOX, p); + if (resolved !== SANDBOX && !resolved.startsWith(SANDBOX + path2.sep)) { throw Object.assign(new Error(`\u8DEF\u5F84\u8D8A\u754C\uFF1A${p} \u4E0D\u5728\u5F53\u524D\u5DE5\u4F5C\u533A\u5185`), { code: "EACCES" }); } - if (resolved === PRIVATE_DATA_DIR || resolved.startsWith(PRIVATE_DATA_DIR + path.sep)) { + if (resolved === PRIVATE_DATA_DIR || resolved.startsWith(PRIVATE_DATA_DIR + path2.sep)) { throw Object.assign(new Error("\u7981\u6B62\u76F4\u63A5\u8BBF\u95EE\u79C1\u6709\u6570\u636E\u76EE\u5F55\uFF0C\u8BF7\u4F7F\u7528 private_data_* \u5DE5\u5177"), { code: "EACCES" }); } return resolved; @@ -1220,6 +1307,18 @@ var ALL_TOOLS = [ required: ["path"] } }, + { + name: "generate_long_image", + description: "\u7528 Playwright \u5C06\u5DE5\u4F5C\u533A\u5185\u7684 HTML \u9875\u9762\u6E32\u67D3\u4E3A\u6574\u9875 PNG \u957F\u56FE\u3002\u8F93\u51FA\u6587\u4EF6\u901A\u5E38\u4E3A public/<\u9875\u9762\u540D>.long.png\uFF0C\u5FC5\u987B\u5728\u5DE5\u4F5C\u533A\u5185\u3002", + inputSchema: { + type: "object", + properties: { + html_path: { type: "string", description: "HTML \u6587\u4EF6\u8DEF\u5F84\uFF0C\u5982 public/report.html" }, + output_path: { type: "string", description: "\u8F93\u51FA PNG \u8DEF\u5F84\uFF0C\u5982 public/report.long.png\uFF1B\u53EF\u9009" } + }, + required: ["html_path"] + } + }, { name: "private_data_info", description: "\u67E5\u770B\u5F53\u524D\u7528\u6237\u201C\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u201D\u7684\u72B6\u6001\u3002\u6BCF\u4E2A\u7528\u6237\u53EA\u6709\u4E00\u4E2A\u79C1\u6709 SQLite \u6570\u636E\u5E93\uFF0C\u9002\u5408\u4FDD\u5B58\u95EE\u5377\u3001\u8868\u5355\u3001\u6E05\u5355\u3001\u8C03\u7814\u6570\u636E\u548C\u5206\u6790\u4E2D\u95F4\u8868\uFF1B\u4E0D\u8981\u521B\u5EFA\u989D\u5916 SQLite \u6587\u4EF6\u3002", @@ -1348,17 +1447,17 @@ if (isScheduleConfigured()) { } var TOOLS = ALLOWED_TOOLS ? ALL_TOOLS.filter((t) => ALLOWED_TOOLS.has(t.name)) : ALL_TOOLS; function ensurePrivateDataDb() { - fs.mkdirSync(PRIVATE_DATA_DIR, { recursive: true }); - if (!fs.existsSync(PRIVATE_DATA_DB)) { + fs2.mkdirSync(PRIVATE_DATA_DIR, { recursive: true }); + if (!fs2.existsSync(PRIVATE_DATA_DB)) { runSqlite(["-batch", PRIVATE_DATA_DB, "PRAGMA journal_mode=WAL; PRAGMA user_version = 1;"]); } return PRIVATE_DATA_DB; } function privateDataSize() { let total = 0; - for (const file of fs.existsSync(PRIVATE_DATA_DIR) ? fs.readdirSync(PRIVATE_DATA_DIR) : []) { + for (const file of fs2.existsSync(PRIVATE_DATA_DIR) ? fs2.readdirSync(PRIVATE_DATA_DIR) : []) { if (file === "private-data.sqlite" || file.startsWith("private-data.sqlite-")) { - total += fs.statSync(path.join(PRIVATE_DATA_DIR, file)).size; + total += fs2.statSync(path2.join(PRIVATE_DATA_DIR, file)).size; } } return total; @@ -1505,38 +1604,57 @@ async function callTool(name, args) { switch (name) { case "read_file": { const abs = resolveSandboxed(args.path); - const content = fs.readFileSync(abs, "utf8"); + const content = fs2.readFileSync(abs, "utf8"); return [{ type: "text", text: content }]; } case "write_file": { const abs = resolveSandboxed(args.path); - fs.mkdirSync(path.dirname(abs), { recursive: true }); - fs.writeFileSync(abs, args.content ?? "", "utf8"); + fs2.mkdirSync(path2.dirname(abs), { recursive: true }); + fs2.writeFileSync(abs, args.content ?? "", "utf8"); return [{ type: "text", text: `\u5DF2\u5199\u5165 ${args.path}\uFF08${(args.content ?? "").length} \u5B57\u8282\uFF09` }]; } case "edit_file": { const abs = resolveSandboxed(args.path); - const original = fs.readFileSync(abs, "utf8"); + const original = fs2.readFileSync(abs, "utf8"); const oldStr = args.old_str ?? ""; if (oldStr && !original.includes(oldStr)) { throw new Error("edit_file: old_str \u5728\u6587\u4EF6\u4E2D\u4E0D\u5B58\u5728\uFF0C\u66FF\u6362\u5931\u8D25"); } const updated = oldStr ? original.replace(oldStr, args.new_str ?? "") : args.new_str ?? ""; - fs.writeFileSync(abs, updated, "utf8"); + fs2.writeFileSync(abs, updated, "utf8"); return [{ type: "text", text: `\u5DF2\u7F16\u8F91 ${args.path}` }]; } case "list_dir": { const target = args?.path ?? "."; const abs = resolveSandboxed(target); - const entries = fs.readdirSync(abs, { withFileTypes: true }); + const entries = fs2.readdirSync(abs, { withFileTypes: true }); const lines = entries.map((e) => `${e.isDirectory() ? "[\u76EE\u5F55]" : "[\u6587\u4EF6]"} ${e.name}`); return [{ type: "text", text: lines.join("\n") || "\uFF08\u7A7A\u76EE\u5F55\uFF09" }]; } case "create_dir": { const abs = resolveSandboxed(args.path); - fs.mkdirSync(abs, { recursive: true }); + fs2.mkdirSync(abs, { recursive: true }); return [{ type: "text", text: `\u5DF2\u521B\u5EFA\u76EE\u5F55 ${args.path}` }]; } + case "generate_long_image": { + const htmlPath = String(args.html_path ?? args.path ?? "").trim(); + if (!htmlPath.toLowerCase().endsWith(".html")) { + throw new Error("generate_long_image: html_path \u5FC5\u987B\u662F .html \u6587\u4EF6"); + } + const htmlAbs = resolveSandboxed(htmlPath); + const outputPath = String(args.output_path ?? "").trim() || htmlPath.replace(/\.html$/i, ".long.png"); + if (!outputPath.toLowerCase().endsWith(".png")) { + throw new Error("generate_long_image: output_path \u5FC5\u987B\u662F .png \u6587\u4EF6"); + } + const outputAbs = resolveSandboxed(outputPath); + const result = await renderLongImage({ htmlPath: htmlAbs, outputPath: outputAbs }); + return [ + { + type: "text", + text: `\u5DF2\u751F\u6210\u957F\u56FE ${outputPath}\uFF08${result.bytes} \u5B57\u8282\uFF09` + } + ]; + } case "private_data_info": { ensurePrivateDataDb(); const size = privateDataSize(); diff --git a/.runtime/portal/package.json b/.runtime/portal/package.json index efb75cb..e3e6c30 100644 --- a/.runtime/portal/package.json +++ b/.runtime/portal/package.json @@ -13,6 +13,7 @@ "http-proxy-middleware": "^3.0.3", "jsonrepair": "^3.14.0", "mysql2": "^3.22.5", + "playwright": "^1.61.1", "qrcode": "^1.5.4", "redis": "^4.7.1", "sharp": "^0.35.2", diff --git a/.runtime/portal/public/brand/tkmind-icon.png b/.runtime/portal/public/brand/tkmind-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..072f9ab363d9cb1498255a27ba173d207f87f2be GIT binary patch literal 62745 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 literal 0 HcmV?d00001 diff --git a/.runtime/portal/public/dev/wechat-share-demo.html b/.runtime/portal/public/dev/wechat-share-demo.html new file mode 100644 index 0000000..2b5f0a7 --- /dev/null +++ b/.runtime/portal/public/dev/wechat-share-demo.html @@ -0,0 +1,50 @@ + + + + + + 微信分享预览 · 连云港三天两夜攻略 🌊 + + + +

+

微信链接卡片预览

+

本地模拟的是「粘贴链接后看到的卡片」,不是 JS-SDK 内部分享弹层。源链接:https://m.tkmind.cn/MindSpace/john/public/lianyungang-guide.html

+

修复后:标题 + 描述 + 缩略图 + 底部「TKMind 智趣」小图标应同时出现。

+
+
+

连云港三天两夜攻略 🌊

+

三天两夜·沙滩酒店·必吃美食

+
+ + TKMind 智趣 +
+
+ +
+
+
og:title 连云港三天两夜攻略 🌊
+
og:description 三天两夜·沙滩酒店·必吃美食
+
og:site_name TKMind 智趣
+
og:image https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=200&q=80
+
icon /brand/tkmind-icon.png
+
+
+ + \ No newline at end of file diff --git a/.runtime/portal/public/dev/wechat-share-preview.html b/.runtime/portal/public/dev/wechat-share-preview.html new file mode 100644 index 0000000..761f5ee --- /dev/null +++ b/.runtime/portal/public/dev/wechat-share-preview.html @@ -0,0 +1,41 @@ + + + + + + 微信分享预览工具 + + + +
+

微信分享预览

+

输入已发布页面路径,本地模拟微信「粘贴链接后」看到的卡片。开发环境走 Portal API:/dev/wechat-share-preview

+
+ + + +
+
+ + + diff --git a/.runtime/portal/schema.sql b/.runtime/portal/schema.sql index 485f37f..984e3cd 100644 --- a/.runtime/portal/schema.sql +++ b/.runtime/portal/schema.sql @@ -11,6 +11,8 @@ CREATE TABLE IF NOT EXISTS h5_users ( status ENUM('active', 'suspended', 'disabled') NOT NULL DEFAULT 'active', plan_type VARCHAR(32) NOT NULL DEFAULT 'free', workspace_root VARCHAR(512) NOT NULL, + low_balance_gift_eligible TINYINT(1) NOT NULL DEFAULT 0, + low_balance_gift_granted_at BIGINT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, UNIQUE KEY uq_h5_users_slug (slug), @@ -121,11 +123,14 @@ CREATE TABLE IF NOT EXISTS h5_upload_sessions ( temporary_storage_key VARCHAR(512) NOT NULL, checksum CHAR(64) NULL, completed_asset_id CHAR(36) NULL, + source_session_id VARCHAR(128) NULL, + source_message_id VARCHAR(128) NULL, status ENUM('reserved', 'uploaded', 'completed', 'cancelled', 'expired', 'failed') NOT NULL DEFAULT 'reserved', expires_at BIGINT NOT NULL, created_at BIGINT NOT NULL, completed_at BIGINT NULL, KEY idx_h5_upload_user_status (user_id, status, expires_at), + KEY idx_h5_upload_source_session (user_id, source_session_id), CONSTRAINT fk_h5_upload_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE, CONSTRAINT fk_h5_upload_space FOREIGN KEY (space_id) REFERENCES h5_user_spaces(id) ON DELETE CASCADE, CONSTRAINT fk_h5_upload_category FOREIGN KEY (category_id) REFERENCES h5_space_categories(id) ON DELETE CASCADE @@ -348,6 +353,7 @@ CREATE TABLE IF NOT EXISTS h5_user_path_grants ( CREATE TABLE IF NOT EXISTS h5_user_sessions ( agent_session_id VARCHAR(128) NOT NULL PRIMARY KEY, user_id CHAR(36) NOT NULL, + origin ENUM('h5', 'wechat') NOT NULL DEFAULT 'h5', created_at BIGINT NOT NULL, KEY idx_h5_user_sessions_user (user_id), CONSTRAINT fk_h5_session_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE @@ -383,6 +389,52 @@ CREATE TABLE IF NOT EXISTS h5_agent_run_events ( CONSTRAINT fk_h5_agent_run_event_run FOREIGN KEY (run_id) REFERENCES h5_agent_runs(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS h5_conversation_packages ( + id VARCHAR(64) PRIMARY KEY, + user_id CHAR(36) NOT NULL, + session_id VARCHAR(128) NOT NULL, + title VARCHAR(255) NULL, + status ENUM('active', 'archived', 'deleted') NOT NULL DEFAULT 'active', + storage_prefix VARCHAR(512) NULL, + manifest_asset_id CHAR(36) NULL, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, + UNIQUE KEY uq_h5_conversation_package_session (user_id, session_id), + KEY idx_h5_conversation_package_user_updated (user_id, updated_at), + CONSTRAINT fk_h5_conversation_package_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE, + CONSTRAINT fk_h5_conversation_package_manifest FOREIGN KEY (manifest_asset_id) REFERENCES h5_assets(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS h5_conversation_artifacts ( + id VARCHAR(64) PRIMARY KEY, + package_id VARCHAR(64) NOT NULL, + asset_id CHAR(36) NULL, + page_id CHAR(36) NULL, + publication_id CHAR(36) NULL, + agent_run_id CHAR(36) NULL, + message_id VARCHAR(128) NULL, + role VARCHAR(32) NULL, + artifact_kind VARCHAR(64) NOT NULL, + display_name VARCHAR(255) NULL, + mime_type VARCHAR(128) NULL, + size_bytes BIGINT NULL, + storage_key VARCHAR(512) NULL, + canonical_url VARCHAR(512) NULL, + sort_order INT NOT NULL DEFAULT 0, + created_at BIGINT NOT NULL, + KEY idx_h5_conversation_artifact_package (package_id, sort_order, created_at), + KEY idx_h5_conversation_artifact_asset (asset_id), + KEY idx_h5_conversation_artifact_page (page_id), + KEY idx_h5_conversation_artifact_publication (publication_id), + KEY idx_h5_conversation_artifact_run (agent_run_id), + KEY idx_h5_conversation_artifact_message (message_id), + CONSTRAINT fk_h5_conversation_artifact_package FOREIGN KEY (package_id) REFERENCES h5_conversation_packages(id) ON DELETE CASCADE, + CONSTRAINT fk_h5_conversation_artifact_asset FOREIGN KEY (asset_id) REFERENCES h5_assets(id) ON DELETE SET NULL, + CONSTRAINT fk_h5_conversation_artifact_page FOREIGN KEY (page_id) REFERENCES h5_page_records(id) ON DELETE SET NULL, + CONSTRAINT fk_h5_conversation_artifact_publication FOREIGN KEY (publication_id) REFERENCES h5_publish_records(id) ON DELETE SET NULL, + CONSTRAINT fk_h5_conversation_artifact_run FOREIGN KEY (agent_run_id) REFERENCES h5_agent_runs(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS h5_session_billing_state ( agent_session_id VARCHAR(128) NOT NULL PRIMARY KEY, user_id CHAR(36) NOT NULL, diff --git a/.runtime/portal/scripts/agent-run-guard.mjs b/.runtime/portal/scripts/agent-run-guard.mjs new file mode 100755 index 0000000..8d1d507 --- /dev/null +++ b/.runtime/portal/scripts/agent-run-guard.mjs @@ -0,0 +1,346 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import mysql from 'mysql2/promise'; + +const execFileAsync = promisify(execFile); +const DEFAULT_WORKER_LABEL = 'cn.tkmind.memind-agent-run-worker'; +const DEFAULT_PORTAL_LABEL = 'cn.tkmind.memind-portal'; + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx < 0) continue; + const key = trimmed.slice(0, idx).trim(); + let value = trimmed.slice(idx + 1).trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + if (!process.env[key]) process.env[key] = value; + } +} + +function truthy(value) { + return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase()); +} + +function positiveInt(value, fallback) { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; +} + +function parseArgs(argv) { + const args = { + apply: argv.includes('--apply'), + dryRun: argv.includes('--dry-run') || !argv.includes('--apply'), + help: argv.includes('--help') || argv.includes('-h'), + }; + return args; +} + +function printHelp() { + console.log([ + 'Usage:', + ' node scripts/agent-run-guard.mjs [--dry-run]', + ' node scripts/agent-run-guard.mjs --apply', + '', + 'Dry-run is the default. --apply can stop the external worker and disable code-run gate in .env.', + ].join('\n')); +} + +function parseMysqlConfig() { + if (process.env.DATABASE_URL) { + const url = new URL(process.env.DATABASE_URL); + if (url.protocol !== 'mysql:') { + throw new Error(`Unsupported DATABASE_URL scheme for agent run guard: ${url.protocol}`); + } + return { + host: url.hostname, + port: Number(url.port || 3306), + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: url.pathname.replace(/^\/+/, ''), + charset: 'utf8mb4', + }; + } + return { + host: process.env.MYSQL_HOST, + port: Number(process.env.MYSQL_PORT || 3306), + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + charset: 'utf8mb4', + }; +} + +async function runCommand(command, args) { + try { + const { stdout, stderr } = await execFileAsync(command, args, { maxBuffer: 1024 * 1024 }); + return { ok: true, stdout, stderr }; + } catch (err) { + return { + ok: false, + code: err?.code ?? null, + stdout: err?.stdout ?? '', + stderr: err?.stderr ?? '', + message: err instanceof Error ? err.message : String(err), + }; + } +} + +function replaceOrAppendEnv(raw, updates) { + const pending = new Map(Object.entries(updates)); + const lines = raw.split('\n'); + const next = lines.map((line) => { + const match = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*=).*/); + if (!match) return line; + const key = match[2]; + if (!pending.has(key)) return line; + const value = pending.get(key); + pending.delete(key); + return `${key}=${value}`; + }); + if (pending.size > 0) { + if (next.length > 0 && next[next.length - 1] !== '') next.push(''); + next.push('## agent-run guard auto-disable'); + for (const [key, value] of pending) next.push(`${key}=${value}`); + } + return next.join('\n').replace(/\n*$/, '\n'); +} + +function timestamp() { + const d = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; +} + +async function readQueueHealth(now) { + const conn = await mysql.createConnection(parseMysqlConfig()); + try { + const [statusRows] = await conn.query( + `SELECT status, COUNT(*) AS count + FROM h5_agent_runs + WHERE status IN ('queued', 'running', 'retryable') + GROUP BY status`, + ); + const statusCounts = {}; + for (const row of statusRows) statusCounts[row.status] = Number(row.count ?? 0); + + const [oldestPendingRows] = await conn.query( + `SELECT MIN(updated_at) AS oldest_updated_at + FROM h5_agent_runs + WHERE status IN ('queued', 'retryable')`, + ); + const oldestPendingUpdatedAt = oldestPendingRows[0]?.oldest_updated_at == null + ? null + : Number(oldestPendingRows[0].oldest_updated_at); + + const [oldestRunningRows] = await conn.query( + `SELECT + r.id, + r.request_id, + r.started_at, + r.updated_at, + h.latest_heartbeat_at + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' + ORDER BY COALESCE(h.latest_heartbeat_at, r.started_at) ASC + LIMIT 1`, + ); + const oldestRunningStartedAt = oldestRunningRows[0]?.started_at == null + ? null + : Number(oldestRunningRows[0].started_at); + const oldestRunningHeartbeatAt = oldestRunningRows[0]?.latest_heartbeat_at == null + ? null + : Number(oldestRunningRows[0].latest_heartbeat_at); + const oldestRunningHeartbeatAgeMs = oldestRunningRows[0] + ? Math.max(0, now - Number(oldestRunningRows[0].latest_heartbeat_at ?? oldestRunningRows[0].started_at ?? now)) + : 0; + const [runningWithoutHeartbeatRows] = await conn.query( + `SELECT COUNT(*) AS count + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' AND h.latest_heartbeat_at IS NULL`, + ); + + const failedWindowMs = positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_FAILED_WINDOW_MS, 10 * 60 * 1000); + const since = now - failedWindowMs; + const [failedRows] = await conn.query( + `SELECT COUNT(*) AS count + FROM h5_agent_runs + WHERE status = 'failed' AND updated_at >= ?`, + [since], + ); + const [latestFailedRows] = await conn.query( + `SELECT id, request_id, error_message, updated_at + FROM h5_agent_runs + WHERE status = 'failed' + ORDER BY updated_at DESC + LIMIT 1`, + ); + + return { + statusCounts, + queuedOrRetryable: Number(statusCounts.queued ?? 0) + Number(statusCounts.retryable ?? 0), + running: Number(statusCounts.running ?? 0), + oldestPendingUpdatedAt, + oldestPendingAgeMs: oldestPendingUpdatedAt == null ? 0 : Math.max(0, now - oldestPendingUpdatedAt), + oldestRunningStartedAt, + oldestRunningAgeMs: oldestRunningStartedAt == null ? 0 : Math.max(0, now - oldestRunningStartedAt), + oldestRunningHeartbeatAt, + oldestRunningHeartbeatAgeMs, + runningWithoutHeartbeatCount: Number(runningWithoutHeartbeatRows[0]?.count ?? 0), + latestRunningRun: oldestRunningRows[0] ? { + id: oldestRunningRows[0].id, + requestId: oldestRunningRows[0].request_id, + startedAt: oldestRunningStartedAt, + updatedAt: oldestRunningRows[0].updated_at == null ? null : Number(oldestRunningRows[0].updated_at), + heartbeatAt: oldestRunningHeartbeatAt, + heartbeatAgeMs: oldestRunningHeartbeatAgeMs, + } : null, + failedWindowMs, + failedRecentCount: Number(failedRows[0]?.count ?? 0), + latestFailedRun: latestFailedRows[0] ? { + id: latestFailedRows[0].id, + requestId: latestFailedRows[0].request_id, + error: latestFailedRows[0].error_message, + updatedAt: Number(latestFailedRows[0].updated_at ?? 0), + } : null, + }; + } finally { + await conn.end(); + } +} + +function evaluateHealth(queue) { + const thresholds = { + maxRecentFailures: positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_MAX_RECENT_FAILURES, 3), + maxPendingAgeMs: positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_MAX_PENDING_AGE_MS, 5 * 60 * 1000), + maxPendingCount: positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_MAX_PENDING_COUNT, 10), + maxRunningAgeMs: positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_MAX_RUNNING_AGE_MS, 15 * 60 * 1000), + }; + const reasons = []; + if (queue.failedRecentCount >= thresholds.maxRecentFailures) { + reasons.push(`recent_failed_count ${queue.failedRecentCount} >= ${thresholds.maxRecentFailures}`); + } + if (queue.oldestPendingAgeMs >= thresholds.maxPendingAgeMs) { + reasons.push(`oldest_pending_age_ms ${queue.oldestPendingAgeMs} >= ${thresholds.maxPendingAgeMs}`); + } + if (queue.queuedOrRetryable >= thresholds.maxPendingCount) { + reasons.push(`pending_count ${queue.queuedOrRetryable} >= ${thresholds.maxPendingCount}`); + } + if (queue.oldestRunningHeartbeatAgeMs >= thresholds.maxRunningAgeMs) { + reasons.push(`oldest_running_heartbeat_age_ms ${queue.oldestRunningHeartbeatAgeMs} >= ${thresholds.maxRunningAgeMs}`); + } + return { thresholds, reasons, shouldPause: reasons.length > 0 }; +} + +async function disableCodeRuns({ root, envFile, workerLabel, portalLabel, gui, reasons, dryRun }) { + const actions = []; + const updates = { + MEMIND_AGENT_CODE_RUNS_ENABLED: '0', + MEMIND_AGENT_RUN_AUTODISPATCH: '1', + }; + if (dryRun) { + return { + applied: false, + actions: [ + `would backup ${envFile}`, + `would set ${Object.entries(updates).map(([k, v]) => `${k}=${v}`).join(', ')}`, + `would stop and disable ${workerLabel}`, + `would kickstart ${portalLabel}`, + ], + }; + } + + const backupRoot = process.env.MEMIND_AGENT_RUN_GUARD_BACKUP_DIR + || path.join(path.dirname(root), 'memind_backups', `${timestamp()}-agent-run-guard-pause`); + fs.mkdirSync(backupRoot, { recursive: true }); + const envBackup = path.join(backupRoot, '.env.before'); + fs.copyFileSync(envFile, envBackup); + actions.push(`backed_up_env:${envBackup}`); + + const raw = fs.readFileSync(envFile, 'utf8'); + const updated = replaceOrAppendEnv(raw, updates); + const marker = [ + '', + `# agent-run guard pause at ${new Date().toISOString()}`, + `# reasons: ${reasons.join('; ')}`, + ].join('\n'); + fs.writeFileSync(envFile, `${updated.replace(/\n*$/, '\n')}${marker}\n`, 'utf8'); + actions.push(`updated_env:${envFile}`); + + await runCommand('launchctl', ['bootout', `${gui}/${workerLabel}`]); + await runCommand('launchctl', ['disable', `${gui}/${workerLabel}`]); + actions.push(`disabled_worker:${workerLabel}`); + + const kick = await runCommand('launchctl', ['kickstart', '-k', `${gui}/${portalLabel}`]); + actions.push(kick.ok ? `kickstarted_portal:${portalLabel}` : `portal_kickstart_failed:${kick.message}`); + return { applied: true, backupRoot, actions }; +} + +const args = parseArgs(process.argv.slice(2)); +if (args.help) { + printHelp(); + process.exit(0); +} + +const root = path.join(path.dirname(new URL(import.meta.url).pathname), '..'); +const envFile = process.env.MEMIND_ENV_FILE || path.join(root, '.env'); +loadEnvFile(envFile); + +const enabled = process.env.MEMIND_AGENT_RUN_GUARD_ENABLED == null + ? true + : truthy(process.env.MEMIND_AGENT_RUN_GUARD_ENABLED); +const workerLabel = process.env.MEMIND_AGENT_RUN_WORKER_LABEL || DEFAULT_WORKER_LABEL; +const portalLabel = process.env.MEMIND_PORTAL_LABEL || DEFAULT_PORTAL_LABEL; +const gui = `gui/${process.getuid()}`; +const now = Date.now(); +const queue = await readQueueHealth(now).catch((err) => ({ + error: err instanceof Error ? (err.message || err.code || err.name) : String(err), +})); +const evaluation = queue.error + ? { thresholds: {}, reasons: [`queue_health_error ${queue.error}`], shouldPause: false } + : evaluateHealth(queue); +let pause = { applied: false, actions: [] }; + +if (enabled && evaluation.shouldPause) { + pause = await disableCodeRuns({ + root, + envFile, + workerLabel, + portalLabel, + gui, + reasons: evaluation.reasons, + dryRun: args.dryRun, + }); +} + +const result = { + ok: enabled ? !evaluation.shouldPause || pause.applied || args.dryRun : true, + checkedAt: new Date(now).toISOString(), + enabled, + mode: args.apply ? 'apply' : 'dry-run', + queue, + evaluation, + pause, +}; + +console.log(JSON.stringify(result, null, 2)); +process.exit(result.ok ? 0 : 1); diff --git a/.runtime/portal/scripts/agent-run-worker.mjs b/.runtime/portal/scripts/agent-run-worker.mjs index 1c16130..dde6e05 100755 --- a/.runtime/portal/scripts/agent-run-worker.mjs +++ b/.runtime/portal/scripts/agent-run-worker.mjs @@ -2,18 +2,22 @@ import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url); // scripts/agent-run-worker.mjs -import fs7 from "node:fs"; -import path10 from "node:path"; +import fs9 from "node:fs"; +import path12 from "node:path"; import { fileURLToPath as fileURLToPath5 } from "node:url"; // agent-run-gateway.mjs import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; var DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5e3, 15e3]; var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed"]); var CODE_TOOL_MODES = /* @__PURE__ */ new Set(["code", "code-task", "code_task", "code-tool", "code_tool", "code_tool_task"]); var RUN_METADATA_KEY = "memindRun"; var DEFAULT_MAX_CONCURRENT_RUNS = 1; var DEFAULT_RUN_TIMEOUT_MS = 15 * 60 * 1e3; +var DEFAULT_RUN_HEARTBEAT_MS = 30 * 1e3; +var TOOL_GATEWAY_SUMMARY_LIMIT = 4096; function nowMs() { return Date.now(); } @@ -52,6 +56,98 @@ function normalizeTaskType(value) { const normalized = String(value ?? "").trim(); return normalized || null; } +function summarizeText(value, limit = TOOL_GATEWAY_SUMMARY_LIMIT) { + const text = String(value ?? ""); + if (text.length <= limit) return text; + return text.slice(text.length - limit); +} +function normalizeExpectedFileCheck(value) { + if (typeof value === "string") { + const expectedPath2 = value.trim(); + return expectedPath2 ? { path: expectedPath2 } : null; + } + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const expectedPath = String(value.path ?? value.file ?? value.relativePath ?? "").trim(); + if (!expectedPath) return null; + const contains = value.contains ?? value.expectedContent ?? value.contentIncludes; + return { + path: expectedPath, + ...contains == null ? {} : { contains: String(contains) } + }; +} +function normalizeToolGatewayValidation(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const checks = []; + const single = normalizeExpectedFileCheck(value.expectedFile ?? value.expectedPath); + if (single) checks.push(single); + if (Array.isArray(value.expectedFiles)) { + for (const item of value.expectedFiles) { + const check = normalizeExpectedFileCheck(item); + if (check) checks.push(check); + } + } + return checks.length > 0 ? { expectedFiles: checks } : null; +} +function resolveValidationPath(cwd, expectedPath) { + if (!String(cwd ?? "").trim()) { + const err = new Error("Tool Gateway validation failed: missing working directory"); + err.code = "TOOL_GATEWAY_VALIDATION_FAILED"; + err.retryable = false; + throw err; + } + const root2 = path.resolve(String(cwd ?? "")); + const target = path.resolve(root2, String(expectedPath ?? "")); + if (target !== root2 && !target.startsWith(`${root2}${path.sep}`)) { + const err = new Error(`Tool Gateway validation path escapes working directory: ${expectedPath}`); + err.code = "TOOL_GATEWAY_VALIDATION_FAILED"; + err.retryable = false; + throw err; + } + return { root: root2, target }; +} +async function validateToolGatewayResult({ result, validation, cwd }) { + if (!validation?.expectedFiles?.length || result?.dryRun) { + return null; + } + const checks = []; + for (const expected of validation.expectedFiles) { + const { root: root2, target } = resolveValidationPath(cwd ?? result?.cwd, expected.path); + let content = ""; + let stat = null; + try { + stat = await fs.stat(target); + content = await fs.readFile(target, "utf8"); + } catch (cause) { + const err = new Error(`Tool Gateway validation failed: expected file not found: ${expected.path}`); + err.code = "TOOL_GATEWAY_VALIDATION_FAILED"; + err.retryable = false; + err.validation = { + expectedFile: expected.path, + cwd: root2, + reason: "missing_file" + }; + err.cause = cause; + throw err; + } + if (expected.contains != null && !content.includes(expected.contains)) { + const err = new Error(`Tool Gateway validation failed: expected content not found in ${expected.path}`); + err.code = "TOOL_GATEWAY_VALIDATION_FAILED"; + err.retryable = false; + err.validation = { + expectedFile: expected.path, + cwd: root2, + reason: "missing_content" + }; + throw err; + } + checks.push({ + path: expected.path, + sizeBytes: Number(stat?.size ?? 0), + contains: expected.contains == null ? null : true + }); + } + return { expectedFiles: checks }; +} function withRunMetadata(userMessage, { toolMode = "chat", taskType = null } = {}) { const message = userMessage && typeof userMessage === "object" && !Array.isArray(userMessage) ? { ...userMessage } : { value: userMessage }; const metadata = message.metadata && typeof message.metadata === "object" && !Array.isArray(message.metadata) ? { ...message.metadata } : {}; @@ -73,7 +169,8 @@ function getRunOptionsFromMessage(userMessage) { } return { toolMode, - taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType) + taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType), + validation: normalizeToolGatewayValidation(runMetadata?.validation ?? metadata?.toolGatewayValidation) }; } function projectRun(row) { @@ -96,6 +193,7 @@ function createAgentRunGateway({ pool: pool2, userAuth: userAuth2, tkmindProxy: tkmindProxy2, + toolGateway: toolGateway2 = null, retryDelaysMs = DEFAULT_RUN_RETRY_DELAYS_MS, autoDispatch = envFlag(process.env.MEMIND_AGENT_RUN_AUTODISPATCH, true), maxConcurrentRuns = positiveInteger( @@ -105,6 +203,10 @@ function createAgentRunGateway({ runTimeoutMs = positiveInteger( process.env.MEMIND_AGENT_RUN_TIMEOUT_MS, DEFAULT_RUN_TIMEOUT_MS + ), + heartbeatMs = positiveInteger( + process.env.MEMIND_AGENT_RUN_HEARTBEAT_MS, + DEFAULT_RUN_HEARTBEAT_MS ) }) { const inFlight = /* @__PURE__ */ new Set(); @@ -216,6 +318,30 @@ function createAgentRunGateway({ ); await appendEvent(runId, status, fields); } + function startRunHeartbeat(runId, { attempt }) { + let stopped = false; + const writeHeartbeat = async () => { + if (stopped) return; + try { + await appendEvent(runId, "worker_heartbeat", { + attempt, + pid: process.pid, + heartbeatMs + }); + } catch (err) { + console.error("[AgentRun] worker heartbeat failed:", err instanceof Error ? err.message : err); + } + }; + void writeHeartbeat(); + const timer = setInterval(() => { + void writeHeartbeat(); + }, heartbeatMs); + timer.unref?.(); + return () => { + stopped = true; + clearInterval(timer); + }; + } async function runWithTimeout(runId, task) { let timer = null; const timeout = new Promise((_, reject) => { @@ -230,6 +356,49 @@ function createAgentRunGateway({ async function executeRun(row, runId) { const userMessage = safeJsonParse(row.user_message_json, {}); const runOptions = getRunOptionsFromMessage(userMessage); + const toolGatewayStatus = toolGateway2?.getStatus ? toolGateway2.getStatus() : null; + if (runOptions.toolMode === "code" && toolGatewayStatus?.enabled) { + const workingDir = userAuth2?.resolveWorkingDir ? await userAuth2.resolveWorkingDir(row.user_id) : void 0; + await appendEvent(runId, "tool_gateway_dispatch", { + protocol: toolGatewayStatus.protocol ?? "agent-run-v1", + taskType: runOptions.taskType, + workingDir: workingDir ?? null + }); + const result = await toolGateway2.executeJob({ + runId, + userId: row.user_id, + requestId: row.request_id, + userMessage, + taskType: runOptions.taskType, + cwd: workingDir, + timeoutMs: runTimeoutMs + }); + await appendEvent(runId, "tool_gateway_result", { + executor: result.executor ?? null, + dryRun: Boolean(result.dryRun), + exitCode: result.exitCode ?? null, + stdoutTail: summarizeText(result.stdout), + stderrTail: summarizeText(result.stderr) + }); + try { + const validation = await validateToolGatewayResult({ + result, + validation: runOptions.validation, + cwd: workingDir + }); + if (validation) { + await appendEvent(runId, "tool_gateway_validation", validation); + } + } catch (err) { + await appendEvent(runId, "tool_gateway_validation_failed", { + code: err?.code ?? null, + message: err instanceof Error ? err.message : String(err), + validation: err?.validation ?? null + }); + throw err; + } + return { sessionId: row.agent_session_id ?? null }; + } let sessionId = row.agent_session_id ?? null; if (!sessionId) { const sessionOptions = {}; @@ -269,6 +438,7 @@ function createAgentRunGateway({ ); if (Number(claim?.affectedRows ?? 0) === 0) return; await appendEvent(runId, "running", { attempt: nextAttempt }); + const stopHeartbeat = startRunHeartbeat(runId, { attempt: nextAttempt }); try { const { sessionId } = await runWithTimeout(runId, () => executeRun(row, runId)); await markRun(runId, "succeeded", { @@ -281,7 +451,7 @@ function createAgentRunGateway({ if (timedOut) { await appendEvent(runId, "timeout", { timeoutMs: runTimeoutMs }); } - const retryable = !timedOut && nextAttempt < retryDelaysMs.length; + const retryable = !timedOut && err?.retryable !== false && nextAttempt < retryDelaysMs.length; await markRun(runId, retryable ? "retryable" : "failed", { error_message: message, completed_at: retryable ? null : nowMs() @@ -289,6 +459,8 @@ function createAgentRunGateway({ if (retryable && autoDispatch) { setTimeout(() => dispatchRun(runId), retryDelaysMs[nextAttempt - 1]); } + } finally { + stopHeartbeat(); } } function drainQueue() { @@ -317,23 +489,166 @@ function createAgentRunGateway({ for (const row of rows) { statusCounts[row.status] = Number(row.count ?? 0); } + const [oldestRunningRows] = await pool2.query( + `SELECT + r.id, + r.request_id, + r.started_at, + r.updated_at, + r.attempts, + h.latest_heartbeat_at + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' + ORDER BY COALESCE(h.latest_heartbeat_at, r.started_at) ASC + LIMIT 1` + ); + const oldestRunningStartedAt = oldestRunningRows[0]?.started_at == null ? null : Number(oldestRunningRows[0].started_at); + const oldestRunningAgeMs = oldestRunningStartedAt == null ? 0 : Math.max(0, nowMs() - oldestRunningStartedAt); + const oldestRunningHeartbeatAt = oldestRunningRows[0]?.latest_heartbeat_at == null ? null : Number(oldestRunningRows[0].latest_heartbeat_at); + const oldestRunningHeartbeatAgeMs = oldestRunningRows[0] ? Math.max(0, nowMs() - Number(oldestRunningRows[0].latest_heartbeat_at ?? oldestRunningRows[0].started_at ?? nowMs())) : 0; + const [runningWithoutHeartbeatRows] = await pool2.query( + `SELECT COUNT(*) AS count + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' AND h.latest_heartbeat_at IS NULL` + ); return { autoDispatch, maxConcurrentRuns, runTimeoutMs, + heartbeatMs, inFlight: inFlight.size, pendingDispatches: queuedDispatches.length, statusCounts, - terminalStatuses: [...TERMINAL_STATUSES] + oldestRunningStartedAt, + oldestRunningAgeMs, + oldestRunningHeartbeatAt, + oldestRunningHeartbeatAgeMs, + runningWithoutHeartbeatCount: Number(runningWithoutHeartbeatRows[0]?.count ?? 0), + latestRunningRun: oldestRunningRows[0] ? { + id: oldestRunningRows[0].id, + requestId: oldestRunningRows[0].request_id, + startedAt: oldestRunningStartedAt, + updatedAt: oldestRunningRows[0].updated_at == null ? null : Number(oldestRunningRows[0].updated_at), + attempts: Number(oldestRunningRows[0].attempts ?? 0), + heartbeatAt: oldestRunningHeartbeatAt, + heartbeatAgeMs: oldestRunningHeartbeatAgeMs + } : null, + terminalStatuses: [...TERMINAL_STATUSES], + toolGateway: toolGateway2?.getStatus ? toolGateway2.getStatus() : null + }; + } + async function recoverStaleRunningRuns({ + staleMs = runTimeoutMs, + limit = maxConcurrentRuns, + dryRun = true, + reason = "stale_running_timeout" + } = {}) { + const normalizedStaleMs = positiveInteger(staleMs, runTimeoutMs); + const normalizedLimit = positiveInteger(limit, maxConcurrentRuns); + const cutoff = nowMs() - normalizedStaleMs; + const [rows] = await pool2.query( + `SELECT + r.id, + r.request_id, + r.started_at, + r.updated_at, + r.attempts, + h.latest_heartbeat_at + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' + AND r.started_at IS NOT NULL + AND COALESCE(h.latest_heartbeat_at, r.started_at) <= ? + ORDER BY COALESCE(h.latest_heartbeat_at, r.started_at) ASC + LIMIT ?`, + [cutoff, normalizedLimit] + ); + const recovered = []; + for (const row of rows) { + const ageMs = Math.max(0, nowMs() - Number(row.started_at ?? 0)); + const heartbeatAt = row.latest_heartbeat_at == null ? null : Number(row.latest_heartbeat_at); + const heartbeatAgeMs = Math.max(0, nowMs() - Number(row.latest_heartbeat_at ?? row.started_at ?? 0)); + const item = { + id: row.id, + requestId: row.request_id, + startedAt: Number(row.started_at ?? 0), + updatedAt: Number(row.updated_at ?? 0), + attempts: Number(row.attempts ?? 0), + heartbeatAt, + heartbeatAgeMs, + ageMs, + reason + }; + if (!dryRun) { + const message = heartbeatAt == null ? `agent run recovered from stale running state after ${ageMs}ms without heartbeat` : `agent run recovered from stale running state after heartbeat was stale for ${heartbeatAgeMs}ms`; + const completedAt = nowMs(); + const [update] = await pool2.query( + `UPDATE h5_agent_runs + SET status = 'failed', error_message = ?, updated_at = ?, completed_at = ? + WHERE id = ? + AND status = 'running' + AND started_at IS NOT NULL + AND COALESCE( + (SELECT MAX(created_at) + FROM h5_agent_run_events + WHERE run_id = h5_agent_runs.id AND event_type = 'worker_heartbeat'), + started_at + ) <= ?`, + [message, completedAt, completedAt, row.id, cutoff] + ); + if (Number(update?.affectedRows ?? 0) === 0) continue; + await appendEvent(row.id, "stale_recovered", { + reason, + staleMs: normalizedStaleMs, + ageMs, + heartbeatAt, + heartbeatAgeMs, + status: "failed", + error: message + }); + } + recovered.push(item); + } + return { + dryRun, + staleMs: normalizedStaleMs, + cutoff, + considered: rows.length, + recovered: dryRun ? 0 : recovered.length, + runs: recovered }; } async function dispatchQueuedRuns({ limit = maxConcurrentRuns } = {}) { + const staleRecovery = await recoverStaleRunningRuns({ + staleMs: runTimeoutMs, + limit: Math.max(maxConcurrentRuns, Number(limit) || maxConcurrentRuns), + dryRun: false, + reason: "worker_dispatch_stale_recovery" + }); const dispatchLimit = Math.max(1, Number(limit) || maxConcurrentRuns); const available = Math.max(0, maxConcurrentRuns - inFlight.size - queuedDispatches.length); if (available <= 0) { return { considered: 0, dispatched: 0, + staleRecovery, queue: await getQueueStatus() }; } @@ -352,6 +667,7 @@ function createAgentRunGateway({ return { considered: rows.length, dispatched: rows.length, + staleRecovery, queue: await getQueueStatus() }; } @@ -360,13 +676,14 @@ function createAgentRunGateway({ getRunForUser, dispatchRun, getQueueStatus, - dispatchQueuedRuns + dispatchQueuedRuns, + recoverStaleRunningRuns }; } // db.mjs import mysql from "mysql2/promise"; -import path from "node:path"; +import path2 from "node:path"; import { fileURLToPath } from "node:url"; // mindspace.mjs @@ -462,7 +779,7 @@ async function initializeDefaultSpace(db, userId, { } // db.mjs -var __dirname = path.dirname(fileURLToPath(import.meta.url)); +var __dirname = path2.dirname(fileURLToPath(import.meta.url)); function isDatabaseConfigured() { return Boolean( process.env.DATABASE_URL || process.env.MYSQL_HOST && process.env.MYSQL_DATABASE @@ -491,12 +808,1823 @@ function createDbPool() { }); } +// llm-providers.mjs +import crypto3 from "node:crypto"; +import fs2 from "node:fs"; +import os from "node:os"; +import path3 from "node:path"; +import { spawn, spawnSync } from "node:child_process"; +import { Agent, fetch as undiciFetch } from "undici"; +var CUSTOM_PROVIDER_ID = "__custom__"; +var LLM_PROVIDER_CATALOG = [ + { + id: CUSTOM_PROVIDER_ID, + label: "\u81EA\u5B9A\u4E49 OpenAI \u517C\u5BB9", + kind: "custom", + apiKeyEnv: null, + defaultModel: "", + models: [] + }, + { + id: "custom_deepseek", + label: "DeepSeek", + kind: "builtin", + apiKeyEnv: "DEEPSEEK_API_KEY", + defaultModel: "deepseek-chat", + models: ["deepseek-chat", "deepseek-reasoner"] + }, + { + id: "openai", + label: "OpenAI", + kind: "builtin", + apiKeyEnv: "OPENAI_API_KEY", + defaultModel: "gpt-4o", + models: ["gpt-4o", "gpt-4o-mini"] + }, + { + id: "openrouter", + label: "OpenRouter", + kind: "builtin", + apiKeyEnv: "OPENROUTER_API_KEY", + defaultModel: "anthropic/claude-sonnet-4", + models: ["anthropic/claude-sonnet-4", "openai/gpt-4o"] + }, + { + id: "anthropic", + label: "Anthropic", + kind: "builtin", + apiKeyEnv: "ANTHROPIC_API_KEY", + defaultModel: "claude-sonnet-4-20250514", + models: ["claude-sonnet-4-20250514", "claude-3-5-haiku-20241022"] + }, + { + id: "custom_qwen", + label: "Qwen VL (\u901A\u4E49\u5343\u95EE)", + kind: "builtin", + apiKeyEnv: null, + // stored as custom provider; apiUrl is injected at createKey time + apiUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", + defaultModel: "qwen-vl-max", + models: ["qwen-vl-max", "qwen-vl-max-latest", "qwen-vl-plus", "qwen-plus", "qwen-turbo"] + } +]; +var LLM_EXECUTOR_CATALOG = [ + { + id: "goose", + label: "Goose", + description: "\u7B56\u7565\u5224\u65AD\u3001\u5206\u6790\u3001\u603B\u7ED3\u548C\u8F7B\u91CF\u4EFB\u52A1\u7F16\u6392", + purposes: ["default"] + }, + { + id: "aider", + label: "Aider", + description: "\u5C0F\u8303\u56F4\u4EE3\u7801\u4FEE\u6539\u548C\u8865\u4E01\u5F0F\u4FEE\u590D", + purposes: ["default"] + }, + { + id: "openhands", + label: "OpenHands", + description: "\u590D\u6742\u4ED3\u5E93\u4EFB\u52A1\u3001\u591A\u6587\u4EF6\u6539\u9020\u548C\u547D\u4EE4\u6267\u884C", + purposes: ["default"] + } +]; +var catalogById = Object.fromEntries(LLM_PROVIDER_CATALOG.map((item) => [item.id, item])); +var executorById = Object.fromEntries(LLM_EXECUTOR_CATALOG.map((item) => [item.id, item])); +var BUILTIN_PROVIDER_TEST_URLS = { + custom_deepseek: process.env.DEEPSEEK_API_BASE_URL ?? "https://api.deepseek.com/v1", + openai: process.env.OPENAI_API_BASE_URL ?? "https://api.openai.com/v1", + openrouter: process.env.OPENROUTER_API_BASE_URL ?? "https://openrouter.ai/api/v1" +}; +var insecureDispatcher = new Agent({ + connect: { rejectUnauthorized: false } +}); +function resolveEncryptionKey(explicitKey) { + const raw = explicitKey ?? process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY ?? "local-dev-secret"; + return crypto3.createHash("sha256").update(raw).digest(); +} +function encryptSecret(plaintext, encryptionKey) { + const key = resolveEncryptionKey(encryptionKey); + const iv = crypto3.randomBytes(12); + const cipher = crypto3.createCipheriv("aes-256-gcm", key, iv); + const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); + return { + ciphertext: encrypted.toString("base64"), + iv: iv.toString("base64"), + tag: cipher.getAuthTag().toString("base64") + }; +} +function decryptSecret({ ciphertext, iv, tag }, encryptionKey) { + const key = resolveEncryptionKey(encryptionKey); + const decipher = crypto3.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64")); + decipher.setAuthTag(Buffer.from(tag, "base64")); + const plain = Buffer.concat([ + decipher.update(Buffer.from(ciphertext, "base64")), + decipher.final() + ]); + return plain.toString("utf8"); +} +function maskApiKey(apiKey) { + if (!apiKey) return ""; + if (apiKey.length <= 8) return "*".repeat(apiKey.length); + const head = apiKey.slice(0, 4); + const tail = apiKey.slice(-4); + return `${head}${"*".repeat(Math.max(apiKey.length - 8, 4))}${tail}`; +} +function parseModelList(raw) { + if (Array.isArray(raw)) { + return [...new Set(raw.map((item) => String(item).trim()).filter(Boolean))]; + } + return [ + ...new Set( + String(raw ?? "").split(/[\n,]/).map((item) => item.trim()).filter(Boolean) + ) + ]; +} +function normalizeApiUrl(raw) { + const trimmed = String(raw ?? "").trim(); + if (!trimmed) return ""; + if (/^https?:\/\//i.test(trimmed)) return trimmed.replace(/\/+$/, ""); + return `http://${trimmed.replace(/\/+$/, "")}`; +} +function resolveApiBaseUrl(apiUrl) { + const normalized = normalizeApiUrl(apiUrl); + if (!normalized) return ""; + if (normalized.endsWith("/chat/completions")) { + return normalized.slice(0, -"/chat/completions".length).replace(/\/+$/, ""); + } + return normalized.replace(/\/+$/, ""); +} +function resolveChatCompletionsUrl(apiUrl) { + const normalized = normalizeApiUrl(apiUrl); + if (!normalized) return ""; + if (normalized.endsWith("/chat/completions")) return normalized; + if (normalized.endsWith("/v1")) return `${normalized}/chat/completions`; + return `${normalized}/chat/completions`; +} +var RELAY_BOOTSTRAP = { + name: process.env.H5_RELAY_BOOTSTRAP_NAME ?? "Relay Buyer Ollama", + apiUrl: process.env.H5_RELAY_BOOTSTRAP_URL ?? "http://127.0.0.1:18300/relay/buyer/v1/chat/completions", + apiKey: process.env.H5_RELAY_BOOTSTRAP_API_KEY ?? "UqyHPKSSEZq0-oPnl8sru-7hZcJ2anPUL1yAVk866Vo", + models: parseModelList(process.env.H5_RELAY_BOOTSTRAP_MODELS ?? "qwen2.5:3b"), + defaultModel: process.env.H5_RELAY_BOOTSTRAP_MODEL ?? "qwen2.5:3b", + relayProvider: process.env.H5_RELAY_BOOTSTRAP_PROVIDER ?? "ollama" +}; +var LOCAL_LLM_FALLBACK = { + enabled: process.env.H5_LOCAL_LLM_FALLBACK !== "0", + providerId: process.env.H5_LOCAL_LLM_PROVIDER_ID ?? "custom_local_ollama_7b", + displayName: process.env.H5_LOCAL_LLM_NAME ?? "Local Ollama 7B", + apiUrl: process.env.H5_LOCAL_LLM_URL ?? "http://127.0.0.1:11434/v1/chat/completions", + apiKey: process.env.H5_LOCAL_LLM_API_KEY ?? "ollama", + model: process.env.H5_LOCAL_LLM_MODEL ?? "qwen2.5:3b" +}; +var cachedLocalFallbackProviderId = null; +async function testRelayConnection({ apiUrl, apiKey, model, relayProvider }, fetchImpl = undiciFetch) { + const url = resolveChatCompletionsUrl(apiUrl); + if (!url) return { ok: false, message: "API \u5730\u5740\u65E0\u6548" }; + if (!apiKey) return { ok: false, message: "\u7F3A\u5C11 API Key" }; + if (!model) return { ok: false, message: "\u7F3A\u5C11\u6A21\u578B" }; + const started = Date.now(); + const body = { + model, + messages: [{ role: "user", content: "Hello" }], + stream: false, + ...relayProvider ? { provider: relayProvider } : {} + }; + const upstream = await fetchImpl(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify(body), + dispatcher: url.startsWith("https://") ? insecureDispatcher : void 0 + }); + const latencyMs = Date.now() - started; + const text = await upstream.text().catch(() => ""); + if (!upstream.ok) { + let detail = text.slice(0, 500) || "(\u7A7A\u54CD\u5E94)"; + if (detail === "{}") { + detail = "\u7A7A JSON \u54CD\u5E94"; + } + const statusHint = upstream.status === 401 ? "Bearer Token \u65E0\u6548\u6216\u5DF2\u8FC7\u671F" : upstream.status === 404 ? "\u5730\u5740\u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5 API URL" : upstream.status === 400 ? "\u8BF7\u6C42\u53C2\u6570\u88AB\u62D2\u7EDD\uFF0C\u8BF7\u68C0\u67E5 model / provider" : `HTTP ${upstream.status}`; + return { + ok: false, + latencyMs, + message: `Relay ${upstream.status} ${statusHint}\uFF1A${detail}` + }; + } + let data; + try { + data = JSON.parse(text); + } catch { + return { ok: false, latencyMs, message: "\u54CD\u5E94\u4E0D\u662F JSON" }; + } + const reply = data?.choices?.[0]?.message?.content ?? data?.message?.content ?? data?.output ?? null; + return { + ok: true, + latencyMs, + model, + reply: reply ? String(reply).slice(0, 300) : "(\u8054\u901A\u6210\u529F\uFF0C\u65E0\u6587\u672C\u5185\u5BB9)" + }; +} +async function testLocalLlmConnection(fetchImpl = undiciFetch) { + if (!LOCAL_LLM_FALLBACK.enabled) { + return { ok: false, message: "\u672C\u5730 LLM fallback \u5DF2\u7981\u7528" }; + } + return testRelayConnection( + { + apiUrl: LOCAL_LLM_FALLBACK.apiUrl, + apiKey: LOCAL_LLM_FALLBACK.apiKey, + model: LOCAL_LLM_FALLBACK.model, + relayProvider: null + }, + fetchImpl + ); +} +function parseModelsJson(raw) { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parseModelList(parsed) : []; + } catch { + return []; + } +} +function rowToPublic(row, catalogItem, apiKeyForMask) { + const providerKind = row.provider_kind ?? "builtin"; + const models = providerKind === "custom" ? parseModelsJson(row.models_json) : catalogItem?.models ?? []; + return { + id: row.id, + providerId: row.provider_id, + providerKind, + providerLabel: providerKind === "custom" ? row.name : catalogItem?.label ?? row.provider_id, + name: row.name, + defaultModel: row.default_model, + models, + apiUrl: row.api_url ?? null, + basePath: row.base_path ?? null, + engine: row.engine ?? "openai", + relayProvider: row.relay_provider ?? null, + goosedProviderId: row.goosed_provider_id ?? null, + status: row.status, + isSelected: Boolean(row.is_selected), + isVisionSelected: Boolean(row.is_vision_selected), + apiKeyMasked: maskApiKey(apiKeyForMask), + createdAt: Number(row.created_at), + updatedAt: Number(row.updated_at) + }; +} +function profileFromRow(row, decryptRow) { + const providerKind = row.provider_kind ?? "builtin"; + const models = providerKind === "custom" ? parseModelsJson(row.models_json) : [row.default_model]; + return { + providerKind, + providerId: row.provider_id, + goosedProviderId: row.goosed_provider_id ?? null, + name: row.name, + defaultModel: row.default_model, + apiKey: decryptRow(row), + apiUrl: row.api_url ?? null, + basePath: row.base_path ?? null, + engine: row.engine ?? "openai", + relayProvider: row.relay_provider ?? null, + models + }; +} +function normalizeExecutor(raw) { + const executor = String(raw ?? "").trim().toLowerCase(); + return executorById[executor] ? executor : null; +} +function normalizePurpose(raw) { + return String(raw ?? "default").trim() || "default"; +} +function rowToExecutorBinding(row, keyPublic = null) { + const executor = normalizeExecutor(row.executor); + const meta = executorById[executor] ?? null; + return { + id: row.id, + executor, + executorLabel: meta?.label ?? row.executor, + executorDescription: meta?.description ?? "", + purpose: row.purpose ?? "default", + providerKeyId: row.provider_key_id ?? null, + providerName: keyPublic?.name ?? null, + providerLabel: keyPublic?.providerLabel ?? null, + providerId: keyPublic?.providerId ?? null, + model: row.model ?? "", + enabled: Boolean(row.enabled), + availableModels: keyPublic?.models ?? [], + createdAt: row.created_at ? Number(row.created_at) : null, + updatedAt: row.updated_at ? Number(row.updated_at) : null + }; +} +function executorEnvForProfile(executor, profile, model, { includeSecret = false } = {}) { + const apiKeyValue = includeSecret ? profile.apiKey : "[hidden]"; + const providerId = profile.providerId; + const resolvedApiUrl = profile.apiUrl || BUILTIN_PROVIDER_TEST_URLS[providerId] || ""; + const baseUrl = resolveApiBaseUrl(resolvedApiUrl); + const common = { + TKMIND_EXECUTOR: executor, + TKMIND_EXECUTOR_PROVIDER: providerId, + TKMIND_EXECUTOR_MODEL: model + }; + if (baseUrl) common.TKMIND_EXECUTOR_API_BASE = baseUrl; + if (includeSecret) common.TKMIND_EXECUTOR_API_KEY = profile.apiKey; + if (executor === "aider") { + const aiderModel = aiderModelNameForProvider(providerId, model); + const env = { + ...common, + AIDER_MODEL: aiderModel + }; + if (providerId === "anthropic") { + env.AIDER_ANTHROPIC_API_KEY = apiKeyValue; + env.ANTHROPIC_API_KEY = apiKeyValue; + } else { + env.AIDER_OPENAI_API_KEY = apiKeyValue; + env.OPENAI_API_KEY = apiKeyValue; + if (providerId === "openrouter") env.OPENROUTER_API_KEY = apiKeyValue; + if (providerId === "custom_deepseek") env.DEEPSEEK_API_KEY = apiKeyValue; + } + if (baseUrl) { + env.AIDER_OPENAI_API_BASE = baseUrl; + env.OPENAI_API_BASE = baseUrl; + } + return env; + } + if (executor === "openhands") { + return { + ...common, + LLM_MODEL: model, + LLM_API_KEY: apiKeyValue, + ...baseUrl ? { LLM_BASE_URL: baseUrl } : {} + }; + } + return { + ...common, + GOOSE_PROVIDER: profile.providerKind === "custom" ? profile.goosedProviderId ?? profile.providerId : profile.providerId, + GOOSE_MODEL: model, + TKMIND_PROVIDER: profile.providerKind === "custom" ? profile.goosedProviderId ?? profile.providerId : profile.providerId, + TKMIND_MODEL: model + }; +} +function aiderModelNameForProvider(providerId, model) { + const normalizedModel = String(model ?? "").trim(); + if (!normalizedModel) return normalizedModel; + if (String(providerId ?? "").trim() === "custom_deepseek") { + return normalizedModel.startsWith("deepseek/") ? normalizedModel : `deepseek/${normalizedModel}`; + } + return normalizedModel; +} +function launchLogDir() { + return path3.join(os.tmpdir(), "memindadm-launches"); +} +function normalizeLaunchMode(mode) { + const normalized = String(mode ?? "headless").trim().toLowerCase(); + return normalized === "serve" ? "serve" : "headless"; +} +function resolveExecutorCommand(executor) { + const candidates = executor === "aider" ? [process.env.AIDER_BIN, process.env.GOOSE_AIDER_BIN, "aider"] : executor === "openhands" ? [process.env.OPENHANDS_BIN, process.env.GOOSE_OPENHANDS_BIN, "openhands"] : [executor]; + return candidates.map((item) => String(item ?? "").trim()).find(Boolean) ?? executor; +} +function commandExists(command) { + if (path3.isAbsolute(command)) { + try { + fs2.accessSync(command, fs2.constants.X_OK); + return true; + } catch { + return false; + } + } + const probe = spawnSync("sh", ["-lc", `command -v ${JSON.stringify(command).slice(1, -1)}`], { + stdio: "ignore" + }); + return probe.status === 0; +} +function spawnDetachedExecutor(plan, { logDir = launchLogDir() } = {}) { + if (!plan?.ok) { + return plan; + } + if (!plan.command) { + return { + ok: false, + executor: plan.executor ?? null, + message: "\u542F\u52A8\u547D\u4EE4\u7F3A\u5931" + }; + } + if (!commandExists(plan.command)) { + return { + ok: false, + executor: plan.executor ?? null, + message: `${plan.command} \u672A\u5B89\u88C5\u6216\u4E0D\u5728 PATH \u4E2D` + }; + } + fs2.mkdirSync(logDir, { recursive: true }); + const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); + const logFile = path3.join( + logDir, + `${plan.executor ?? "executor"}-${stamp}-${crypto3.randomUUID().slice(0, 8)}.log` + ); + const fd = fs2.openSync(logFile, "a"); + try { + const child = spawn(plan.command, plan.args ?? [], { + cwd: plan.cwd ?? process.cwd(), + env: { + ...process.env, + ...plan.env ?? {} + }, + detached: true, + stdio: ["ignore", fd, fd] + }); + child.unref(); + return { + ok: true, + executor: plan.executor ?? null, + pid: child.pid ?? null, + command: plan.command, + args: plan.args ?? [], + cwd: plan.cwd ?? process.cwd(), + logFile, + notes: plan.notes ?? [] + }; + } catch (err) { + return { + ok: false, + executor: plan.executor ?? null, + message: err instanceof Error ? err.message : "\u542F\u52A8\u5931\u8D25", + logFile + }; + } finally { + try { + fs2.closeSync(fd); + } catch { + } + } +} +function isProcessAlive(pid) { + if (!pid || Number.isNaN(Number(pid))) return false; + try { + process.kill(Number(pid), 0); + return true; + } catch (err) { + if (err && typeof err === "object" && "code" in err && err.code === "EPERM") return true; + return false; + } +} +function launchStateKey(executor, purpose = "default") { + return `${normalizeExecutor(executor)}:${normalizePurpose(purpose)}`; +} +function buildExecutorLaunchPlan(runtime, options = {}) { + if (!runtime?.ok) { + return { + ok: false, + executor: runtime?.executor ?? null, + message: runtime?.message ?? "\u8FD0\u884C\u914D\u7F6E\u672A\u5C31\u7EEA" + }; + } + const mode = normalizeLaunchMode(options.mode ?? "headless"); + const cwd = String(options.cwd ?? process.cwd()); + const instruction = String(options.instruction ?? "").trim(); + const env = runtime.env ?? {}; + if (runtime.executor === "goose") { + return { + ok: false, + executor: "goose", + message: "Goose \u4F5C\u4E3A\u73B0\u6709\u670D\u52A1\u5165\u53E3\uFF0C\u4E0D\u901A\u8FC7\u672C\u65B9\u6CD5\u542F\u52A8" + }; + } + if (runtime.executor === "aider") { + const hasInstruction = Boolean(instruction); + const command = resolveExecutorCommand("aider"); + const model = aiderModelNameForProvider(env.TKMIND_EXECUTOR_PROVIDER, runtime.model); + return { + ok: true, + executor: "aider", + cwd, + command, + args: [ + "--model", + model, + ...hasInstruction ? ["--message", instruction] : [], + "--yes-always" + ], + env, + notes: [ + "Aider \u901A\u8FC7\u7EDF\u4E00\u6A21\u578B\u4E2D\u5FC3\u63D0\u4F9B\u7684 API Key / Base URL \u8FD0\u884C\u3002", + "\u5B9E\u9645\u542F\u52A8\u65F6\u5EFA\u8BAE\u5728\u4ED3\u5E93\u76EE\u5F55\u4E2D\u6267\u884C\u3002" + ] + }; + } + if (runtime.executor === "openhands") { + if (mode === "headless" && !instruction) { + return { + ok: false, + executor: "openhands", + message: "OpenHands headless \u6A21\u5F0F\u9700\u8981 instruction" + }; + } + const command = resolveExecutorCommand("openhands"); + const args2 = mode === "serve" ? ["serve", "--mount-cwd"] : ["--headless", "--json", "--override-with-envs", "--task", instruction]; + return { + ok: true, + executor: "openhands", + cwd, + command, + args: args2, + env, + notes: [ + mode === "serve" ? "OpenHands GUI Server \u4F9D\u8D56 Docker\uFF0C\u9002\u5408\u4EBA\u5DE5\u4EA4\u4E92\u8C03\u8BD5\u3002" : "Headless \u6A21\u5F0F\u9002\u5408\u540E\u7EED\u548C\u540E\u53F0\u8C03\u5EA6\u5BF9\u63A5\u3002" + ] + }; + } + return { + ok: false, + executor: runtime.executor, + message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" + }; +} +async function goosedApiFetch(apiTarget, apiSecret, pathname, init = {}, fetchImpl = undiciFetch) { + const url = new URL(pathname, apiTarget); + const headers = { + ...init.headers ?? {}, + "X-Secret-Key": apiSecret + }; + if (init.body && !headers["Content-Type"]) { + headers["Content-Type"] = "application/json"; + } + const dispatcher = apiTarget.startsWith("https://") ? insecureDispatcher : void 0; + return fetchImpl(url, { ...init, headers, dispatcher }); +} +async function writeGoosedConfig(apiTarget, apiSecret, key, value, isSecret, fetchImpl) { + const upstream = await goosedApiFetch( + apiTarget, + apiSecret, + "/config/upsert", + { + method: "POST", + body: JSON.stringify({ key, value, is_secret: isSecret }) + }, + fetchImpl + ); + if (upstream.ok) return { ok: true, skipped: false }; + if (upstream.status === 404) return { ok: false, skipped: true }; + const text = await upstream.text().catch(() => ""); + throw new Error(`\u540C\u6B65 TKMind Agent \u914D\u7F6E ${key} \u5931\u8D25: ${text || upstream.status}`); +} +async function ensureLocalFallbackProviderOnGoosed(apiTarget, apiSecret, fetchImpl = undiciFetch) { + if (!LOCAL_LLM_FALLBACK.enabled) { + throw new Error("\u672C\u5730 LLM fallback \u5DF2\u7981\u7528"); + } + const profile = { + providerKind: "custom", + name: LOCAL_LLM_FALLBACK.displayName, + apiUrl: LOCAL_LLM_FALLBACK.apiUrl, + apiKey: LOCAL_LLM_FALLBACK.apiKey, + models: [LOCAL_LLM_FALLBACK.model], + defaultModel: LOCAL_LLM_FALLBACK.model, + goosedProviderId: cachedLocalFallbackProviderId, + engine: "openai" + }; + const goosedProviderId = await upsertCustomProviderOnGoosed( + apiTarget, + apiSecret, + profile, + fetchImpl + ); + cachedLocalFallbackProviderId = goosedProviderId; + return goosedProviderId; +} +async function updateSessionProvider(apiFetchImpl, sessionId, provider, model) { + const upstream = await apiFetchImpl("/agent/update_provider", { + method: "POST", + body: JSON.stringify({ + session_id: sessionId, + provider, + model + }) + }); + if (!upstream.ok) { + const text = await upstream.text().catch(() => ""); + throw new Error(text || `\u5207\u6362\u4F1A\u8BDD Provider \u5931\u8D25: ${upstream.status}`); + } +} +async function upsertCustomProviderOnGoosed(apiTarget, apiSecret, profile, fetchImpl) { + const headers = profile.relayProvider ? { "X-Provider": profile.relayProvider } : void 0; + const body = { + engine: profile.engine || "openai", + display_name: profile.name, + api_url: profile.apiUrl, + api_key: profile.apiKey, + models: profile.models, + supports_streaming: true, + requires_auth: true, + ...profile.basePath ? { base_path: profile.basePath } : {}, + ...headers ? { headers } : {} + }; + if (profile.goosedProviderId) { + const upstream2 = await goosedApiFetch( + apiTarget, + apiSecret, + `/config/custom-providers/${encodeURIComponent(profile.goosedProviderId)}`, + { method: "PUT", body: JSON.stringify(body) }, + fetchImpl + ); + if (!upstream2.ok) { + const text = await upstream2.text().catch(() => ""); + throw new Error(`\u66F4\u65B0 TKMind Agent \u81EA\u5B9A\u4E49 provider \u5931\u8D25: ${text || upstream2.status}`); + } + return profile.goosedProviderId; + } + const upstream = await goosedApiFetch( + apiTarget, + apiSecret, + "/config/custom-providers", + { method: "POST", body: JSON.stringify(body) }, + fetchImpl + ); + if (!upstream.ok) { + const text = await upstream.text().catch(() => ""); + throw new Error(`\u521B\u5EFA TKMind Agent \u81EA\u5B9A\u4E49 provider \u5931\u8D25: ${text || upstream.status}`); + } + const data = await upstream.json(); + return data.provider_name; +} +async function removeCustomProviderOnGoosed(apiTarget, apiSecret, goosedProviderId, fetchImpl) { + if (!goosedProviderId) return; + const upstream = await goosedApiFetch( + apiTarget, + apiSecret, + `/config/custom-providers/${encodeURIComponent(goosedProviderId)}`, + { method: "DELETE" }, + fetchImpl + ); + if (!upstream.ok && upstream.status !== 404) { + const text = await upstream.text().catch(() => ""); + throw new Error(`\u5220\u9664 TKMind Agent \u81EA\u5B9A\u4E49 provider \u5931\u8D25: ${text || upstream.status}`); + } +} +async function syncBuiltinProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl) { + const catalogItem = catalogById[profile.providerId]; + if (!catalogItem?.apiKeyEnv) { + throw new Error(`\u672A\u77E5\u5185\u7F6E provider: ${profile.providerId}`); + } + await writeGoosedConfig( + apiTarget, + apiSecret, + catalogItem.apiKeyEnv, + profile.apiKey, + true, + fetchImpl + ); + await writeGoosedConfig( + apiTarget, + apiSecret, + "GOOSE_PROVIDER", + profile.providerId, + false, + fetchImpl + ); + await writeGoosedConfig( + apiTarget, + apiSecret, + "GOOSE_MODEL", + profile.defaultModel, + false, + fetchImpl + ); + await writeGoosedConfig( + apiTarget, + apiSecret, + "TKMIND_PROVIDER", + profile.providerId, + false, + fetchImpl + ); + await writeGoosedConfig( + apiTarget, + apiSecret, + "TKMIND_MODEL", + profile.defaultModel, + false, + fetchImpl + ); +} +async function syncProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl = undiciFetch) { + if (profile.providerKind === "custom") { + const goosedProviderId = await upsertCustomProviderOnGoosed( + apiTarget, + apiSecret, + profile, + fetchImpl + ); + await writeGoosedConfig( + apiTarget, + apiSecret, + "GOOSE_PROVIDER", + goosedProviderId, + false, + fetchImpl + ); + await writeGoosedConfig( + apiTarget, + apiSecret, + "GOOSE_MODEL", + profile.defaultModel, + false, + fetchImpl + ); + await writeGoosedConfig( + apiTarget, + apiSecret, + "TKMIND_PROVIDER", + goosedProviderId, + false, + fetchImpl + ); + await writeGoosedConfig( + apiTarget, + apiSecret, + "TKMIND_MODEL", + profile.defaultModel, + false, + fetchImpl + ); + return goosedProviderId; + } + await syncBuiltinProfileToGoosed(apiTarget, apiSecret, profile, fetchImpl); + return profile.providerId; +} +function isCustomPayload(payload) { + return payload?.providerId === CUSTOM_PROVIDER_ID || payload?.providerKind === "custom"; +} +function validateCustomPayload(payload) { + const name = String(payload?.name ?? "").trim(); + const apiKey = String(payload?.apiKey ?? "").trim(); + const apiUrl = normalizeApiUrl(payload?.apiUrl); + const models = parseModelList(payload?.models); + const defaultModel = String(payload?.defaultModel ?? "").trim() || models[0] || ""; + if (!name) return { ok: false, message: "\u8BF7\u586B\u5199\u914D\u7F6E\u540D\u79F0" }; + if (!apiUrl) return { ok: false, message: "\u8BF7\u586B\u5199 API \u5730\u5740" }; + if (!apiKey) return { ok: false, message: "\u8BF7\u586B\u5199 API Key / Bearer Token" }; + if (models.length === 0) return { ok: false, message: "\u8BF7\u81F3\u5C11\u586B\u5199\u4E00\u4E2A\u6A21\u578B" }; + if (!models.includes(defaultModel)) { + return { ok: false, message: "\u9ED8\u8BA4\u6A21\u578B\u5FC5\u987B\u5728\u6A21\u578B\u5217\u8868\u4E2D" }; + } + return { + ok: true, + value: { + name, + apiKey, + apiUrl, + models, + defaultModel, + basePath: String(payload?.basePath ?? "").trim() || null, + engine: String(payload?.engine ?? "openai").trim() || "openai", + relayProvider: String(payload?.relayProvider ?? "").trim() || null + } + }; +} +function createLlmProviderService(pool2, { apiTarget, apiTargets: apiTargets2 = [], apiSecret, encryptionKey, apiFetchImpl = undiciFetch } = {}) { + const launchStates = /* @__PURE__ */ new Map(); + const syncTargets = [ + ...new Set([...Array.isArray(apiTargets2) ? apiTargets2 : [], apiTarget].filter(Boolean)) + ]; + function catalogItem(providerId) { + return catalogById[providerId] ?? null; + } + function decryptRow(row) { + return decryptSecret( + { + ciphertext: row.api_key_ciphertext, + iv: row.api_key_iv, + tag: row.api_key_tag + }, + encryptionKey + ); + } + async function getRowById(id) { + const [rows] = await pool2.query("SELECT * FROM h5_llm_provider_keys WHERE id = ? LIMIT 1", [ + id + ]); + return rows[0] ?? null; + } + async function getSelectedRow() { + const [rows] = await pool2.query( + "SELECT * FROM h5_llm_provider_keys WHERE is_selected = 1 AND status = ? LIMIT 1", + ["active"] + ); + return rows[0] ?? null; + } + async function getExecutorBindingRow(executor, purpose = "default") { + const normalizedExecutor = normalizeExecutor(executor); + if (!normalizedExecutor) return null; + const [rows] = await pool2.query( + "SELECT * FROM h5_llm_executor_bindings WHERE executor = ? AND purpose = ? LIMIT 1", + [normalizedExecutor, normalizePurpose(purpose)] + ); + return rows[0] ?? null; + } + async function clearSelected() { + await pool2.query("UPDATE h5_llm_provider_keys SET is_selected = 0, updated_at = ?", [ + Date.now() + ]); + } + async function getVisionRow() { + const [rows] = await pool2.query( + "SELECT * FROM h5_llm_provider_keys WHERE is_vision_selected = 1 AND status = ? LIMIT 1", + ["active"] + ); + return rows[0] ?? null; + } + async function clearVisionSelected() { + await pool2.query("UPDATE h5_llm_provider_keys SET is_vision_selected = 0, updated_at = ?", [ + Date.now() + ]); + } + let visionKeyConfiguredCache = null; + const syncRowCache = /* @__PURE__ */ new Map(); + const SYNC_ROW_CACHE_TTL_MS = 3e4; + async function syncRow(row, fetchImpl = apiFetchImpl) { + const cacheKey = `${row.id}:${row.updated_at}`; + const cached = syncRowCache.get(cacheKey); + if (cached && Date.now() - cached.syncedAt < SYNC_ROW_CACHE_TTL_MS && fetchImpl === apiFetchImpl) { + return cached.goosedProviderId; + } + const profile = profileFromRow(row, decryptRow); + const goosedProviderId = await syncProfileToGoosed( + apiTarget, + apiSecret, + profile, + fetchImpl + ); + if (profile.providerKind === "custom" && goosedProviderId !== row.goosed_provider_id) { + await pool2.query( + "UPDATE h5_llm_provider_keys SET goosed_provider_id = ?, provider_id = ?, updated_at = ? WHERE id = ?", + [goosedProviderId, goosedProviderId, Date.now(), row.id] + ); + } + if (fetchImpl === apiFetchImpl) { + syncRowCache.set(cacheKey, { goosedProviderId, syncedAt: Date.now() }); + } + return goosedProviderId; + } + async function syncRowWithModel(row, model, fetchImpl = apiFetchImpl) { + const originalModel = row.default_model; + row.default_model = model || row.default_model; + try { + return await syncRow(row, fetchImpl); + } finally { + row.default_model = originalModel; + } + } + async function resolveExecutorProvider(executor, purpose = "default", fetchImpl = apiFetchImpl) { + const binding = await getExecutorBindingRow(executor, purpose); + if (!binding?.enabled) { + return { ok: false, message: `${executorById[executor]?.label ?? executor} \u672A\u542F\u7528\u6A21\u578B\u7ED1\u5B9A` }; + } + if (!binding.provider_key_id) { + return { ok: false, message: `${executorById[executor]?.label ?? executor} \u672A\u7ED1\u5B9A Provider` }; + } + const row = await getRowById(binding.provider_key_id); + if (!row || row.status !== "active") { + return { ok: false, message: "\u7ED1\u5B9A\u7684 Provider \u4E0D\u5B58\u5728\u6216\u5DF2\u7981\u7528" }; + } + const publicRow = rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)); + if (!publicRow.models.includes(binding.model)) { + return { ok: false, message: "\u7ED1\u5B9A\u6A21\u578B\u4E0D\u5728\u5F53\u524D Provider \u652F\u6301\u5217\u8868\u4E2D" }; + } + try { + const goosedProviderId = await syncRowWithModel(row, binding.model, fetchImpl); + const profile = profileFromRow(row, decryptRow); + const providerId = profile.providerKind === "custom" ? goosedProviderId ?? profile.goosedProviderId ?? profile.providerId : profile.providerId; + return { + ok: true, + providerId, + model: binding.model, + source: "executor_binding", + executor, + purpose: binding.purpose + }; + } catch (err) { + return { + ok: false, + message: err instanceof Error ? err.message : "\u540C\u6B65\u6267\u884C\u5668\u6A21\u578B\u914D\u7F6E\u5931\u8D25" + }; + } + } + async function resolveExecutorRuntimeConfig(executor, { purpose = "default", includeSecret = false } = {}) { + const normalizedExecutor = normalizeExecutor(executor); + if (!normalizedExecutor) return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; + const binding = await getExecutorBindingRow(normalizedExecutor, purpose); + if (!binding?.enabled) { + return { + ok: false, + executor: normalizedExecutor, + purpose, + message: `${executorById[normalizedExecutor]?.label ?? normalizedExecutor} \u672A\u542F\u7528\u6A21\u578B\u7ED1\u5B9A` + }; + } + if (!binding.provider_key_id) { + return { + ok: false, + executor: normalizedExecutor, + purpose, + message: `${executorById[normalizedExecutor]?.label ?? normalizedExecutor} \u672A\u7ED1\u5B9A Provider` + }; + } + const row = await getRowById(binding.provider_key_id); + if (!row || row.status !== "active") { + return { + ok: false, + executor: normalizedExecutor, + purpose, + message: "\u7ED1\u5B9A\u7684 Provider \u4E0D\u5B58\u5728\u6216\u5DF2\u7981\u7528" + }; + } + const keyPublic = rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)); + if (!keyPublic.models.includes(binding.model)) { + return { + ok: false, + executor: normalizedExecutor, + purpose, + message: "\u7ED1\u5B9A\u6A21\u578B\u4E0D\u5728\u5F53\u524D Provider \u652F\u6301\u5217\u8868\u4E2D" + }; + } + const profile = profileFromRow(row, decryptRow); + const runtimeProfile = includeSecret ? profile : { ...profile, apiKey: "" }; + return { + ok: true, + executor: normalizedExecutor, + executorLabel: executorById[normalizedExecutor]?.label ?? normalizedExecutor, + purpose: binding.purpose, + providerKeyId: row.id, + providerName: row.name, + providerKind: profile.providerKind, + providerId: profile.providerId, + providerLabel: keyPublic.providerLabel, + goosedProviderId: profile.goosedProviderId, + model: binding.model, + apiUrl: profile.apiUrl, + apiBaseUrl: resolveApiBaseUrl(profile.apiUrl), + apiKeyMasked: keyPublic.apiKeyMasked, + env: executorEnvForProfile(normalizedExecutor, runtimeProfile, binding.model, { + includeSecret + }) + }; + } + function getLaunchState(executor, purpose = "default") { + const key = launchStateKey(executor, purpose); + const current = launchStates.get(key); + if (!current) return null; + if (current.pid && !isProcessAlive(current.pid)) { + current.running = false; + current.stoppedAt = current.stoppedAt ?? Date.now(); + current.exitReason = current.exitReason ?? "not_running"; + launchStates.set(key, current); + } + return current; + } + function setLaunchState(executor, purpose, state) { + const key = launchStateKey(executor, purpose); + launchStates.set(key, { + ...state, + executor: normalizeExecutor(executor), + purpose: normalizePurpose(purpose) + }); + return launchStates.get(key); + } + function stateToPublic(state) { + if (!state) return null; + return { + executor: state.executor, + purpose: state.purpose, + pid: state.pid ?? null, + running: Boolean(state.running), + command: state.command ?? null, + args: state.args ?? [], + cwd: state.cwd ?? null, + logFile: state.logFile ?? null, + startedAt: state.startedAt ?? null, + stoppedAt: state.stoppedAt ?? null, + exitReason: state.exitReason ?? null, + mode: state.mode ?? null, + instruction: state.instruction ?? null + }; + } + async function stopLaunchState(state, { force = false } = {}) { + if (!state?.pid) { + return { ok: false, message: "\u672A\u627E\u5230\u8FD0\u884C\u4E2D\u7684\u6267\u884C\u5668\u8FDB\u7A0B" }; + } + if (!isProcessAlive(state.pid)) { + state.running = false; + state.stoppedAt = state.stoppedAt ?? Date.now(); + state.exitReason = state.exitReason ?? "not_running"; + return { ok: true, stopped: true, state: stateToPublic(state) }; + } + try { + process.kill(state.pid, "SIGTERM"); + if (!force) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + } + if (force || isProcessAlive(state.pid)) { + try { + process.kill(state.pid, "SIGKILL"); + } catch { + } + } + state.running = false; + state.stoppedAt = Date.now(); + state.exitReason = force ? "force_kill" : "sigterm"; + return { ok: true, stopped: true, state: stateToPublic(state) }; + } catch (err) { + return { + ok: false, + message: err instanceof Error ? err.message : "\u505C\u6B62\u5931\u8D25" + }; + } + } + async function testSelectedRow(row) { + const profile = profileFromRow(row, decryptRow); + if (profile.providerKind === "custom") { + try { + return await testRelayConnection( + { + apiUrl: profile.apiUrl, + apiKey: profile.apiKey, + model: profile.defaultModel, + relayProvider: profile.relayProvider + }, + apiFetchImpl + ); + } catch (err) { + return { + ok: false, + message: err instanceof Error ? err.message : String(err) + }; + } + } + return { ok: true, model: profile.defaultModel }; + } + async function resolveSelectedProvider(fetchImpl = apiFetchImpl) { + const row = await getSelectedRow(); + if (!row) { + return { ok: false, message: "\u8BF7\u5148\u542F\u7528 LLM \u914D\u7F6E" }; + } + const profile = profileFromRow(row, decryptRow); + const selectedTest = await testSelectedRow(row); + if (!selectedTest.ok) { + return { + ok: false, + message: selectedTest.message ?? "\u5F53\u524D\u9009\u4E2D\u7684 LLM \u4E0D\u53EF\u7528" + }; + } + try { + const goosedProviderId = await syncRow(row, fetchImpl); + const providerId = profile.providerKind === "custom" ? goosedProviderId ?? profile.goosedProviderId ?? profile.providerId : profile.providerId; + return { + ok: true, + providerId, + model: profile.defaultModel, + source: "selected" + }; + } catch (err) { + return { + ok: false, + message: err instanceof Error ? err.message : "\u540C\u6B65 LLM \u914D\u7F6E\u5931\u8D25" + }; + } + } + async function resolveLocalFallbackProvider(fetchImpl = apiFetchImpl) { + let localTest; + try { + localTest = await testLocalLlmConnection(apiFetchImpl); + } catch (err) { + localTest = { + ok: false, + message: err instanceof Error ? err.message : String(err) + }; + } + if (!localTest.ok) { + return { + ok: false, + message: localTest.message ?? "\u672C\u5730 LLM \u4E0D\u53EF\u7528" + }; + } + const providerId = await ensureLocalFallbackProviderOnGoosed( + apiTarget, + apiSecret, + fetchImpl + ); + return { + ok: true, + providerId, + model: LOCAL_LLM_FALLBACK.model, + source: "local_fallback" + }; + } + const goosedApi = (pathname, init) => goosedApiFetch(apiTarget, apiSecret, pathname, init, apiFetchImpl); + return { + catalog: LLM_PROVIDER_CATALOG, + executorCatalog: LLM_EXECUTOR_CATALOG, + async listKeys() { + const [rows] = await pool2.query( + "SELECT * FROM h5_llm_provider_keys ORDER BY is_selected DESC, updated_at DESC" + ); + return rows.map( + (row) => rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)) + ); + }, + async listExecutorBindings() { + const keys = await this.listKeys(); + const keyMap = new Map(keys.map((key) => [key.id, key])); + const [rows] = await pool2.query( + 'SELECT * FROM h5_llm_executor_bindings ORDER BY FIELD(executor, "goose", "aider", "openhands"), purpose' + ); + const rowMap = new Map( + rows.map((row) => [`${row.executor}:${row.purpose}`, row]) + ); + return LLM_EXECUTOR_CATALOG.map((executor) => { + const row = rowMap.get(`${executor.id}:default`); + if (!row) { + return { + id: null, + executor: executor.id, + executorLabel: executor.label, + executorDescription: executor.description, + purpose: "default", + providerKeyId: null, + providerName: null, + providerLabel: null, + providerId: null, + model: "", + enabled: false, + availableModels: [], + createdAt: null, + updatedAt: null + }; + } + return rowToExecutorBinding(row, keyMap.get(row.provider_key_id) ?? null); + }); + }, + async setExecutorBinding(executor, payload = {}) { + const normalizedExecutor = normalizeExecutor(executor); + if (!normalizedExecutor) return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; + const purpose = normalizePurpose(payload.purpose); + const enabled = payload.enabled !== false; + const keyId = String(payload.keyId ?? payload.providerKeyId ?? "").trim() || null; + const model = String(payload.model ?? "").trim(); + if (enabled && !keyId) return { ok: false, message: "\u8BF7\u9009\u62E9 Provider" }; + if (enabled && !model) return { ok: false, message: "\u8BF7\u9009\u62E9\u6A21\u578B" }; + let keyPublic = null; + if (keyId) { + const row2 = await getRowById(keyId); + if (!row2) return { ok: false, message: "Provider \u4E0D\u5B58\u5728" }; + if (row2.status !== "active") return { ok: false, message: "Provider \u5DF2\u7981\u7528" }; + keyPublic = rowToPublic(row2, catalogItem(row2.provider_id), decryptRow(row2)); + if (model && !keyPublic.models.includes(model)) { + return { ok: false, message: "\u6A21\u578B\u4E0D\u5728\u8BE5 Provider \u652F\u6301\u5217\u8868\u4E2D" }; + } + } + const existing = await getExecutorBindingRow(normalizedExecutor, purpose); + const now = Date.now(); + if (existing) { + await pool2.query( + `UPDATE h5_llm_executor_bindings + SET provider_key_id = ?, model = ?, enabled = ?, updated_at = ? + WHERE id = ?`, + [keyId, model, enabled ? 1 : 0, now, existing.id] + ); + } else { + await pool2.query( + `INSERT INTO h5_llm_executor_bindings + (id, executor, purpose, provider_key_id, model, enabled, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [crypto3.randomUUID(), normalizedExecutor, purpose, keyId, model, enabled ? 1 : 0, now, now] + ); + } + const row = await getExecutorBindingRow(normalizedExecutor, purpose); + return { ok: true, binding: rowToExecutorBinding(row, keyPublic) }; + }, + async getExecutorRuntimeConfig(executor, options = {}) { + return resolveExecutorRuntimeConfig(executor, options); + }, + async listExecutorRuntimeConfigs(options = {}) { + const configs = await Promise.all( + LLM_EXECUTOR_CATALOG.map( + (executor) => resolveExecutorRuntimeConfig(executor.id, options) + ) + ); + return configs; + }, + async getExecutorLaunchPlan(executor, options = {}) { + const runtime = await resolveExecutorRuntimeConfig(executor, { + purpose: options.purpose ?? "default", + includeSecret: options.includeSecret ?? false + }); + return buildExecutorLaunchPlan(runtime, options); + }, + async listExecutorLaunchPlans(options = {}) { + const runtimes = await this.listExecutorRuntimeConfigs({ + purpose: options.purpose ?? "default", + includeSecret: options.includeSecret ?? false + }); + return runtimes.map((runtime) => buildExecutorLaunchPlan(runtime, options)); + }, + async listExecutorLaunchStates(options = {}) { + const purpose = normalizePurpose(options.purpose ?? "default"); + return LLM_EXECUTOR_CATALOG.map((item) => stateToPublic(getLaunchState(item.id, purpose))); + }, + async getExecutorLaunchState(executor, options = {}) { + const purpose = normalizePurpose(options.purpose ?? "default"); + return stateToPublic(getLaunchState(executor, purpose)); + }, + async launchExecutor(executor, options = {}) { + const normalizedExecutor = normalizeExecutor(executor); + if (!normalizedExecutor) { + return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; + } + const purpose = normalizePurpose(options.purpose ?? "default"); + const mode = normalizeLaunchMode(options.mode ?? (normalizedExecutor === "openhands" ? "serve" : "headless")); + const instruction = String(options.instruction ?? "").trim(); + const currentState = getLaunchState(normalizedExecutor, purpose); + if (currentState?.running) { + return { + ok: true, + executor: normalizedExecutor, + purpose, + running: true, + reused: true, + launch: stateToPublic(currentState) + }; + } + if (normalizedExecutor === "goose") { + return { + ok: false, + executor: "goose", + message: "Goose \u4F5C\u4E3A\u73B0\u6709\u670D\u52A1\u5165\u53E3\uFF0C\u4E0D\u901A\u8FC7\u540E\u53F0\u76F4\u63A5\u542F\u52A8" + }; + } + if (normalizedExecutor === "aider" && !instruction) { + return { + ok: false, + executor: "aider", + message: "Aider \u542F\u52A8\u9700\u8981 instruction" + }; + } + if (normalizedExecutor === "openhands" && mode === "headless" && !instruction) { + return { + ok: false, + executor: "openhands", + message: "OpenHands headless \u542F\u52A8\u9700\u8981 instruction" + }; + } + const runtime = await resolveExecutorRuntimeConfig(normalizedExecutor, { + purpose, + includeSecret: true + }); + const plan = buildExecutorLaunchPlan(runtime, { + purpose, + mode, + cwd: options.cwd, + instruction, + includeSecret: true + }); + if (!plan.ok) { + return plan; + } + const launch = spawnDetachedExecutor(plan, { logDir: options.logDir }); + if (!launch.ok) return launch; + const state = setLaunchState(normalizedExecutor, purpose, { + pid: launch.pid ?? null, + running: true, + command: launch.command, + args: launch.args ?? [], + cwd: launch.cwd ?? plan.cwd, + logFile: launch.logFile, + startedAt: Date.now(), + mode, + instruction: instruction || null + }); + return { + ...launch, + purpose, + running: true, + launch: stateToPublic(state) + }; + }, + async stopExecutor(executor, options = {}) { + const normalizedExecutor = normalizeExecutor(executor); + if (!normalizedExecutor) { + return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; + } + const purpose = normalizePurpose(options.purpose ?? "default"); + const state = getLaunchState(normalizedExecutor, purpose); + if (!state) { + return { ok: false, executor: normalizedExecutor, purpose, message: "\u672A\u627E\u5230\u6267\u884C\u8BB0\u5F55" }; + } + const result = await stopLaunchState(state, { force: Boolean(options.force) }); + if (!result.ok) return { ok: false, executor: normalizedExecutor, purpose, message: result.message }; + setLaunchState(normalizedExecutor, purpose, state); + return { + ok: true, + executor: normalizedExecutor, + purpose, + launch: result.state + }; + }, + async restartExecutor(executor, options = {}) { + const normalizedExecutor = normalizeExecutor(executor); + if (!normalizedExecutor) { + return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; + } + const purpose = normalizePurpose(options.purpose ?? "default"); + const state = getLaunchState(normalizedExecutor, purpose); + if (!state) { + return { ok: false, executor: normalizedExecutor, purpose, message: "\u672A\u627E\u5230\u53EF\u91CD\u542F\u7684\u6267\u884C\u8BB0\u5F55" }; + } + await stopLaunchState(state, { force: true }); + const launchOptions = { + purpose, + mode: options.mode ?? state.mode ?? (normalizedExecutor === "openhands" ? "serve" : "headless"), + cwd: options.cwd ?? state.cwd ?? process.cwd(), + instruction: options.instruction ?? state.instruction ?? "", + logDir: options.logDir + }; + return this.launchExecutor(normalizedExecutor, launchOptions); + }, + async createKey(payload) { + const custom = isCustomPayload(payload); + let providerKind = "builtin"; + let providerId = String(payload?.providerId ?? "").trim(); + let insertName = String(payload?.name ?? "").trim(); + let apiKey = String(payload?.apiKey ?? "").trim(); + let defaultModel = String(payload?.defaultModel ?? "").trim(); + let apiUrl = null; + let basePath = null; + let engine = "openai"; + let relayProvider = null; + let modelsJson = null; + let meta = catalogItem(providerId); + if (custom) { + const validated = validateCustomPayload(payload); + if (!validated.ok) return validated; + providerKind = "custom"; + providerId = CUSTOM_PROVIDER_ID; + meta = catalogById[CUSTOM_PROVIDER_ID]; + insertName = validated.value.name; + apiKey = validated.value.apiKey; + apiUrl = validated.value.apiUrl; + basePath = validated.value.basePath; + engine = validated.value.engine; + relayProvider = validated.value.relayProvider; + defaultModel = validated.value.defaultModel; + modelsJson = JSON.stringify(validated.value.models); + } else if (providerId === "custom_qwen") { + if (!meta) return { ok: false, message: "\u4E0D\u652F\u6301\u7684 provider" }; + if (!insertName) return { ok: false, message: "\u8BF7\u586B\u5199\u914D\u7F6E\u540D\u79F0" }; + if (!apiKey) return { ok: false, message: "\u8BF7\u586B\u5199 API Key" }; + providerKind = "custom"; + apiUrl = meta.apiUrl ?? "https://dashscope.aliyuncs.com/compatible-mode/v1"; + const qwenModels = meta.models; + defaultModel = defaultModel || meta.defaultModel || qwenModels[0]; + if (!qwenModels.includes(defaultModel)) defaultModel = qwenModels[0]; + modelsJson = JSON.stringify(qwenModels); + } else { + if (!meta) return { ok: false, message: "\u4E0D\u652F\u6301\u7684 provider" }; + if (!insertName) return { ok: false, message: "\u8BF7\u586B\u5199\u914D\u7F6E\u540D\u79F0" }; + if (!apiKey) return { ok: false, message: "\u8BF7\u586B\u5199 API Key" }; + defaultModel = defaultModel || meta.defaultModel; + if (!meta.models.includes(defaultModel)) { + return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6A21\u578B" }; + } + } + const [existing] = await pool2.query( + "SELECT id FROM h5_llm_provider_keys WHERE name = ? LIMIT 1", + [insertName] + ); + if (existing.length > 0) return { ok: false, message: "\u914D\u7F6E\u540D\u79F0\u5DF2\u5B58\u5728" }; + const [countRows] = await pool2.query("SELECT COUNT(*) AS total FROM h5_llm_provider_keys"); + const shouldSelect = Number(countRows[0]?.total ?? 0) === 0; + const encrypted = encryptSecret(apiKey, encryptionKey); + const now = Date.now(); + const id = crypto3.randomUUID(); + if (shouldSelect) await clearSelected(); + await pool2.query( + `INSERT INTO h5_llm_provider_keys + (id, provider_id, provider_kind, api_url, base_path, models_json, goosed_provider_id, engine, relay_provider, + name, api_key_ciphertext, api_key_iv, api_key_tag, default_model, status, is_selected, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)`, + [ + id, + providerId, + providerKind, + apiUrl, + basePath, + modelsJson, + engine, + relayProvider, + insertName, + encrypted.ciphertext, + encrypted.iv, + encrypted.tag, + defaultModel, + shouldSelect ? 1 : 0, + now, + now + ] + ); + let row = await getRowById(id); + if (shouldSelect) { + await syncRow(row); + row = await getRowById(id); + } + return { ok: true, key: rowToPublic(row, meta, apiKey) }; + }, + async updateKey(id, payload) { + const row = await getRowById(id); + if (!row) return { ok: false, message: "\u914D\u7F6E\u4E0D\u5B58\u5728" }; + const providerKind = row.provider_kind ?? "builtin"; + const nextName = payload?.name !== void 0 ? String(payload.name).trim() : row.name; + const nextApiKey = payload?.apiKey !== void 0 ? String(payload.apiKey).trim() : null; + const nextStatus = payload?.status === "disabled" || payload?.status === "active" ? payload.status : row.status; + if (!nextName) return { ok: false, message: "\u8BF7\u586B\u5199\u914D\u7F6E\u540D\u79F0" }; + const [existing] = await pool2.query( + "SELECT id FROM h5_llm_provider_keys WHERE name = ? AND id <> ? LIMIT 1", + [nextName, id] + ); + if (existing.length > 0) return { ok: false, message: "\u914D\u7F6E\u540D\u79F0\u5DF2\u5B58\u5728" }; + if (row.is_selected && nextStatus === "disabled") { + return { ok: false, message: "\u5F53\u524D\u542F\u7528\u7684\u914D\u7F6E\u4E0D\u80FD\u7981\u7528\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u5176\u4ED6\u914D\u7F6E" }; + } + let nextModel = row.default_model; + let nextApiUrl = row.api_url; + let nextBasePath = row.base_path; + let nextEngine = row.engine ?? "openai"; + let nextRelayProvider = row.relay_provider; + let nextModelsJson = row.models_json; + if (providerKind === "custom") { + const models = payload?.models !== void 0 ? parseModelList(payload.models) : parseModelsJson(row.models_json); + nextModel = payload?.defaultModel !== void 0 ? String(payload.defaultModel).trim() : row.default_model; + nextApiUrl = payload?.apiUrl !== void 0 ? normalizeApiUrl(payload.apiUrl) : row.api_url; + nextBasePath = payload?.basePath !== void 0 ? String(payload.basePath).trim() || null : row.base_path; + nextEngine = payload?.engine !== void 0 ? String(payload.engine).trim() || "openai" : row.engine; + nextRelayProvider = payload?.relayProvider !== void 0 ? String(payload.relayProvider).trim() || null : row.relay_provider; + if (models.length === 0) return { ok: false, message: "\u8BF7\u81F3\u5C11\u4FDD\u7559\u4E00\u4E2A\u6A21\u578B" }; + if (!models.includes(nextModel)) { + return { ok: false, message: "\u9ED8\u8BA4\u6A21\u578B\u5FC5\u987B\u5728\u6A21\u578B\u5217\u8868\u4E2D" }; + } + if (!nextApiUrl) return { ok: false, message: "\u8BF7\u586B\u5199 API \u5730\u5740" }; + nextModelsJson = JSON.stringify(models); + } else { + const meta = catalogItem(row.provider_id); + if (!meta) return { ok: false, message: "\u4E0D\u652F\u6301\u7684 provider" }; + nextModel = payload?.defaultModel !== void 0 ? String(payload.defaultModel).trim() : row.default_model; + if (!meta.models.includes(nextModel)) { + return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6A21\u578B" }; + } + } + const encrypted = nextApiKey ? encryptSecret(nextApiKey, encryptionKey) : { + ciphertext: row.api_key_ciphertext, + iv: row.api_key_iv, + tag: row.api_key_tag + }; + const now = Date.now(); + await pool2.query( + `UPDATE h5_llm_provider_keys + SET name = ?, api_url = ?, base_path = ?, models_json = ?, engine = ?, relay_provider = ?, + api_key_ciphertext = ?, api_key_iv = ?, api_key_tag = ?, + default_model = ?, status = ?, updated_at = ? + WHERE id = ?`, + [ + nextName, + nextApiUrl, + nextBasePath, + nextModelsJson, + nextEngine, + nextRelayProvider, + encrypted.ciphertext, + encrypted.iv, + encrypted.tag, + nextModel, + nextStatus, + now, + id + ] + ); + if (row.is_selected) { + await syncRow(await getRowById(id)); + } + const updated = await getRowById(id); + return { + ok: true, + key: rowToPublic(updated, catalogItem(updated.provider_id), nextApiKey || decryptRow(row)) + }; + }, + async selectKey(id) { + const row = await getRowById(id); + if (!row) return { ok: false, message: "\u914D\u7F6E\u4E0D\u5B58\u5728" }; + if (row.status !== "active") { + return { ok: false, message: "\u5DF2\u7981\u7528\u7684\u914D\u7F6E\u4E0D\u80FD\u542F\u7528" }; + } + await clearSelected(); + const now = Date.now(); + await pool2.query( + "UPDATE h5_llm_provider_keys SET is_selected = 1, updated_at = ? WHERE id = ?", + [now, id] + ); + await syncRow(row); + const updated = await getRowById(id); + return { + ok: true, + key: rowToPublic(updated, catalogItem(updated.provider_id), decryptRow(updated)) + }; + }, + async deleteKey(id) { + const row = await getRowById(id); + if (!row) return { ok: false, message: "\u914D\u7F6E\u4E0D\u5B58\u5728" }; + if (row.is_selected) { + return { ok: false, message: "\u5F53\u524D\u542F\u7528\u7684\u914D\u7F6E\u4E0D\u80FD\u5220\u9664\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u5176\u4ED6\u914D\u7F6E" }; + } + if ((row.provider_kind ?? "builtin") === "custom" && row.goosed_provider_id) { + await removeCustomProviderOnGoosed( + apiTarget, + apiSecret, + row.goosed_provider_id, + apiFetchImpl + ); + } + await pool2.query("DELETE FROM h5_llm_provider_keys WHERE id = ?", [id]); + return { ok: true }; + }, + async syncSelectedToGoosed() { + const targets = syncTargets.length ? syncTargets : [apiTarget].filter(Boolean); + let lastResolved = null; + for (const target of targets) { + const targetFetch = (url, init) => { + const pathname = `${url.pathname}${url.search}`; + return goosedApiFetch(target, apiSecret, pathname, init, apiFetchImpl); + }; + let resolved = await resolveExecutorProvider("goose", "default", targetFetch); + if (!resolved.ok) { + resolved = await resolveSelectedProvider(targetFetch); + } + if (!resolved.ok) { + return { ok: false, synced: false, target, message: resolved.message }; + } + lastResolved = resolved; + } + return { + ok: true, + synced: true, + targets, + source: lastResolved.source, + providerId: lastResolved.providerId, + model: lastResolved.model + }; + }, + async applyBestProviderForSession(sessionId, fetchImpl = apiFetchImpl) { + let resolved = await resolveExecutorProvider("goose", "default", fetchImpl); + if (!resolved.ok) { + resolved = await resolveSelectedProvider(fetchImpl); + } + if (!resolved.ok) { + return resolved; + } + const sessionGoosedApi = (pathname, init) => goosedApiFetch(apiTarget, apiSecret, pathname, init, fetchImpl); + await updateSessionProvider(sessionGoosedApi, sessionId, resolved.providerId, resolved.model); + return resolved; + }, + /** Switch session to local Ollama (credits exhausted or relay 500 payload limit). */ + async applyLocalFallbackForSession(sessionId, fetchImpl = apiFetchImpl) { + const resolved = await resolveLocalFallbackProvider(fetchImpl); + if (!resolved.ok) { + return resolved; + } + const sessionGoosedApi = (pathname, init) => goosedApiFetch(apiTarget, apiSecret, pathname, init, fetchImpl); + await updateSessionProvider(sessionGoosedApi, sessionId, resolved.providerId, resolved.model); + return resolved; + }, + async hasVisionKey() { + if (visionKeyConfiguredCache !== null) return visionKeyConfiguredCache; + const row = await getVisionRow(); + visionKeyConfiguredCache = row !== null; + return visionKeyConfiguredCache; + }, + async getVisionSettings() { + const row = await getVisionRow(); + if (!row) { + return { keyId: null, keyName: null, providerLabel: null, visionModel: null, availableModels: [] }; + } + const publicRow = rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)); + return { + keyId: publicRow.id, + keyName: publicRow.name, + providerLabel: publicRow.providerLabel, + visionModel: publicRow.defaultModel, + availableModels: publicRow.models + }; + }, + async setVisionKey(keyId, model) { + const nextModel = String(model ?? "").trim(); + if (!nextModel) return { ok: false, message: "\u8BF7\u9009\u62E9\u89C6\u89C9\u6A21\u578B" }; + const row = await getRowById(keyId); + if (!row) return { ok: false, message: "\u914D\u7F6E\u4E0D\u5B58\u5728" }; + if (row.status !== "active") return { ok: false, message: "\u8BE5\u914D\u7F6E\u5DF2\u7981\u7528" }; + const publicRow = rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)); + if (!publicRow.models.includes(nextModel)) { + return { ok: false, message: "\u6A21\u578B\u4E0D\u5728\u8BE5\u914D\u7F6E\u652F\u6301\u5217\u8868\u4E2D" }; + } + await clearVisionSelected(); + const now = Date.now(); + await pool2.query( + "UPDATE h5_llm_provider_keys SET is_vision_selected = 1, default_model = ?, updated_at = ? WHERE id = ?", + [nextModel, now, keyId] + ); + visionKeyConfiguredCache = true; + return { ok: true, vision: await this.getVisionSettings() }; + }, + async clearVisionKey() { + await clearVisionSelected(); + visionKeyConfiguredCache = false; + return { ok: true }; + }, + async applyVisionProviderForSession(sessionId, fetchImpl = apiFetchImpl) { + const row = await getVisionRow(); + if (!row) return { ok: false, message: "\u672A\u914D\u7F6E\u56FE\u7247\u4EFB\u52A1\u6A21\u578B" }; + const sessionGoosedApi = (pathname, init) => goosedApiFetch(apiTarget, apiSecret, pathname, init, fetchImpl); + let goosedProviderId; + try { + goosedProviderId = await syncRow(row, fetchImpl); + if (!goosedProviderId) { + goosedProviderId = profileFromRow(row, decryptRow).goosedProviderId ?? row.provider_id; + } + } catch (err) { + return { ok: false, message: err instanceof Error ? err.message : "\u540C\u6B65\u89C6\u89C9\u6A21\u578B\u5931\u8D25" }; + } + await updateSessionProvider(sessionGoosedApi, sessionId, goosedProviderId, row.default_model); + return { ok: true, providerId: goosedProviderId, model: row.default_model, source: "vision" }; + }, + // Calls the vision provider directly (not through Goose) to analyze images. + // Returns the model's text description, or null on failure. + async analyzeImagesWithVision(imageItems, userText) { + const row = await getVisionRow(); + if (!row) return null; + const apiUrl = String(row.api_url ?? "").trim(); + if (!apiUrl) return null; + const apiKey = decryptRow(row); + if (!apiKey) return null; + const model = String(row.default_model ?? "qwen-vl-max").trim(); + const content = [ + ...imageItems.map((item) => ({ + type: "image_url", + image_url: { url: `data:${item.mimeType};base64,${item.data}` } + })), + { type: "text", text: String(userText || "\u8BF7\u63CF\u8FF0\u8FD9\u5F20\u56FE\u7247\u7684\u5185\u5BB9").trim() } + ]; + try { + const resp = await undiciFetch(`${apiUrl}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify({ + model, + messages: [{ role: "user", content }], + max_tokens: 1200 + }), + signal: AbortSignal.timeout(3e4) + }); + if (!resp.ok) return null; + const json = await resp.json(); + return json?.choices?.[0]?.message?.content ?? null; + } catch { + return null; + } + }, + async getGlobalSettings() { + const row = await getSelectedRow(); + if (!row) { + return { + keyId: null, + keyName: null, + providerLabel: null, + globalModel: null, + availableModels: [] + }; + } + const publicRow = rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)); + return { + keyId: publicRow.id, + keyName: publicRow.name, + providerLabel: publicRow.providerLabel, + globalModel: publicRow.defaultModel, + availableModels: publicRow.models + }; + }, + async setGlobalModel(model) { + const nextModel = String(model ?? "").trim(); + if (!nextModel) return { ok: false, message: "\u8BF7\u9009\u62E9\u5168\u5C40\u6A21\u578B" }; + const row = await getSelectedRow(); + if (!row) return { ok: false, message: "\u8BF7\u5148\u542F\u7528\u4E00\u4E2A LLM \u914D\u7F6E" }; + const publicRow = rowToPublic(row, catalogItem(row.provider_id), decryptRow(row)); + if (!publicRow.models.includes(nextModel)) { + return { ok: false, message: "\u6A21\u578B\u4E0D\u5728\u5F53\u524D Provider \u652F\u6301\u5217\u8868\u4E2D" }; + } + await pool2.query( + "UPDATE h5_llm_provider_keys SET default_model = ?, updated_at = ? WHERE id = ?", + [nextModel, Date.now(), row.id] + ); + await syncRow(await getRowById(row.id)); + return { ok: true, global: await this.getGlobalSettings() }; + }, + async testDraft(payload) { + const custom = isCustomPayload(payload); + if (!custom) { + return { ok: false, message: "\u8054\u901A\u6D4B\u8BD5\u76EE\u524D\u652F\u6301\u81EA\u5B9A\u4E49 OpenAI \u517C\u5BB9\u914D\u7F6E" }; + } + const validated = validateCustomPayload(payload); + if (!validated.ok) return validated; + const model = String(payload?.testModel ?? payload?.defaultModel ?? validated.value.defaultModel).trim(); + if (!validated.value.models.includes(model)) { + return { ok: false, message: "\u6D4B\u8BD5\u6A21\u578B\u4E0D\u5728\u6A21\u578B\u5217\u8868\u4E2D" }; + } + return testRelayConnection( + { + apiUrl: validated.value.apiUrl, + apiKey: validated.value.apiKey, + model, + relayProvider: validated.value.relayProvider + }, + apiFetchImpl + ); + }, + async testKey(id, testModel) { + const row = await getRowById(id); + if (!row) return { ok: false, message: "\u914D\u7F6E\u4E0D\u5B58\u5728" }; + const model = String(testModel ?? row.default_model).trim(); + const providerKind = row.provider_kind ?? "builtin"; + const meta = catalogItem(row.provider_id); + const models = providerKind === "custom" ? parseModelsJson(row.models_json) : meta?.models ?? []; + if (!models.includes(model)) { + return { ok: false, message: "\u6D4B\u8BD5\u6A21\u578B\u4E0D\u5728\u914D\u7F6E\u5217\u8868\u4E2D" }; + } + if (providerKind !== "custom") { + const apiUrl = BUILTIN_PROVIDER_TEST_URLS[row.provider_id]; + if (!apiUrl) { + return { ok: false, message: "\u8BE5\u5185\u7F6E Provider \u6682\u4E0D\u652F\u6301\u8054\u901A\u6D4B\u8BD5" }; + } + return testRelayConnection( + { + apiUrl, + apiKey: decryptRow(row), + model, + relayProvider: null + }, + apiFetchImpl + ); + } + return testRelayConnection( + { + apiUrl: row.api_url, + apiKey: decryptRow(row), + model, + relayProvider: row.relay_provider + }, + apiFetchImpl + ); + }, + async ensureBootstrapRelay() { + const [existing] = await pool2.query( + "SELECT id FROM h5_llm_provider_keys WHERE name = ? LIMIT 1", + [RELAY_BOOTSTRAP.name] + ); + if (existing.length > 0) { + return { ok: true, created: false, keyId: existing[0].id }; + } + const result = await this.createKey({ + providerId: CUSTOM_PROVIDER_ID, + name: RELAY_BOOTSTRAP.name, + apiKey: RELAY_BOOTSTRAP.apiKey, + apiUrl: RELAY_BOOTSTRAP.apiUrl, + models: RELAY_BOOTSTRAP.models, + defaultModel: RELAY_BOOTSTRAP.defaultModel, + relayProvider: RELAY_BOOTSTRAP.relayProvider + }); + if (!result.ok) return result; + if (result.key && !result.key.isSelected) { + await this.selectKey(result.key.id); + } + return { ok: true, created: true, key: result.key }; + } + }; +} + // tkmind-proxy.mjs -import fs3 from "node:fs"; -import path6 from "node:path"; +import fs5 from "node:fs"; +import path8 from "node:path"; import { Readable, Transform as Transform2, Writable } from "node:stream"; import { pipeline } from "node:stream/promises"; -import { Agent, fetch as undiciFetch } from "undici"; +import { Agent as Agent2, fetch as undiciFetch2 } from "undici"; // sse-billing.mjs import { Transform } from "node:stream"; @@ -578,7 +2706,7 @@ data: ${JSON.stringify(data)} } // capabilities.mjs -import path2 from "node:path"; +import path4 from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; // policies.mjs @@ -767,8 +2895,8 @@ var USER_API_ALLOWLIST = [ { method: "POST", pattern: /^\/agent\/harness_remember$/ } ]; function isNativeH5ApiPath(pathname) { - const path11 = String(pathname ?? ""); - return path11.startsWith("/mindspace/") || path11.startsWith("/plaza/"); + const path13 = String(pathname ?? ""); + return path13.startsWith("/mindspace/") || path13.startsWith("/plaza/"); } function evaluateProxyRequest(method, pathname, policies, { unrestricted = false } = {}) { if (unrestricted) return { allowed: true }; @@ -794,7 +2922,7 @@ function evaluateProxyRequest(method, pathname, policies, { unrestricted = false function resolveSandboxMcpServerPath(overridePath) { const normalized = String(overridePath ?? "").trim(); if (normalized) return normalized; - return path2.join(path2.dirname(fileURLToPath2(import.meta.url)), "mindspace-sandbox-mcp.mjs"); + return path4.join(path4.dirname(fileURLToPath2(import.meta.url)), "mindspace-sandbox-mcp.mjs"); } function resolveSandboxMcpNodeExecPath(overridePath) { const normalized = String(overridePath ?? "").trim(); @@ -1000,7 +3128,7 @@ function sandboxDeveloperTools(capabilities) { function sandboxMcpTools(capabilities) { const tools = []; if (capabilities.static_publish) { - tools.push("read_file", "write_file", "edit_file", "create_dir"); + tools.push("read_file", "write_file", "edit_file", "create_dir", "generate_long_image"); if (capabilities.shell || capabilities.code_browse) tools.push("list_dir"); } if (capabilities.private_data_space) { @@ -1166,16 +3294,81 @@ function normalizeCapabilityPatch(patch) { } // user-publish.mjs -import fs from "node:fs"; -import path3 from "node:path"; +import fs3 from "node:fs"; +import path5 from "node:path"; import { fileURLToPath as fileURLToPath3 } from "node:url"; -var __dirname2 = path3.dirname(fileURLToPath3(import.meta.url)); + +// mindspace-canonical-url.mjs +var MINDSPACE_PUBLIC_ROOT = "MindSpace"; +var MINDSPACE_PUBLIC_ZONE = "public"; +function urlError(message, code, details = {}) { + return Object.assign(new Error(message), { code, ...details }); +} +function requiredSegment(value, field) { + const normalized = String(value ?? "").trim(); + if (!normalized) { + throw urlError(`${field} is required`, "invalid_mindspace_url_segment", { field }); + } + if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) { + throw urlError(`${field} must be a single URL segment`, "invalid_mindspace_url_segment", { + field + }); + } + return normalized; +} +function normalizeMindSpacePublicBaseUrl(publicBaseUrl) { + const normalized = String(publicBaseUrl ?? "").trim().replace(/\/+$/, ""); + if (!normalized) { + throw urlError("publicBaseUrl is required", "invalid_mindspace_public_base_url"); + } + return normalized; +} +function normalizeMindSpacePublicRelativePath(relativePath) { + const raw = String(relativePath ?? "").trim(); + if (!raw) { + throw urlError("relativePath is required", "invalid_mindspace_public_relative_path"); + } + if (raw.includes("\0") || raw.includes("\\")) { + throw urlError("relativePath must use URL-style forward slashes", "invalid_mindspace_public_relative_path"); + } + if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw) || raw.startsWith("//")) { + throw urlError("relativePath must not be an absolute URL", "invalid_mindspace_public_relative_path"); + } + const clean = raw.replace(/^\/+/, ""); + const parts = clean.split("/").filter(Boolean); + if (parts.length === 0 || parts.includes("..")) { + throw urlError("relativePath must not traverse parent directories", "invalid_mindspace_public_relative_path"); + } + return parts.join("/"); +} +function createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath }) { + const base = normalizeMindSpacePublicBaseUrl(publicBaseUrl); + const owner = requiredSegment(ownerKey, "ownerKey"); + const relative = normalizeMindSpacePublicRelativePath(relativePath); + const encodedRelative = relative.split("/").map(encodeURIComponent).join("/"); + return `${base}/${MINDSPACE_PUBLIC_ROOT}/${encodeURIComponent(owner)}/${encodedRelative}`; +} +function createMindSpaceUserRootUrl({ publicBaseUrl, ownerKey }) { + const base = normalizeMindSpacePublicBaseUrl(publicBaseUrl); + const owner = requiredSegment(ownerKey, "ownerKey"); + return `${base}/${MINDSPACE_PUBLIC_ROOT}/${encodeURIComponent(owner)}/`; +} +function createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey, filename }) { + const clean = normalizeMindSpacePublicRelativePath(filename); + const relative = clean.startsWith(`${MINDSPACE_PUBLIC_ZONE}/`) ? clean : `${MINDSPACE_PUBLIC_ZONE}/${clean}`; + return createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath: relative }); +} + +// user-publish.mjs +var __dirname2 = path5.dirname(fileURLToPath3(import.meta.url)); var PUBLISH_SKILL_NAME = "static-page-publish"; var DOCX_SKILL_NAME = "docx-generate"; +var LONG_IMAGE_SKILL_NAME = "long-image-download"; var PUBLISH_ROOT_DIR = "MindSpace"; var PUBLIC_ZONE_DIR = "public"; -var PUBLISH_SKILL_DIR = path3.join(__dirname2, "skills", PUBLISH_SKILL_NAME); -var DOCX_SKILL_DIR = path3.join(__dirname2, "skills", DOCX_SKILL_NAME); +var PUBLISH_SKILL_DIR = path5.join(__dirname2, "skills", PUBLISH_SKILL_NAME); +var DOCX_SKILL_DIR = path5.join(__dirname2, "skills", DOCX_SKILL_NAME); +var LONG_IMAGE_SKILL_DIR = path5.join(__dirname2, "skills", LONG_IMAGE_SKILL_NAME); var WORKSPACE_HINTS_FILENAME = ".tkmindhints"; var LEGACY_WORKSPACE_HINTS_FILENAME = ".goosehints"; function renderBrandingBlock(userAddressName) { @@ -1211,57 +3404,55 @@ function resolveUsernameSlug(user) { return String(user.username).trim().toLowerCase(); } function resolvePublishDir(h5Root, user) { - return path3.join(h5Root, PUBLISH_ROOT_DIR, resolvePublishKey(user)); + return path5.join(h5Root, PUBLISH_ROOT_DIR, resolvePublishKey(user)); } function resolveGoosedSandboxRoot(h5Root, user, env = process.env) { const canonicalRoot = String( env.GOOSED_SANDBOX_PUBLISH_ROOT ?? env.MEMIND_SHARED_PUBLISH_ROOT ?? "" ).trim(); if (canonicalRoot) { - return path3.join(path3.resolve(canonicalRoot), resolvePublishKey(user)); + return path5.join(path5.resolve(canonicalRoot), resolvePublishKey(user)); } return resolvePublishDir(h5Root, user); } function resolveLegacyPublishDir(h5Root, user) { if (!user?.username) return null; - return path3.join(h5Root, PUBLISH_ROOT_DIR, resolveUsernameSlug(user)); + return path5.join(h5Root, PUBLISH_ROOT_DIR, resolveUsernameSlug(user)); } function mergePublishTrees(sourceDir, targetDir) { - if (!fs.existsSync(sourceDir)) return; - for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { - const from = path3.join(sourceDir, entry.name); - const to = path3.join(targetDir, entry.name); + if (!fs3.existsSync(sourceDir)) return; + for (const entry of fs3.readdirSync(sourceDir, { withFileTypes: true })) { + const from = path5.join(sourceDir, entry.name); + const to = path5.join(targetDir, entry.name); if (entry.isDirectory()) { - fs.mkdirSync(to, { recursive: true }); + fs3.mkdirSync(to, { recursive: true }); mergePublishTrees(from, to); - } else if (!fs.existsSync(to)) { - fs.copyFileSync(from, to); + } else if (!fs3.existsSync(to)) { + fs3.copyFileSync(from, to); } } } function migrateUserPublishDir(h5Root, user, legacyUsersRoot = null) { const target = resolvePublishDir(h5Root, user); - fs.mkdirSync(target, { recursive: true }); + fs3.mkdirSync(target, { recursive: true }); const legacyDirs = [ resolveLegacyPublishDir(h5Root, user), - legacyUsersRoot ? path3.join(legacyUsersRoot, resolveUsernameSlug(user)) : null + legacyUsersRoot ? path5.join(legacyUsersRoot, resolveUsernameSlug(user)) : null ].filter(Boolean); for (const legacyDir of legacyDirs) { - if (path3.resolve(legacyDir) === path3.resolve(target)) continue; + if (path5.resolve(legacyDir) === path5.resolve(target)) continue; mergePublishTrees(legacyDir, target); } return target; } function buildPublicUrl(publicBaseUrl, publishKey, filename = "") { - const base = `${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(publishKey)}`; - if (!filename) return `${base}/`; - const clean = filename.replace(/^\/+/, ""); - return `${base}/${clean.split("/").map(encodeURIComponent).join("/")}`; + if (!filename) { + return createMindSpaceUserRootUrl({ publicBaseUrl, ownerKey: publishKey }); + } + return createMindSpacePublicUrl({ publicBaseUrl, ownerKey: publishKey, relativePath: filename }); } function buildPublicZonePageUrl(publicBaseUrl, publishKey, filename) { - const clean = String(filename ?? "").replace(/^\/+/, ""); - const relative = clean.startsWith(`${PUBLIC_ZONE_DIR}/`) ? clean : `${PUBLIC_ZONE_DIR}/${clean}`; - return buildPublicUrl(publicBaseUrl, publishKey, relative); + return createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey: publishKey, filename }); } function renderPublishSkill({ slug, username, publicBaseUrl, publishDir, displayName }) { const addressName = resolveUserAddressName({ displayName, username, slug }); @@ -1279,7 +3470,7 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u 1. **\u552F\u4E00\u53EF\u5199\u76EE\u5F55**\uFF1A\`${publishDir}\` 2. **\u7981\u6B62**\u4F7F\u7528\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u5982 \`/Users/...\`\u3001\`../\` \u8DF3\u51FA\u76EE\u5F55\uFF09 -3. **\u5141\u8BB8**\u5728\u672C\u76EE\u5F55\u5185\u4F7F\u7528 \`write_file\`\u3001\`edit_file\`\u3001\`read_file\`\u3001\`list_dir\`\u3001\`generate_docx\`\uFF1B**\u7981\u6B62**\u8BBF\u95EE\u6B64\u76EE\u5F55\u5916\u7684\u8DEF\u5F84\uFF08\u542B \`../\`\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\u9879\u76EE\u6839\u76EE\u5F55\uFF1B\u7CFB\u7EDF\u4F1A\u5728 OS \u5C42\u62E6\u622A\u8D8A\u754C\u8BBF\u95EE\uFF09 +3. **\u5141\u8BB8**\u5728\u672C\u76EE\u5F55\u5185\u4F7F\u7528 \`write_file\`\u3001\`edit_file\`\u3001\`read_file\`\u3001\`list_dir\`\u3001\`generate_docx\`\u3001\`generate_long_image\`\uFF1B**\u7981\u6B62**\u8BBF\u95EE\u6B64\u76EE\u5F55\u5916\u7684\u8DEF\u5F84\uFF08\u542B \`../\`\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\u9879\u76EE\u6839\u76EE\u5F55\uFF1B\u7CFB\u7EDF\u4F1A\u5728 OS \u5C42\u62E6\u622A\u8D8A\u754C\u8BBF\u95EE\uFF09 4. **\u7981\u6B62**\u5B50 Agent\u3001\u6269\u5C55\u7BA1\u7406\u3001\u4FEE\u6539\u5DE5\u4F5C\u533A\u5916\u6587\u4EF6 5. \u9875\u9762\u9ED8\u8BA4\u5199\u5165 \`public/\` \u5206\u533A\uFF0C\u4F7F\u7528\u76F8\u5BF9\u8DEF\u5F84\u5982 \`public/report.html\`\u3001\`public/assets/chart.png\` 6. \u751F\u6210 HTML \u540E\uFF0C\u5411\u7528\u6237\u63D0\u4F9B\u53EF\u8BBF\u95EE\u94FE\u63A5\uFF0C\u683C\u5F0F\uFF1A @@ -1293,17 +3484,20 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u 3. \u5728 \`\` \u5199\u5165 **mindspace-cover** \u5143\u6570\u636E\uFF08\u89C1\u4E0B\u6587\uFF0C\u5FC5\u987B\u4E0E\u9875\u9762\u4E3B\u9898\u4E00\u81F4\uFF09 4. \u9875\u9762\u5185\u8D44\u6E90\u4F7F\u7528**\u76F8\u5BF9\u8DEF\u5F84**\uFF08\`assets/foo.png\`\uFF09\uFF0C\u4E0D\u8981\u7528\u78C1\u76D8\u7EDD\u5BF9\u8DEF\u5F84 5. \u4FDD\u5B58 HTML \u540E\uFF0C\u670D\u52A1\u7AEF\u4F1A**\u7ACB\u5373**\u751F\u6210\u540C\u540D\u9884\u89C8\u56FE \`<\u6587\u4EF6\u540D>.thumbnail.svg\`\uFF08Agent \u4EA4\u4E92\u9636\u6BB5\u5373\u751F\u6548\uFF0C\u65E0\u9700\u7B49\u7528\u6237\u4FDD\u5B58\u5230\u300C\u6211\u7684\u7A7A\u95F4\u300D\uFF09 -6. \u4E0B\u8F7D\u6309\u94AE\u7684\u76F8\u5BF9\u94FE\u63A5\u76EE\u6807\uFF08\u5982 \`public/report.docx\`\uFF09\u5FC5\u987B\u901A\u8FC7 \`generate_docx\` \u843D\u76D8\uFF0C\u4E14 basename \u4E0E HTML \u4E00\u81F4 -7. \u5B8C\u6210\u540E\u6309\u300C\u56DE\u590D\u683C\u5F0F\u300D\u8FD4\u56DE\u53EF\u70B9\u51FB\u94FE\u63A5 +6. \u9ED8\u8BA4\u53EA\u751F\u6210 HTML\uFF1B\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6 +7. \u82E5\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u4E0B\u8F7D\u6309\u94AE\u7684\u76F8\u5BF9\u94FE\u63A5\u76EE\u6807\uFF08\u5982 \`public/report.docx\`\uFF09\u5FC5\u987B\u901A\u8FC7 \`generate_docx\` \u843D\u76D8\uFF0C\u4E14 basename \u4E0E HTML \u4E00\u81F4 +8. \u82E5\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`long-image-download\`\uFF0C\u8C03\u7528 \`generate_long_image\` \u751F\u6210\u5E76\u786E\u8BA4 \`public/<\u9875\u9762\u540D>.long.png\` \u5DF2\u5B58\u5728 +9. \u5B8C\u6210\u540E\u6309\u300C\u56DE\u590D\u683C\u5F0F\u300D\u8FD4\u56DE\u53EF\u70B9\u51FB\u94FE\u63A5 -## \u4F34\u751F\u4E0B\u8F7D\u6587\u4EF6\uFF08\u5FC5\u987B\uFF09 +## \u6309\u9700\u4F34\u751F\u4E0B\u8F7D\u6587\u4EF6 +- \u9ED8\u8BA4\u4E0D\u751F\u6210\u4F34\u751F\u6587\u4EF6\uFF1B\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u4E0B\u8F7D\u9644\u4EF6\u65F6\u624D\u751F\u6210 - \u76F8\u5BF9\u8DEF\u5F84\u4E0B\u8F7D\u94FE\u63A5\uFF08\`.docx\` / \`.pdf\` \u7B49\uFF09\u5FC5\u987B\u5728 HTML \u540C\u76EE\u5F55\u6216\u5B50\u76EE\u5F55\u771F\u5B9E\u5B58\u5728 - Word \u6587\u6863\u5FC5\u987B\u7528 sandbox-fs \u7684 \`generate_docx\` \u751F\u6210\uFF1B\u7981\u6B62\u7528 \`computercontroller\` / shell \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6\u4F5C\u4E3A\u4EA4\u4ED8\u4F9D\u636E - \u6539 HTML \u6587\u4EF6\u540D\u65F6\u540C\u6B65 rename/copy \u4F34\u751F\u6587\u4EF6\uFF1B\u4ECE \`oa/\` \u590D\u5236\u5230 \`public/\` \u518D\u94FE\u63A5 - \u53EF\u8DD1 \`node scripts/check-mindspace-public-links.mjs\` \u81EA\u68C0 -## Word \u4E0B\u8F7D\u9875\uFF08\u5FC5\u987B\uFF09 +## Word \u4E0B\u8F7D\u9875\uFF08\u6309\u9700\uFF09 - \u5982\u679C\u7528\u6237\u660E\u786E\u8981\u6C42\u9875\u9762\u91CC**\u53EF\u4E0B\u8F7D Word / docx**\uFF0C\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\` - \u7528 \`.agents/skills/docx-generate/generate_docx.py\` \u751F\u6210 \`public/\u6587\u4EF6\u540D.docx\` @@ -1324,6 +3518,7 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u - \u6807\u9898\u7528\u9875\u9762\u771F\u5B9E\u4E3B\u9898\uFF08\u4E0D\u8981\u7528\u300C\u70B9\u51FB\u8FD9\u91CC\u300D\u300C\u94FE\u63A5\u300D\uFF09 - \u53EF\u540C\u65F6\u7ED9\u51FA\u672C\u5730\u76F8\u5BF9\u8DEF\u5F84\uFF08\u5982 \`public/pilates-618.html\`\uFF09\u4E0E\u516C\u7F51\u94FE\u63A5 - \u8BF4\u660E\uFF1A\u9759\u6001\u6587\u4EF6\u4FDD\u5B58\u5373\u751F\u6548\uFF0C**\u65E0\u9700\u91CD\u542F** +- \u82E5\u751F\u6210\u4E86\u957F\u56FE\uFF0C\u540C\u65F6\u7ED9 \`[\u957F\u56FE\u9884\u89C8](.../public/pilates-618.long.png)\` \u548C \`[\u4E0B\u8F7D\u957F\u56FE](.../public/pilates-618.html?download=long-image)\` ## \u4FE1\u606F\u6D41\u9884\u89C8\u56FE\uFF08\u5FC5\u987B\uFF09 @@ -1358,10 +3553,10 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u - \u5B8C\u6574 \`\`\uFF0C\`lang="zh-CN"\` - \u79FB\u52A8\u7AEF\u53CB\u597D\uFF1A\`\` - \u6DF1\u8272/\u6D45\u8272\u4E0E\u5185\u5BB9\u4E00\u81F4\uFF1B\u4E3B\u8272\u5728 CSS \u4E0E mindspace-cover \u4E2D\u4FDD\u6301\u4E00\u81F4 -- \u9875\u811A\u5E73\u53F0\u8054\u7CFB\u884C**\u5FC5\u987B**\u4F7F\u7528 \`data-mindspace-page-tag="platform-brand"\` \u6807\u8BB0\uFF0C\u4E14\u90AE\u7BB1/\u57DF\u540D\u53EA\u7528 **tkmind.cn**\uFF08\u5982 \`contact@tkmind.cn\`\uFF09\uFF0C**\u7981\u6B62** \`tkmind.ai\` +- \u9875\u811A\u5E73\u53F0\u54C1\u724C\u884C**\u5FC5\u987B**\u4F7F\u7528 \`data-mindspace-page-tag="platform-brand"\` \u6807\u8BB0\uFF0C\u663E\u793A\u4E3A **TKMind \xB7 \u667A\u8DA3**\uFF0C**\u7981\u6B62**\u4F7F\u7528\u90AE\u7BB1\u6216 \`tkmind.ai\` \`\`\`html -

\u{1F4E7} contact@tkmind.cn

+

TKMind \xB7 \u667A\u8DA3

\`\`\` - \u5E26 \`data-mindspace-page-tag\` \u7684\u533A\u57DF\u4E3A\u5E73\u53F0\u56FA\u5B9A\u4FE1\u606F\uFF1A\u7528\u6237\u5728\u7F16\u8F91\u6A21\u5F0F\u4E2D\u4E0D\u53EF\u89C1\u3001\u4E0D\u53EF\u6539\uFF1B\u9884\u89C8\u4E0E\u53D1\u5E03\u540E\u6B63\u5E38\u663E\u793A @@ -1379,6 +3574,7 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u - shell \u4EC5\u7528\u4E8E\u672C\u76EE\u5F55\u5185\u6574\u7406\u6587\u4EF6/\u7B80\u5355\u811A\u672C\uFF1B\u4E0D\u8981 \`rm -rf\` \u8D8A\u754C\u8DEF\u5F84\u3001\u4E0D\u8981\u5B89\u88C5\u7CFB\u7EDF\u7EA7\u4F9D\u8D56 - **\u7981\u6B62**\u5728 HTML \u4E2D\u7528 \`data:...;base64,...\` \u5185\u5D4C Word/PDF\uFF1B\u4E8C\u8FDB\u5236\u6587\u4EF6\u5355\u72EC\u843D\u76D8\u540E\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\uFF08\u89C1 \`docx-generate\` \u6280\u80FD\uFF09 - \u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF0C\u7981\u6B62\u8DF3\u8FC7 \`docx-generate\` \u76F4\u63A5\u5728 HTML \u4E2D\u4F2A\u9020\u4E0B\u8F7D\u94FE\u63A5 +- \u7528\u6237\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\u65F6\uFF0C\u7981\u6B62\u8DF3\u8FC7 \`long-image-download\` \u6216\u628A \`.thumbnail.svg\` \u5F53\u6210\u957F\u56FE ## \u793A\u4F8B @@ -1419,6 +3615,13 @@ ${renderBrandingBlock(addressName)} - \u82E5\u7528\u6237\u8981\u6C42\u9875\u9762\u91CC\u53EF\u4E0B\u8F7D Word / docx\uFF1A\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\` - \u5148\u751F\u6210\u5E76\u786E\u8BA4 \`public/*.docx\` \u5DF2\u5B58\u5728\uFF0C\u518D\u5199 \`public/*.html\` - HTML \u4E2D\u53EA\u80FD\u7528\u540C\u76EE\u5F55\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\uFF0C\u7981\u6B62\u53EA\u5199\u4E0B\u8F7D\u6309\u94AE\u5374\u6CA1\u6709\u5148\u628A \`.docx\` \u843D\u76D8 + +## \u957F\u56FE\u4E0B\u8F7D + +- \u82E5\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\uFF1A\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`long-image-download\` +- \u8C03\u7528 \`generate_long_image\` \u751F\u6210 \`public/<\u9875\u9762\u540D>.long.png\` +- \u5148\u786E\u8BA4 \`public/*.long.png\` \u5DF2\u5B58\u5728\uFF0C\u518D\u56DE\u590D \`[\u957F\u56FE\u9884\u89C8](...long.png)\` \u548C \`[\u4E0B\u8F7D\u957F\u56FE](...html?download=long-image)\` +- \`.thumbnail.svg\` \u53EA\u662F\u4FE1\u606F\u6D41\u5C01\u9762\uFF0C\u4E0D\u80FD\u5F53\u6210\u957F\u56FE `; } function buildSandboxSessionConstraints({ baseConstraints, developerTools = [] }) { @@ -1453,12 +3656,15 @@ function buildSandboxSessionConstraints({ baseConstraints, developerTools = [] } "## \u751F\u6210 / \u53D1\u5E03 HTML \u9875\u9762", "- \u4F60\u6709 write_file/edit_file \u5DE5\u5177\uFF1A**\u5FC5\u987B\u7531\u4F60**\u5199\u5165 `public/xxx.html`\uFF08\u6216\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55 `.html`\uFF09", "- \u5F00\u59CB\u524D\u6267\u884C load_skill \u2192 `static-page-publish`\uFF0C\u6309\u6280\u80FD\u8BF4\u660E\u5199\u5165 mindspace-cover \u5143\u6570\u636E", - "- \u5982\u679C\u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF1A\u5148 `load_skill` \u2192 `docx-generate`\uFF0C\u751F\u6210\u5E76\u786E\u8BA4 `public/*.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u5199 HTML \u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5", + "- \u9ED8\u8BA4\u53EA\u751F\u6210 HTML\uFF1B\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6", + "- \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF1A\u5148 `load_skill` \u2192 `docx-generate`\uFF0C\u751F\u6210\u5E76\u786E\u8BA4 `public/*.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u5199 HTML \u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5", + "- \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\u65F6\uFF1A\u5148 `load_skill` \u2192 `long-image-download`\uFF0C\u8C03\u7528 `generate_long_image` \u751F\u6210\u5E76\u786E\u8BA4 `public/*.long.png` \u5DF2\u5B58\u5728\uFF0C\u518D\u56DE\u590D\u957F\u56FE\u9884\u89C8\u4E0E\u4E0B\u8F7D\u94FE\u63A5", "- **\u7981\u6B62**\u7528 shell / cat / heredoc / echo / cp \u5199\u5165 HTML\uFF1Bshell \u5728\u5BB9\u5668\u5185\u6267\u884C\uFF0C\u6587\u4EF6\u4E0D\u4F1A\u51FA\u73B0\u5728\u516C\u7F51 MindSpace \u8DEF\u5F84", "- \u7528 `apps__create_app` \u8BBE\u8BA1\u9875\u9762\u65F6\uFF0C\u6700\u540E\u4ECD\u8981\u6309 `static-page-publish` skill \u628A\u5185\u5BB9 write_file \u843D\u5230 `public/`\uFF0C\u624D\u6709\u516C\u7F51\u94FE\u63A5", "- **\u7981\u6B62**\u8BA9\u7528\u6237\u624B\u52A8\u4FDD\u5B58\u5230 public \u6216\u8BF4\u65E0\u6CD5\u751F\u6210\u9875\u9762\uFF08\u9664\u975E write_file \u8C03\u7528\u5931\u8D25\uFF09", "- \u5B8C\u6210\u540E\u56DE\u590D `[\u9875\u9762\u6807\u9898](\u516C\u7F51URL)` \u53EF\u70B9\u51FB\u94FE\u63A5\uFF1B\u5199\u5165 `public/` \u65F6 URL \u5FC5\u987B\u542B `/public/` \u8DEF\u5F84\u6BB5\uFF1B\u6309\u672C\u4F1A\u8BDD\u7ED9\u51FA\u7684\u516C\u7F51\u524D\u7F00\u6A21\u677F\u62FC\u63A5\u771F\u5B9E\u5730\u5740", - "- \u4E0B\u8F7D\u9644\u4EF6\uFF1AWord \u7528 `generate_docx` \u5199\u5165 `public/*.docx`\uFF0C\u76F8\u5BF9\u94FE\u63A5\u6587\u4EF6\u5FC5\u987B\u5DF2\u5728 `public/` \u843D\u76D8\uFF0Cbasename \u4E0E HTML \u4E00\u81F4" + "- \u6309\u9700\u4E0B\u8F7D\u9644\u4EF6\uFF1AWord \u7528 `generate_docx` \u5199\u5165 `public/*.docx`\uFF0C\u76F8\u5BF9\u94FE\u63A5\u6587\u4EF6\u5FC5\u987B\u5DF2\u5728 `public/` \u843D\u76D8\uFF0Cbasename \u4E0E HTML \u4E00\u81F4", + "- \u6309\u9700\u957F\u56FE\u9644\u4EF6\uFF1A\u7528 `generate_long_image` \u5199\u5165 `public/*.long.png`\uFF0C\u4E0D\u8981\u628A `.thumbnail.svg` \u5F53\u6210\u957F\u56FE" ); return lines.join("\n"); } @@ -1476,7 +3682,9 @@ function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, di "- **\u7981\u6B62**\uFF1A\u8BBF\u95EE `assets/` \u5185\u90E8\u8DEF\u5F84\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\u4E3B\u673A\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u7981\u6B62\u7528\u516C\u7F51 URL \u5217\u76EE\u5F55\u6216\u8BFB CSV", "- **\u8DEF\u5F84\u89C4\u5219**\uFF1A\u53EA\u7528\u76F8\u5BF9\u8DEF\u5F84\uFF1B\u7981\u6B62 `../`\uFF1B\u5DE5\u4F5C\u533A\u5916\u7684\u8DEF\u5F84\u4F1A\u88AB\u7CFB\u7EDF\u62D2\u7EDD\uFF08OS \u5C42\u5F3A\u5236\uFF0C\u975E\u8F6F\u7EA6\u675F\uFF09", "- **\u751F\u6210\u9875\u9762\uFF08\u5FC5\u987B\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u5148 `load_skill` \u2192 `static-page-publish`\uFF0C\u518D\u7528 `write_file`/`edit_file` \u5199\u5165 `public/\u9875\u9762.html`", - "- **Word \u4E0B\u8F7D\u9875\uFF08\u5FC5\u987B\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u82E5\u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate`\uFF0C\u751F\u6210\u5E76\u786E\u8BA4 `public/*.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u5728 HTML \u4E2D\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5", + "- **\u9ED8\u8BA4\u53EA\u751F\u6210 HTML**\uFF1A\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6", + "- **Word \u4E0B\u8F7D\u9875\uFF08\u6309\u9700\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u82E5\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate`\uFF0C\u751F\u6210\u5E76\u786E\u8BA4 `public/*.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u5728 HTML \u4E2D\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5", + "- **\u957F\u56FE\u4E0B\u8F7D\uFF08\u6309\u9700\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u82E5\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `long-image-download`\uFF0C\u8C03\u7528 `generate_long_image` \u751F\u6210\u5E76\u786E\u8BA4 `public/*.long.png` \u5DF2\u5B58\u5728", "- **\u7528\u6237\u53EF\u89C1\u56DE\u590D**\uFF1A\u4E0D\u8981\u5411\u7528\u6237\u590D\u8FF0 load_skill\u3001\u6280\u80FD\u66F4\u65B0\u3001\u9875\u811A\u6807\u8BB0\u3001mindspace-cover \u7B49\u5185\u90E8\u5B9E\u73B0\uFF1B\u5B8C\u6210\u540E\u76F4\u63A5\u7ED9\u51FA\u9875\u9762\u94FE\u63A5\u6216\u7ED3\u679C", "- **\u7981\u6B62**\u7528 shell \u5199\u5165 HTML\uFF1B**\u7981\u6B62**\u8BA9\u7528\u6237\u300C\u624B\u52A8\u4FDD\u5B58\u5230 public \u76EE\u5F55\u300D\u6216\u8BF4\u300C\u6211\u65E0\u6CD5\u751F\u6210\u9875\u9762\u300D\u2014\u2014\u9664\u975E write_file \u5DF2\u5931\u8D25\u5E76\u62A5\u544A\u9519\u8BEF", "- \u5B8C\u6210\u540E\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 `[\u6807\u9898](URL)`\uFF1B\u5199\u5165 `public/\u9875\u9762.html` \u65F6 URL \u4E3A `.../MindSpace/<\u7528\u6237ID>/public/\u9875\u9762.html`", @@ -1484,37 +3692,47 @@ function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, di "- **\u9700\u8981\u67E5\u5B9E\u65F6/\u771F\u5B9E\u4E16\u754C\u4FE1\u606F\u65F6**\uFF1A\u5148 `load_skill` \u2192 `web`\uFF0C\u4F18\u5148 `web_search`/`fetch_url`\uFF1B\u56FD\u5185 google.com \u4E0D\u53EF\u8FBE\uFF0C\u591A\u6B21\u65E0\u679C\u65F6\u6539\u8D70 Bing/360 \u7B49\u53EF\u8FBE\u641C\u7D22\u6E90", "- **\u7981\u6B62**\u8BA9\u7528\u6237\u300C\u624B\u52A8\u4FDD\u5B58\u5230 public \u76EE\u5F55\u300D\u6216\u8BF4\u300C\u6211\u65E0\u6CD5\u751F\u6210\u9875\u9762\u300D\u2014\u2014\u9664\u975E write_file \u5DF2\u5931\u8D25\u5E76\u62A5\u544A\u9519\u8BEF", "- \u5B8C\u6210\u540E\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 `[\u6807\u9898](URL)`\uFF1B\u5199\u5165 `public/\u9875\u9762.html` \u65F6 URL \u4E3A `.../MindSpace/<\u7528\u6237ID>/public/\u9875\u9762.html`\uFF0C\u6309\u6A21\u677F\u62FC\u771F\u5B9E\u5730\u5740", - "- \u4E0B\u8F7D\u94FE\u63A5\uFF08\u5982 `report.docx`\uFF09\u5FC5\u987B\u4E0E HTML \u540C\u76EE\u5F55\u4E14\u6587\u4EF6\u540D\u4E00\u81F4\uFF1BWord \u5FC5\u987B\u7528 sandbox-fs `generate_docx` \u751F\u6210\uFF0C\u4E0D\u8981\u7528 `computercontroller` / shell \u4F5C\u4E3A\u4EA4\u4ED8\u4F9D\u636E", + "- \u6309\u9700\u4E0B\u8F7D\u94FE\u63A5\uFF08\u5982 `report.docx`\uFF09\u5FC5\u987B\u4E0E HTML \u540C\u76EE\u5F55\u4E14\u6587\u4EF6\u540D\u4E00\u81F4\uFF1BWord \u5FC5\u987B\u7528 sandbox-fs `generate_docx` \u751F\u6210\uFF0C\u4E0D\u8981\u7528 `computercontroller` / shell \u4F5C\u4E3A\u4EA4\u4ED8\u4F9D\u636E", + "- \u6309\u9700\u957F\u56FE\u94FE\u63A5\uFF1A\u751F\u6210 `report.long.png` \u540E\u7ED9 `[\u957F\u56FE\u9884\u89C8](.../report.long.png)`\uFF0C\u4E0B\u8F7D\u7528 `[\u4E0B\u8F7D\u957F\u56FE](.../report.html?download=long-image)`", `- \u53D1\u5E03\u6280\u80FD\uFF1A\`${PUBLISH_SKILL_NAME}\`\uFF08\u751F\u6210\u9875\u9762\u524D\u5E94 load_skill\uFF09` ].join("\n"); } function ensureWorkspaceHintsInstalled(publishDir, context) { - const hintsPath = path3.join(publishDir, WORKSPACE_HINTS_FILENAME); - const legacyPath = path3.join(publishDir, LEGACY_WORKSPACE_HINTS_FILENAME); + const hintsPath = path5.join(publishDir, WORKSPACE_HINTS_FILENAME); + const legacyPath = path5.join(publishDir, LEGACY_WORKSPACE_HINTS_FILENAME); const content = renderWorkspaceHints(context); - fs.writeFileSync(hintsPath, content, "utf8"); - if (fs.existsSync(legacyPath)) { - fs.unlinkSync(legacyPath); + fs3.writeFileSync(hintsPath, content, "utf8"); + if (fs3.existsSync(legacyPath)) { + fs3.unlinkSync(legacyPath); } return hintsPath; } function ensureDocxSkillInstalled(publishDir) { - if (!fs.existsSync(DOCX_SKILL_DIR)) return null; - const skillRoot = path3.join(publishDir, ".agents", "skills", DOCX_SKILL_NAME); - fs.rmSync(skillRoot, { recursive: true, force: true }); - fs.mkdirSync(path3.dirname(skillRoot), { recursive: true }); - fs.cpSync(DOCX_SKILL_DIR, skillRoot, { recursive: true }); - return path3.join(skillRoot, "SKILL.md"); + if (!fs3.existsSync(DOCX_SKILL_DIR)) return null; + const skillRoot = path5.join(publishDir, ".agents", "skills", DOCX_SKILL_NAME); + fs3.rmSync(skillRoot, { recursive: true, force: true }); + fs3.mkdirSync(path5.dirname(skillRoot), { recursive: true }); + fs3.cpSync(DOCX_SKILL_DIR, skillRoot, { recursive: true }); + return path5.join(skillRoot, "SKILL.md"); +} +function ensureLongImageSkillInstalled(publishDir) { + if (!fs3.existsSync(LONG_IMAGE_SKILL_DIR)) return null; + const skillRoot = path5.join(publishDir, ".agents", "skills", LONG_IMAGE_SKILL_NAME); + fs3.rmSync(skillRoot, { recursive: true, force: true }); + fs3.mkdirSync(path5.dirname(skillRoot), { recursive: true }); + fs3.cpSync(LONG_IMAGE_SKILL_DIR, skillRoot, { recursive: true }); + return path5.join(skillRoot, "SKILL.md"); } function ensurePublishSkillInstalled(publishDir, context) { ensureDocxSkillInstalled(publishDir); - const skillRoot = path3.join(publishDir, ".agents", "skills", PUBLISH_SKILL_NAME); - fs.mkdirSync(skillRoot, { recursive: true }); - const skillPath = path3.join(skillRoot, "SKILL.md"); + ensureLongImageSkillInstalled(publishDir); + const skillRoot = path5.join(publishDir, ".agents", "skills", PUBLISH_SKILL_NAME); + fs3.mkdirSync(skillRoot, { recursive: true }); + const skillPath = path5.join(skillRoot, "SKILL.md"); const content = renderPublishSkill(context); - const existing = fs.existsSync(skillPath) ? fs.readFileSync(skillPath, "utf8") : ""; + const existing = fs3.existsSync(skillPath) ? fs3.readFileSync(skillPath, "utf8") : ""; if (existing !== content) { - fs.writeFileSync(skillPath, content, "utf8"); + fs3.writeFileSync(skillPath, content, "utf8"); } return skillPath; } @@ -1530,7 +3748,7 @@ function ensureUserPublishLayout({ const username = user.username ? resolveUsernameSlug(user) : slug; const displayName = user.displayName ?? user.display_name ?? null; const publishDir = resolvePublishDir(h5Root, user); - fs.mkdirSync(path3.join(publishDir, "assets"), { recursive: true }); + fs3.mkdirSync(path5.join(publishDir, "assets"), { recursive: true }); const context = { slug, username, displayName, publicBaseUrl, publishDir }; if (installWorkspaceHints) { ensureWorkspaceHintsInstalled(publishDir, context); @@ -1549,8 +3767,8 @@ function ensureUserPublishLayout({ } // user-memory-profile.mjs -import fs2 from "node:fs"; -import path4 from "node:path"; +import fs4 from "node:fs"; +import path6 from "node:path"; var USER_MEMORY_PROFILE_FILENAME = ".tkmind-profile.json"; var USER_MEMORY_PROFILE_VERSION = 1; var DEFAULT_TIMEZONE = "Asia/Shanghai"; @@ -1574,13 +3792,13 @@ function buildInitialUserMemoryProfile({ }; } function resolveUserMemoryProfilePath(workspaceRoot) { - return path4.join(workspaceRoot, USER_MEMORY_PROFILE_FILENAME); + return path6.join(workspaceRoot, USER_MEMORY_PROFILE_FILENAME); } function loadUserMemoryProfile(workspaceRoot) { const profilePath = resolveUserMemoryProfilePath(workspaceRoot); - if (!fs2.existsSync(profilePath)) return null; + if (!fs4.existsSync(profilePath)) return null; try { - const raw = fs2.readFileSync(profilePath, "utf8"); + const raw = fs4.readFileSync(profilePath, "utf8"); const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return null; return parsed; @@ -1600,7 +3818,7 @@ function ensureUserMemoryProfile(workspaceRoot, context) { displayName: nextDisplayName, updatedAt: Date.now() }; - fs2.writeFileSync(profilePath, `${JSON.stringify(updated, null, 2)} + fs4.writeFileSync(profilePath, `${JSON.stringify(updated, null, 2)} `, "utf8"); return updated; } @@ -1612,7 +3830,7 @@ function ensureUserMemoryProfile(workspaceRoot, context) { username: context.username, slug: context.slug }); - fs2.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)} + fs4.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)} `, "utf8"); return profile; } @@ -1958,7 +4176,7 @@ function renderCodeExecutorGuidance(sessionPolicy) { } // session-reconcile.mjs -import path5 from "node:path"; +import path7 from "node:path"; function extensionName(config) { return config?.name ?? null; } @@ -2004,7 +4222,7 @@ function extensionsNeedingRefresh(currentExtensions, desiredExtensions) { } function samePath(left, right) { if (!left || !right) return false; - return path5.resolve(left) === path5.resolve(right); + return path7.resolve(left) === path7.resolve(right); } async function readJson(upstream) { const text = await upstream.text(); @@ -2134,15 +4352,15 @@ async function reconcileAgentSession(apiFetch2, sessionId, { } // imgproxy-signer.mjs -import crypto3 from "node:crypto"; +import crypto4 from "node:crypto"; function createImgproxySigner(key, salt) { const keyBuf = Buffer.from(key, "hex"); const saltBuf = Buffer.from(salt, "hex"); - function signPath(path11) { - const signaturePayload = Buffer.concat([saltBuf, Buffer.from(path11)]); - const signature = crypto3.createHmac("sha256", keyBuf).update(signaturePayload).digest(); + function signPath(path13) { + const signaturePayload = Buffer.concat([saltBuf, Buffer.from(path13)]); + const signature = crypto4.createHmac("sha256", keyBuf).update(signaturePayload).digest(); const signatureB64 = signature.toString("base64url"); - return `/${signatureB64}${path11}`; + return `/${signatureB64}${path13}`; } function buildUrl(baseUrl, storagePath, preset = "display") { if (!baseUrl || !storagePath) { @@ -2179,7 +4397,7 @@ function createImgproxySigner(key, salt) { } // tkmind-proxy.mjs -var insecureDispatcher = new Agent({ +var insecureDispatcher2 = new Agent2({ connect: { rejectUnauthorized: false } }); function isHttpsTarget(target) { @@ -2227,10 +4445,10 @@ async function apiFetch(target, apiSecret, pathname, init = {}) { if (init.body && !headers["Content-Type"]) { headers["Content-Type"] = "application/json"; } - return undiciFetch(url, { + return undiciFetch2(url, { ...init, headers, - dispatcher: isHttpsTarget(target) ? insecureDispatcher : void 0 + dispatcher: isHttpsTarget(target) ? insecureDispatcher2 : void 0 }); } function createRuntimeRouter({ @@ -2414,7 +4632,12 @@ function createRuntimeRouter({ key("worker", workerId, "metrics_sampled_at"), key("worker", workerId, "last_first_token_ms"), key("worker", workerId, "last_first_token_at"), - key("worker", workerId, "first_token_count") + key("worker", workerId, "first_token_count"), + key("worker", workerId, "heartbeat_source"), + key("worker", workerId, "heartbeat_ok"), + key("worker", workerId, "heartbeat_status_code"), + key("worker", workerId, "heartbeat_latency_ms"), + key("worker", workerId, "heartbeat_error") ]).catch(() => []) : []; const firstToken5m = await firstTokenWindowStats(client, workerId, 5 * 60 * 1e3); const firstToken1h = await firstTokenWindowStats(client, workerId, 60 * 60 * 1e3); @@ -2442,6 +4665,11 @@ function createRuntimeRouter({ lastFirstTokenMs: readNumber(values?.[18]), lastFirstTokenAt: values?.[19] ? Number(values[19]) : null, firstTokenCount: readNumber(values?.[20]), + heartbeatSource: values?.[21] ?? null, + heartbeatOk: values?.[22] == null ? null : /^(1|true|yes)$/i.test(String(values[22])), + heartbeatStatusCode: readNumber(values?.[23]), + heartbeatLatencyMs: readNumber(values?.[24]), + heartbeatError: values?.[25] || null, firstToken5m, firstToken1h, score: workerScoreFromValues(values) @@ -2528,10 +4756,10 @@ function publicHtmlExistsForUser(owner, relativePath, currentUser) { if (normalizedOwner !== normalizedUserId && normalizedOwner !== normalizedUsername) return true; const normalizedRelativePath = normalizeStaticHtmlRelativePath(relativePath); if (!normalizedRelativePath || !normalizedRelativePath.toLowerCase().endsWith(".html")) return false; - const root2 = path6.resolve(process.cwd(), PUBLISH_ROOT_DIR, normalizedOwner); - const target = path6.resolve(root2, normalizedRelativePath); - if (target !== root2 && !target.startsWith(`${root2}${path6.sep}`)) return false; - return fs3.existsSync(target) && fs3.statSync(target).isFile(); + const root2 = path8.resolve(process.cwd(), PUBLISH_ROOT_DIR, normalizedOwner); + const target = path8.resolve(root2, normalizedRelativePath); + if (target !== root2 && !target.startsWith(`${root2}${path8.sep}`)) return false; + return fs5.existsSync(target) && fs5.statSync(target).isFile(); } function sanitizeOwnPublicHtmlUrl(publicUrl, owner, rawRelativePath, currentUser) { const relativePath = decodePathSegment(rawRelativePath); @@ -2539,7 +4767,7 @@ function sanitizeOwnPublicHtmlUrl(publicUrl, owner, rawRelativePath, currentUser if (!publicHtmlExistsForUser(owner, canonicalRelativePath, currentUser)) { return { ok: false, - notice: buildMissingPublicHtmlNotice(path6.posix.basename(canonicalRelativePath || relativePath)) + notice: buildMissingPublicHtmlNotice(path8.posix.basename(canonicalRelativePath || relativePath)) }; } return { @@ -2719,11 +4947,11 @@ async function buildVisionPayload({ userId, publishLayout, localFetchAsset, - llmProviderService, + llmProviderService: llmProviderService2, imgproxySigner = null }) { void imgproxySigner; - if (!llmProviderService || !userId) return null; + if (!llmProviderService2 || !userId) return null; const rawImageUrls = extractImageUrlsFromMessage(userMessage); if (rawImageUrls.length === 0) return null; const originalDisplayText = typeof userMessage?.metadata?.displayText === "string" && userMessage.metadata.displayText.trim() ? userMessage.metadata.displayText : Array.isArray(userMessage?.content) ? userMessage.content.filter((item) => item?.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim() : ""; @@ -2742,7 +4970,7 @@ async function buildVisionPayload({ return rawUrl; })(); try { - const response = await undiciFetch(fetchableUrl); + const response = await undiciFetch2(fetchableUrl); if (!response.ok) { throw new Error(`Vision fetch failed: ${response.status}`); } @@ -2777,7 +5005,7 @@ async function buildVisionPayload({ } } if (imageItems.length === 0) return null; - const visionDescription = await llmProviderService.analyzeImagesWithVision(imageItems, "\u8BF7\u8BE6\u7EC6\u63CF\u8FF0\u56FE\u7247\u7684\u89C6\u89C9\u5185\u5BB9\uFF1A\u4E3B\u4F53\u3001\u989C\u8272\u3001\u98CE\u683C\u3001\u6784\u56FE\uFF0C\u4E0D\u8981\u751F\u6210\u4EE3\u7801\u6216\u9875\u9762\u65B9\u6848").catch(() => null); + const visionDescription = await llmProviderService2.analyzeImagesWithVision(imageItems, "\u8BF7\u8BE6\u7EC6\u63CF\u8FF0\u56FE\u7247\u7684\u89C6\u89C9\u5185\u5BB9\uFF1A\u4E3B\u4F53\u3001\u989C\u8272\u3001\u98CE\u683C\u3001\u6784\u56FE\uFF0C\u4E0D\u8981\u751F\u6210\u4EE3\u7801\u6216\u9875\u9762\u65B9\u6848").catch(() => null); const pathList = imageItems.map((item, i) => `\u56FE\u7247${i + 1}: \u56FE\u7247${i + 1}`).join(" "); const publicUrlPrefix = publishLayout?.publicUrl ? `${String(publishLayout.publicUrl).replace(/\/$/, "")}/public/` : null; const injectedNote = "\n\n\u3010TKMind \u56FE\u7247\u5206\u6790\u7ED3\u679C \u2014 \u4EC5\u4F9B\u6267\u884C\u53C2\u8003\uFF0C\u4E0D\u8981\u5411\u7528\u6237\u590D\u8FF0\u6B64\u6BB5\u5185\u5BB9\u3011\n" + (visionDescription ? `Qwen VL \u56FE\u7247\u63CF\u8FF0\uFF1A @@ -2824,7 +5052,7 @@ function createTkmindProxy({ apiTargets: apiTargets2, apiSecret, userAuth: userAuth2, - llmProviderService, + llmProviderService: llmProviderService2, localFetchAsset, subscriptionService, sessionSnapshotService, @@ -3080,10 +5308,10 @@ function createTkmindProxy({ } } async function applySessionLlmProvider(sessionId) { - if (!llmProviderService || !sessionId) return null; + if (!llmProviderService2 || !sessionId) return null; try { const target = await resolveTarget(sessionId); - return await llmProviderService.applyBestProviderForSession( + return await llmProviderService2.applyBestProviderForSession( sessionId, (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init) ); @@ -3096,18 +5324,18 @@ function createTkmindProxy({ } } async function applyLocalFallbackForSession(sessionId) { - if (!llmProviderService || !sessionId) return null; + if (!llmProviderService2 || !sessionId) return null; const target = await resolveTarget(sessionId); - return llmProviderService.applyLocalFallbackForSession( + return llmProviderService2.applyLocalFallbackForSession( sessionId, (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init) ); } const visionActiveSessions = /* @__PURE__ */ new Set(); async function applyVisionProviderForSession(sessionId) { - if (!llmProviderService || !sessionId) return null; + if (!llmProviderService2 || !sessionId) return null; const target = await resolveTarget(sessionId); - return llmProviderService.applyVisionProviderForSession( + return llmProviderService2.applyVisionProviderForSession( sessionId, (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init) ); @@ -3118,7 +5346,7 @@ function createTkmindProxy({ userId, publishLayout, localFetchAsset, - llmProviderService, + llmProviderService: llmProviderService2, imgproxySigner }); } @@ -3169,7 +5397,7 @@ function createTkmindProxy({ const user = await userAuth2.getUserById(userId); if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728"); let finalUserMessage = userMessage; - if (llmProviderService && messageHasImages(userMessage) && await llmProviderService.hasVisionKey()) { + if (llmProviderService2 && messageHasImages(userMessage) && await llmProviderService2.hasVisionKey()) { const publishLayout = await userAuth2.getUserPublishLayout(userId).catch(() => null); const visionResult = await buildVisionBody(userMessage, userId, publishLayout).catch(() => null); if (visionResult?.userMessage) { @@ -3664,11 +5892,11 @@ function createTkmindProxy({ await userAuth2.getAgentSessionPolicy(req.currentUser.id) ); } - if (isReplyPath && llmProviderService) { + if (isReplyPath && llmProviderService2) { const sessionId = sessionMatch?.[1]; if (sessionId) { const hasImages = messageHasImages(req.body?.user_message); - if (hasImages && await llmProviderService.hasVisionKey()) { + if (hasImages && await llmProviderService2.hasVisionKey()) { const publishLayout = await userAuth2.getUserPublishLayout(req.currentUser?.id).catch(() => null); const visionResult = await buildVisionBody( baseBody?.user_message ?? req.body?.user_message, @@ -3750,10 +5978,193 @@ function createTkmindProxy({ }; } +// tool-gateway.mjs +import { spawn as nodeSpawn } from "node:child_process"; +import { EventEmitter } from "node:events"; +var CODE_EXECUTORS = /* @__PURE__ */ new Set(["aider", "openhands"]); +var DEFAULT_STDIO_LIMIT = 64 * 1024; +function envFlag2(value, fallback = false) { + const raw = String(value ?? "").trim().toLowerCase(); + if (!raw) return fallback; + return ["1", "true", "yes", "on"].includes(raw); +} +function positiveInteger2(value, fallback) { + const n = Number(value); + if (!Number.isFinite(n) || n <= 0) return fallback; + return Math.floor(n); +} +function normalizeExecutor2(value, fallback = "aider") { + const normalized = String(value ?? fallback).trim().toLowerCase(); + if (CODE_EXECUTORS.has(normalized)) return normalized; + return fallback; +} +function csvSet(value) { + return new Set( + String(value ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean) + ); +} +function appendLimited(current, chunk, limit) { + const next = `${current}${Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk ?? "")}`; + if (next.length <= limit) return next; + return next.slice(next.length - limit); +} +function extractToolInstruction(userMessage) { + const content = userMessage?.content; + if (typeof content === "string") return content.trim(); + if (!Array.isArray(content)) { + return String(userMessage?.text ?? userMessage?.value ?? "").trim(); + } + return content.map((item) => { + if (typeof item === "string") return item; + if (item?.type === "text") return item.text ?? ""; + return ""; + }).map((item) => String(item ?? "").trim()).filter(Boolean).join("\n").trim(); +} +function createToolGateway({ + llmProviderService: llmProviderService2, + env = process.env, + spawnImpl = nodeSpawn +} = {}) { + const enabled = envFlag2(env.MEMIND_TOOL_GATEWAY_ENABLED, false); + const dryRun = envFlag2(env.MEMIND_TOOL_GATEWAY_DRY_RUN, false); + const defaultExecutor = normalizeExecutor2(env.MEMIND_TOOL_GATEWAY_DEFAULT_EXECUTOR, "aider"); + const openhandsTaskTypes = csvSet( + env.MEMIND_TOOL_GATEWAY_OPENHANDS_TASK_TYPES ?? "repo_refactor,multi_file,complex_repo" + ); + const stdioLimit = positiveInteger2(env.MEMIND_TOOL_GATEWAY_STDIO_LIMIT, DEFAULT_STDIO_LIMIT); + function getStatus() { + return { + enabled, + dryRun, + protocol: "agent-run-v1", + executors: ["aider", "openhands"], + defaultExecutor, + openhandsTaskTypes: [...openhandsTaskTypes] + }; + } + function selectExecutor({ userMessage, taskType } = {}) { + const metadata = userMessage?.metadata ?? {}; + const runMetadata = metadata.memindRun ?? metadata.agentRun ?? {}; + const requested = normalizeExecutor2(runMetadata.executor, ""); + if (requested) return requested; + const normalizedTaskType = String(taskType ?? runMetadata.taskType ?? "").trim().toLowerCase(); + if (openhandsTaskTypes.has(normalizedTaskType)) return "openhands"; + return defaultExecutor; + } + async function executeJob({ + runId, + requestId, + userId, + userMessage, + taskType, + cwd, + timeoutMs = 15 * 60 * 1e3 + } = {}) { + if (!enabled) { + throw new Error("Tool Gateway is disabled"); + } + if (!llmProviderService2?.getExecutorLaunchPlan) { + throw new Error("Tool Gateway missing llm provider service"); + } + const instruction = extractToolInstruction(userMessage); + if (!instruction) { + throw new Error("Tool Gateway job missing instruction"); + } + const executor = selectExecutor({ userMessage, taskType }); + const plan = await llmProviderService2.getExecutorLaunchPlan(executor, { + cwd, + mode: "headless", + instruction, + purpose: "default", + includeSecret: true + }); + if (!plan?.ok) { + throw new Error(plan?.message ?? `Tool Gateway launch plan unavailable for ${executor}`); + } + if (dryRun) { + return { + ok: true, + dryRun: true, + executor, + protocol: "agent-run-v1", + cwd: plan.cwd, + command: plan.command, + args: plan.args ?? [], + runId, + requestId, + userId + }; + } + return await new Promise((resolve, reject) => { + let stdout = ""; + let stderr = ""; + const child = spawnImpl(plan.command, plan.args ?? [], { + cwd: plan.cwd, + env: { ...process.env, ...plan.env ?? {} }, + stdio: ["ignore", "pipe", "pipe"] + }); + const cleanup = () => { + if (timer) clearTimeout(timer); + }; + const timer = setTimeout(() => { + child.kill("SIGTERM"); + const err = new Error(`Tool Gateway job timed out after ${timeoutMs}ms`); + err.code = "TOOL_GATEWAY_TIMEOUT"; + reject(err); + }, timeoutMs); + if (child.stdout instanceof EventEmitter) { + child.stdout.on("data", (chunk) => { + stdout = appendLimited(stdout, chunk, stdioLimit); + }); + } + if (child.stderr instanceof EventEmitter) { + child.stderr.on("data", (chunk) => { + stderr = appendLimited(stderr, chunk, stdioLimit); + }); + } + child.on("error", (err) => { + cleanup(); + reject(err); + }); + child.on("exit", (code, signal) => { + cleanup(); + if (code === 0) { + resolve({ + ok: true, + executor, + protocol: "agent-run-v1", + cwd: plan.cwd, + command: plan.command, + args: plan.args ?? [], + exitCode: code, + signal: signal ?? null, + stdout, + stderr + }); + return; + } + const err = new Error(`Tool Gateway executor ${executor} exited with ${signal ?? code}`); + err.code = "TOOL_GATEWAY_EXECUTOR_FAILED"; + err.executor = executor; + err.exitCode = code; + err.signal = signal ?? null; + err.stdout = stdout; + err.stderr = stderr; + reject(err); + }); + }); + } + return { + getStatus, + selectExecutor, + executeJob + }; +} + // user-auth.mjs -import crypto4 from "node:crypto"; -import fs6 from "node:fs"; -import path9 from "node:path"; +import crypto5 from "node:crypto"; +import fs8 from "node:fs"; +import path11 from "node:path"; import { Algorithm as Argon2Algorithm, hashRawSync as argon2HashRawSync } from "@node-rs/argon2"; // billing.mjs @@ -3841,11 +6252,11 @@ function buildInsufficientBalancePayload(balanceCents, config = loadRechargeConf } // user-space.mjs -import fs4 from "node:fs"; -import path7 from "node:path"; +import fs6 from "node:fs"; +import path9 from "node:path"; var UPLOAD_ZONE_CODES = ["oa", "public"]; function resolveMindspaceStorageRoot(h5Root, env = process.env) { - return path7.resolve(env.MINDSPACE_STORAGE_ROOT ?? path7.join(h5Root, "data", "mindspace")); + return path9.resolve(env.MINDSPACE_STORAGE_ROOT ?? path9.join(h5Root, "data", "mindspace")); } var WORKSPACE_HINTS_FILENAME2 = ".tkmindhints"; var LEGACY_WORKSPACE_HINTS_FILENAME2 = ".goosehints"; @@ -3861,32 +6272,32 @@ function renderUserSpaceBrandingBlock(userAddressName) { `; } function resolveZoneDir(workspaceRoot, categoryCode) { - return path7.join(workspaceRoot, categoryCode); + return path9.join(workspaceRoot, categoryCode); } function resolveZoneFilePath(workspaceRoot, categoryCode, filename) { - return path7.join(resolveZoneDir(workspaceRoot, categoryCode), filename); + return path9.join(resolveZoneDir(workspaceRoot, categoryCode), filename); } function zoneLabel(categoryCode) { return SYSTEM_CATEGORIES.find((item) => item.code === categoryCode)?.name ?? categoryCode; } function ensureUserZoneDirs(workspaceRoot) { for (const code of UPLOAD_ZONE_CODES) { - fs4.mkdirSync(resolveZoneDir(workspaceRoot, code), { recursive: true }); + fs6.mkdirSync(resolveZoneDir(workspaceRoot, code), { recursive: true }); } } function mirrorAssetToZone({ workspaceRoot, categoryCode, filename, sourcePath }) { if (!UPLOAD_ZONE_CODES.includes(categoryCode)) return null; - if (!sourcePath || !fs4.existsSync(sourcePath)) return null; + if (!sourcePath || !fs6.existsSync(sourcePath)) return null; ensureUserZoneDirs(workspaceRoot); const dest = resolveZoneFilePath(workspaceRoot, categoryCode, filename); - fs4.mkdirSync(path7.dirname(dest), { recursive: true }); - fs4.copyFileSync(sourcePath, dest); + fs6.mkdirSync(path9.dirname(dest), { recursive: true }); + fs6.copyFileSync(sourcePath, dest); return dest; } function isPathInsideUserWorkspace(workspaceRoot, requestedPath) { - const base = path7.resolve(workspaceRoot); - const resolved = path7.resolve(requestedPath); - return resolved === base || resolved.startsWith(`${base}${path7.sep}`); + const base = path9.resolve(workspaceRoot); + const resolved = path9.resolve(requestedPath); + return resolved === base || resolved.startsWith(`${base}${path9.sep}`); } function renderUserSpaceHints({ username, workspaceRoot, displayName, slug }) { const addressName = resolveUserAddressName({ displayName, username, slug }); @@ -3917,8 +6328,9 @@ ${zoneLines.join("\n")} - \u7528\u6237\u8981\u7F51\u9875 / HTML / \u62A5\u544A / \u5206\u4EAB\u94FE\u63A5\u65F6\uFF1A**\u4F60\u5FC5\u987B\u4EB2\u81EA\u5B8C\u6210**\uFF0C\u4E0D\u8981\u63A8\u7ED9\u7528\u6237\u624B\u52A8\u64CD\u4F5C - \u5148 \`load_skill\` \u2192 \`static-page-publish\`\uFF0C\u518D\u4F7F\u7528 \`write_file\` \u521B\u5EFA \`public/\u9875\u9762\u540D.html\`\uFF08\u9700\u8981\u8C03\u6574\u5DF2\u6709\u9875\u9762\u65F6\u7528 \`edit_file\`\uFF09 -- \u5982\u679C\u9875\u9762\u91CC\u8FD8\u8981\u63D0\u4F9B Word / docx \u4E0B\u8F7D\uFF1A\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\`\uFF0C\u751F\u6210 \`public/\u6587\u4EF6\u540D.docx\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5199 HTML \u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863 -- \u9875\u9762\u9700\u8981 Word / docx \u4E0B\u8F7D\u65F6\uFF0C\u5FC5\u987B\u7528 sandbox-fs \u7684 \`generate_docx\` \u5199\u5165 \`public/*.docx\`\uFF0C\u4E0D\u8981\u7528 shell/computercontroller \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6 +- \u9ED8\u8BA4\u53EA\u751F\u6210 HTML\uFF1B\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6 +- \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF1A\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\`\uFF0C\u751F\u6210 \`public/\u6587\u4EF6\u540D.docx\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5199 HTML \u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863 +- \u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF0C\u5FC5\u987B\u7528 sandbox-fs \u7684 \`generate_docx\` \u5199\u5165 \`public/*.docx\`\uFF0C\u4E0D\u8981\u7528 shell/computercontroller \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6 - \u82E5\u7528 \`apps__create_app\` \u8BBE\u8BA1/\u9884\u89C8\u9875\u9762\uFF1A\u8FD9\u4E00\u6B65\u53EA\u662F\u5728 Apps \u7A97\u53E3\u5185\u521B\u5EFA\u4EA4\u4E92\u5F0F App\uFF0C**\u8FD8\u6CA1\u6709\u516C\u7F51\u94FE\u63A5**\uFF1B\u5FC5\u987B\u7D27\u63A5\u7740\u6309 \`static-page-publish\` skill \u628A\u6700\u7EC8\u5185\u5BB9 \`write_file\` \u843D\u5230 \`public/*.html\`\uFF0C\u624D\u80FD\u7ED9\u51FA\u771F\u5B9E\u53EF\u8BBF\u95EE\u7684\u94FE\u63A5 - \u5199\u5165 \`\` \u7684 **mindspace-cover** \u5143\u6570\u636E\uFF08\u8BE6\u89C1 \`.agents/skills/static-page-publish/SKILL.md\`\uFF09 - \u4FDD\u5B58\u540E\u7ACB\u5373\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 \`[\u6807\u9898](URL)\`\uFF1B\u5199\u5165 \`public/\u9875\u9762.html\` \u65F6 URL \u4E3A \`.../MindSpace/<\u7528\u6237ID>/public/\u9875\u9762.html\` @@ -3947,20 +6359,21 @@ function buildUserSpaceConstraints({ username, workspaceRoot, publicBaseUrl, slu "- **\u7981\u6B62**\uFF1A\u8BBF\u95EE\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001MindSpace \u6839\u76EE\u5F55\u3001data/mindspace \u5185\u90E8\u8DEF\u5F84\u3001\u4E3B\u673A\u7EDD\u5BF9\u8DEF\u5F84", "- **\u7981\u6B62**\u7528\u516C\u7F51 URL \u5217\u76EE\u5F55\u6216\u8BFB CSV\uFF1B\u751F\u6210 HTML \u5199\u5165 `public/` \u5E76\u7ED9\u51FA\u516C\u7F51\u94FE\u63A5", "- **\u751F\u6210\u9875\u9762**\uFF1A\u5148 `load_skill` \u2192 `static-page-publish`\uFF0C\u518D\u7528 `write_file` / `edit_file` \u5199\u5165 `public/*.html`\uFF1B**\u7981\u6B62**\u8BA9\u7528\u6237\u624B\u52A8\u4FDD\u5B58\u6587\u4EF6", - "- **Word \u4E0B\u8F7D\u9875**\uFF1A\u82E5\u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate` \u751F\u6210 `public/*.docx` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5728 HTML \u91CC\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863", + "- **\u9ED8\u8BA4\u53EA\u751F\u6210 HTML**\uFF1A\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6", + "- **Word \u4E0B\u8F7D\u9875**\uFF1A\u82E5\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate` \u751F\u6210 `public/*.docx` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5728 HTML \u91CC\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863", "- \u7528 `apps__create_app` \u8BBE\u8BA1\u9875\u9762\u65F6\uFF0C\u6700\u540E\u4E00\u6B65\u4ECD\u8981\u6309 `static-page-publish` skill \u628A\u5185\u5BB9 `write_file` \u843D\u5230 `public/*.html`\uFF0C\u518D\u7ED9\u51FA\u4E0B\u65B9\u524D\u7F00\u62FC\u51FA\u7684\u771F\u5B9E\u94FE\u63A5\uFF0C\u4E0D\u8981\u505C\u5728 App \u9636\u6BB5\u5C31\u56DE\u590D\u94FE\u63A5", "- **\u9700\u8981\u67E5\u5B9E\u65F6/\u771F\u5B9E\u4E16\u754C\u4FE1\u606F\uFF08\u5982\u673A\u6784\u540D\u5355\u3001\u65B0\u95FB\u3001\u884C\u60C5\uFF09\u65F6**\uFF1A\u5148 `load_skill` \u2192 `web`\uFF0C\u4F18\u5148\u7528\u5176 `web_search`/`fetch_url`\uFF1B\u56FD\u5185\u73AF\u5883\u4E0B google.com \u4E0D\u53EF\u8FBE\uFF0C`web_search` \u591A\u6B21\u65E0\u679C\u65F6\u6539\u8D70 Bing/360 \u7B49\u53EF\u8FBE\u641C\u7D22\u6E90\uFF0C\u907F\u514D\u5BF9\u53CD\u722C\u7AD9\u70B9\u53CD\u590D\u786C\u6293", - "- **Word \u4E0B\u8F7D\u9875**\uFF1A\u82E5\u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate` \u751F\u6210 `public/*.docx` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5728 HTML \u91CC\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\uFF1B\u4E0D\u8981\u7528 shell/computercontroller \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6", + "- **Word \u4E0B\u8F7D\u9875**\uFF1A\u82E5\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate` \u751F\u6210 `public/*.docx` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5728 HTML \u91CC\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\uFF1B\u4E0D\u8981\u7528 shell/computercontroller \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6", publicBaseUrl && slug ? `- \u516C\u7F51 HTML \u524D\u7F00\uFF08\u516C\u5F00\u533A\uFF09\uFF1A\`${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${slug}/public/\`\uFF08\u5199\u5165 \`public/\u9875\u9762.html\` \u65F6\u5206\u4EAB\u94FE\u63A5\u5FC5\u987B\u542B \`public/\`\uFF09` : null ].filter(Boolean).join("\n"); } function ensureUserSpaceHints(workspaceRoot, context) { - const hintsPath = path7.join(workspaceRoot, WORKSPACE_HINTS_FILENAME2); - const legacyPath = path7.join(workspaceRoot, LEGACY_WORKSPACE_HINTS_FILENAME2); + const hintsPath = path9.join(workspaceRoot, WORKSPACE_HINTS_FILENAME2); + const legacyPath = path9.join(workspaceRoot, LEGACY_WORKSPACE_HINTS_FILENAME2); const content = renderUserSpaceHints({ ...context, workspaceRoot }); - fs4.writeFileSync(hintsPath, content, "utf8"); - if (fs4.existsSync(legacyPath)) { - fs4.unlinkSync(legacyPath); + fs6.writeFileSync(hintsPath, content, "utf8"); + if (fs6.existsSync(legacyPath)) { + fs6.unlinkSync(legacyPath); } return hintsPath; } @@ -3976,7 +6389,7 @@ async function syncUserZonesFromAssets(pool2, storageRoot, userId, workspaceRoot [userId, ...UPLOAD_ZONE_CODES] ); for (const row of rows) { - const sourcePath = path7.join(storageRoot, row.storage_key); + const sourcePath = path9.join(storageRoot, row.storage_key); mirrorAssetToZone({ workspaceRoot, categoryCode: row.category_code, @@ -4018,10 +6431,10 @@ async function ensureUserSpaceLayout({ } // skills-registry.mjs -import fs5 from "node:fs"; -import path8 from "node:path"; +import fs7 from "node:fs"; +import path10 from "node:path"; import { fileURLToPath as fileURLToPath4 } from "node:url"; -var __dirname3 = path8.dirname(fileURLToPath4(import.meta.url)); +var __dirname3 = path10.dirname(fileURLToPath4(import.meta.url)); var DEFAULT_USER_SKILLS = { web: true, search: true, @@ -4030,6 +6443,7 @@ var DEFAULT_USER_SKILLS = { "table-viewer": true, "product-campaign-page": true, "docx-generate": true, + "long-image-download": true, [PUBLISH_SKILL_NAME]: false }; var USER_ROLE_SKILL_PRESETS = { @@ -4057,14 +6471,14 @@ function parseSkillFrontmatter(content) { return { name, description }; } function listPlatformSkillCatalog(h5Root = __dirname3) { - const skillsRoot = path8.join(h5Root, "skills"); - if (!fs5.existsSync(skillsRoot)) return []; + const skillsRoot = path10.join(h5Root, "skills"); + if (!fs7.existsSync(skillsRoot)) return []; const catalog = []; - for (const entry of fs5.readdirSync(skillsRoot, { withFileTypes: true })) { + for (const entry of fs7.readdirSync(skillsRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; - const skillPath = path8.join(skillsRoot, entry.name, "SKILL.md"); - if (!fs5.existsSync(skillPath)) continue; - const raw = fs5.readFileSync(skillPath, "utf8"); + const skillPath = path10.join(skillsRoot, entry.name, "SKILL.md"); + if (!fs7.existsSync(skillPath)) continue; + const raw = fs7.readFileSync(skillPath, "utf8"); const meta = parseSkillFrontmatter(raw); const name = meta.name || entry.name; catalog.push({ @@ -4118,14 +6532,14 @@ function applySkillGrantsToCapabilities(capabilities, skillMap) { return effective; } function copySkillTree(srcDir, destDir) { - fs5.mkdirSync(destDir, { recursive: true }); - for (const entry of fs5.readdirSync(srcDir, { withFileTypes: true })) { - const from = path8.join(srcDir, entry.name); - const to = path8.join(destDir, entry.name); + fs7.mkdirSync(destDir, { recursive: true }); + for (const entry of fs7.readdirSync(srcDir, { withFileTypes: true })) { + const from = path10.join(srcDir, entry.name); + const to = path10.join(destDir, entry.name); if (entry.isDirectory()) { copySkillTree(from, to); } else { - fs5.copyFileSync(from, to); + fs7.copyFileSync(from, to); } } } @@ -4139,22 +6553,22 @@ function syncSkillsToWorkspace({ }) { const enabled = new Set(grantedSkillNames(skillMap)); const platformNames = new Set(catalog.map((item) => item.name)); - const agentsSkills = path8.join(publishDir, ".agents", "skills"); - if (fs5.existsSync(agentsSkills)) { - for (const entry of fs5.readdirSync(agentsSkills, { withFileTypes: true })) { + const agentsSkills = path10.join(publishDir, ".agents", "skills"); + if (fs7.existsSync(agentsSkills)) { + for (const entry of fs7.readdirSync(agentsSkills, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const skillName = entry.name; const catalogItem = catalog.find((item) => item.name === skillName || item.dirName === skillName); const resolvedName = catalogItem?.name ?? skillName; if (platformNames.has(resolvedName) && !enabled.has(resolvedName)) { - fs5.rmSync(path8.join(agentsSkills, entry.name), { recursive: true, force: true }); + fs7.rmSync(path10.join(agentsSkills, entry.name), { recursive: true, force: true }); } } } for (const item of catalog) { if (!enabled.has(item.name)) continue; - const srcDir = path8.join(h5Root, "skills", item.dirName); - const destDir = path8.join(agentsSkills, item.name); + const srcDir = path10.join(h5Root, "skills", item.dirName); + const destDir = path10.join(agentsSkills, item.name); if (item.name === PUBLISH_SKILL_NAME && user) { ensurePublishSkillInstalled(publishDir, { slug: String(user.id).trim().toLowerCase(), @@ -4164,8 +6578,8 @@ function syncSkillsToWorkspace({ }); continue; } - if (fs5.existsSync(srcDir)) { - if (fs5.existsSync(destDir)) fs5.rmSync(destDir, { recursive: true, force: true }); + if (fs7.existsSync(srcDir)) { + if (fs7.existsSync(destDir)) fs7.rmSync(destDir, { recursive: true, force: true }); copySkillTree(srcDir, destDir); } } @@ -4176,7 +6590,7 @@ var USER_COOKIE = "tkmind_user_session"; function safeEqual(left, right) { const a = Buffer.from(left); const b = Buffer.from(right); - return a.length === b.length && crypto4.timingSafeEqual(a, b); + return a.length === b.length && crypto5.timingSafeEqual(a, b); } var PASSWORD_ALGORITHM_PBKDF2 = "pbkdf2-sha512"; var PASSWORD_ALGORITHM_ARGON2ID = "argon2id"; @@ -4185,7 +6599,7 @@ var ARGON2_PASSES = 3; var ARGON2_PARALLELISM = 1; var ARGON2_TAG_LENGTH = 32; function hashPasswordPbkdf2(password, salt) { - return crypto4.pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex"); + return crypto5.pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex"); } function hashPasswordArgon2id(password, salt) { return argon2HashRawSync(password, { @@ -4198,7 +6612,7 @@ function hashPasswordArgon2id(password, salt) { }).toString("hex"); } function createPasswordRecord(password, algorithm = PASSWORD_ALGORITHM_ARGON2ID) { - const salt = crypto4.randomBytes(16).toString("hex"); + const salt = crypto5.randomBytes(16).toString("hex"); if (algorithm === PASSWORD_ALGORITHM_ARGON2ID) { return { salt, @@ -4229,11 +6643,11 @@ function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } function hashSessionToken(token) { - return crypto4.createHash("sha256").update(token).digest("hex"); + return crypto5.createHash("sha256").update(token).digest("hex"); } function createUserAuth(pool2, options = {}) { - const usersRoot = path9.resolve(options.usersRoot ?? "/tmp/tkmind_go_users"); - const h5Root = path9.resolve(options.h5Root ?? path9.join(usersRoot, "..")); + const usersRoot = path11.resolve(options.usersRoot ?? "/tmp/tkmind_go_users"); + const h5Root = path11.resolve(options.h5Root ?? path11.join(usersRoot, "..")); const env = options.env ?? process.env; const storageRoot = resolveMindspaceStorageRoot(h5Root, env); const publicBaseUrl = resolvePublicBaseUrl(env); @@ -4266,7 +6680,7 @@ function createUserAuth(pool2, options = {}) { await pool2.query( `INSERT INTO h5_login_sessions (id, user_id, token_hash, expires_at, created_at) VALUES (?, ?, ?, ?, ?)`, - [crypto4.randomUUID(), userId, hashSessionToken(token), expiresAt, now] + [crypto5.randomUUID(), userId, hashSessionToken(token), expiresAt, now] ); return expiresAt; }; @@ -4281,7 +6695,7 @@ function createUserAuth(pool2, options = {}) { ); }; const ensureWorkspace = (workspaceRoot) => { - fs6.mkdirSync(workspaceRoot, { recursive: true }); + fs8.mkdirSync(workspaceRoot, { recursive: true }); }; const isAdminRole = (user) => user?.role === "admin"; const getUserById = async (userId) => { @@ -4329,7 +6743,7 @@ function createUserAuth(pool2, options = {}) { return { ...base, publishSlug: publishKey, - publishUrl: `${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(publishKey)}/`, + publishUrl: buildPublicUrl(publicBaseUrl, publishKey), publishSkillName: PUBLISH_SKILL_NAME }; }; @@ -4360,7 +6774,7 @@ function createUserAuth(pool2, options = {}) { ensurePublishSkillInstalled(web.publishDir, hintsContext); ensureWorkspaceHintsInstalled(web.publishDir, hintsContext); const legacyPublishDir = resolveLegacyPublishDir(h5Root, user); - if (legacyPublishDir && legacyPublishDir !== web.publishDir && fs6.existsSync(legacyPublishDir)) { + if (legacyPublishDir && legacyPublishDir !== web.publishDir && fs8.existsSync(legacyPublishDir)) { ensureWorkspaceHintsInstalled(legacyPublishDir, { ...hintsContext, publishDir: legacyPublishDir }); } ensureUserMemoryProfile(web.publishDir, { @@ -4409,8 +6823,8 @@ function createUserAuth(pool2, options = {}) { const syncUserPublishWorkspace = async (user) => { if (!user) return null; const layout = await publishLayoutFor(user); - const current = path9.resolve(user.workspace_root); - const target = path9.resolve(layout.publishDir); + const current = path11.resolve(user.workspace_root); + const target = path11.resolve(layout.publishDir); if (current !== target) { const now = Date.now(); await pool2.query(`UPDATE h5_users SET workspace_root = ?, updated_at = ? WHERE id = ?`, [ @@ -4481,7 +6895,7 @@ function createUserAuth(pool2, options = {}) { (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at) VALUES (?, ?, 'web', 'low_balance_gift', ?, ?, ?, 'unread', NULL, ?, ?)`, [ - crypto4.randomUUID(), + crypto5.randomUUID(), userId, "\u65B0\u7528\u6237\u989D\u5EA6\u5DF2\u81EA\u52A8\u8865\u9001", `\u68C0\u6D4B\u5230\u4F60\u7684\u4F59\u989D\u5DF2\u4F4E\u4E8E \xA5${(lowBalanceGiftThresholdCents / 100).toFixed(2)}\uFF0C\u7CFB\u7EDF\u5DF2\u81EA\u52A8\u8D60\u9001 \xA5${(lowBalanceGiftAmountCents / 100).toFixed(2)} \u65B0\u7528\u6237\u989D\u5EA6\u3002\u672C\u798F\u5229\u4EC5\u53EF\u9886\u53D6\u4E00\u6B21\u3002`, @@ -4509,7 +6923,7 @@ function createUserAuth(pool2, options = {}) { return { ok: false, message: "\u8BF7\u8F93\u5165\u6709\u6548\u90AE\u7BB1" }; } const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password); - const userId = crypto4.randomUUID(); + const userId = crypto5.randomUUID(); const layout = await publishLayoutFor({ id: userId, username: normalized }); const workspaceRoot = layout.publishDir; const now = Date.now(); @@ -4624,7 +7038,7 @@ function createUserAuth(pool2, options = {}) { row.password_algorithm = nextPassword.passwordAlgorithm; } loginFailures.delete(failureKey); - const token = crypto4.randomBytes(32).toString("base64url"); + const token = crypto5.randomBytes(32).toString("base64url"); await storeSession(row.id, row.role, token, now); return { ok: true, token, user: publicUser(row) }; }; @@ -4936,7 +7350,7 @@ function createUserAuth(pool2, options = {}) { return { ok: false, message: "\u5BC6\u7801\u81F3\u5C11 6 \u4F4D" }; } const isAdmin = role === "admin"; - const userId = crypto4.randomUUID(); + const userId = crypto5.randomUUID(); const root2 = (await publishLayoutFor({ id: userId, username: normalized })).publishDir; const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password); const now = Date.now(); @@ -5014,7 +7428,7 @@ function createUserAuth(pool2, options = {}) { values.push(patch.status); } if (patch.workspaceRoot !== void 0) { - const root2 = path9.resolve(patch.workspaceRoot); + const root2 = path11.resolve(patch.workspaceRoot); fields.push("workspace_root = ?"); values.push(root2); ensureWorkspace(root2); @@ -5145,7 +7559,7 @@ function createUserAuth(pool2, options = {}) { (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at) VALUES (?, ?, 'web', 'space_purchase', ?, ?, ?, 'unread', NULL, ?, ?)`, [ - crypto4.randomUUID(), + crypto5.randomUUID(), userId, "\u7A7A\u95F4\u6269\u5BB9\u6210\u529F", `\u5DF2\u8D2D\u4E70 ${purchaseMb} MB \u7A7A\u95F4\uFF0C\u652F\u4ED8 \xA5${(costCents / 100).toFixed(2)}\u3002`, @@ -5220,7 +7634,7 @@ function createUserAuth(pool2, options = {}) { (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at) VALUES (?, ?, 'web', ?, ?, ?, ?, 'unread', NULL, ?, ?)`, [ - crypto4.randomUUID(), + crypto5.randomUUID(), userId, rechargeType, title, @@ -6061,7 +8475,7 @@ function createUserAuth(pool2, options = {}) { }; const PENDING_BIND_TTL_MS = 15 * 60 * 1e3; const issueUserSession = async (userId, role, now = Date.now()) => { - const token = crypto4.randomBytes(32).toString("base64url"); + const token = crypto5.randomBytes(32).toString("base64url"); await storeSession(userId, role, token, now); return token; }; @@ -6153,7 +8567,7 @@ function createUserAuth(pool2, options = {}) { status = "active", now = Date.now() }) => { - const id = crypto4.randomUUID(); + const id = crypto5.randomUUID(); await pool2.query( `INSERT INTO h5_wechat_agent_routes (id, user_id, app_id, openid, agent_session_id, status, created_at, updated_at) @@ -6247,7 +8661,7 @@ function createUserAuth(pool2, options = {}) { now = Date.now() }) => { if (!appId || !openid || !msgType) return null; - const id = crypto4.randomUUID(); + const id = crypto5.randomUUID(); await pool2.query( `INSERT INTO h5_wechat_mp_message_details (id, app_id, openid, user_id, msg_id, msg_type, display_text, agent_text, @@ -6303,7 +8717,7 @@ function createUserAuth(pool2, options = {}) { (id, user_id, app_id, openid, unionid, nickname, avatar_url, last_login_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ - crypto4.randomUUID(), + crypto5.randomUUID(), userId, appId, openid, @@ -6358,7 +8772,7 @@ function createUserAuth(pool2, options = {}) { now = Date.now() }) => { await pruneWechatPendingBinds(now); - const token = crypto4.randomBytes(24).toString("base64url"); + const token = crypto5.randomBytes(24).toString("base64url"); await pool2.query( `INSERT INTO h5_wechat_pending_binds (token, app_id, openid, unionid, nickname, avatar_url, return_to, @@ -6418,22 +8832,22 @@ function createUserAuth(pool2, options = {}) { }; const generateWechatUsername = async (openid) => { const cleaned = String(openid).replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); - const suffix = cleaned.slice(-8) || crypto4.randomBytes(4).toString("hex"); + const suffix = cleaned.slice(-8) || crypto5.randomBytes(4).toString("hex"); let candidate = `wx_${suffix}`.slice(0, 32); if (!isValidUsername(candidate)) { - candidate = `wx_${crypto4.randomBytes(4).toString("hex")}`; + candidate = `wx_${crypto5.randomBytes(4).toString("hex")}`; } for (let attempt = 0; attempt < 8; attempt += 1) { const [rows] = await pool2.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [ candidate ]); if (!rows[0]) return candidate; - candidate = `wx_${suffix.slice(0, Math.max(1, 8 - attempt))}${crypto4.randomBytes(2).toString("hex")}`.slice( + candidate = `wx_${suffix.slice(0, Math.max(1, 8 - attempt))}${crypto5.randomBytes(2).toString("hex")}`.slice( 0, 32 ); } - return `wx_${crypto4.randomBytes(6).toString("hex")}`.slice(0, 32); + return `wx_${crypto5.randomBytes(6).toString("hex")}`.slice(0, 32); }; const registerViaWechat = async ({ appId, @@ -6444,9 +8858,9 @@ function createUserAuth(pool2, options = {}) { now = Date.now() }) => { const normalized = await generateWechatUsername(openid); - const randomPassword = crypto4.randomBytes(24).toString("base64url"); + const randomPassword = crypto5.randomBytes(24).toString("base64url"); const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(randomPassword); - const userId = crypto4.randomUUID(); + const userId = crypto5.randomUUID(); const layout = await publishLayoutFor({ id: userId, username: normalized }); const workspaceRoot = layout.publishDir; const displayName = nickname?.trim() || "\u5FAE\u4FE1\u7528\u6237"; @@ -6487,7 +8901,7 @@ function createUserAuth(pool2, options = {}) { (id, user_id, app_id, openid, unionid, nickname, avatar_url, last_login_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ - crypto4.randomUUID(), + crypto5.randomUUID(), userId, appId, openid, @@ -6804,10 +9218,10 @@ function createUserAuth(pool2, options = {}) { } // scripts/agent-run-worker.mjs -var root = path10.join(path10.dirname(fileURLToPath5(import.meta.url)), ".."); +var root = path12.join(path12.dirname(fileURLToPath5(import.meta.url)), ".."); function loadEnvFile(filePath) { - if (!fs7.existsSync(filePath)) return; - for (const line of fs7.readFileSync(filePath, "utf8").split("\n")) { + if (!fs9.existsSync(filePath)) return; + for (const line of fs9.readFileSync(filePath, "utf8").split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eq = trimmed.indexOf("="); @@ -6828,15 +9242,25 @@ function parseArgs(argv) { const args2 = { once: false, status: false, + recoverStale: false, + applyRecovery: false, + runId: "", pollMs: Number(process.env.MEMIND_AGENT_RUN_WORKER_POLL_MS ?? 1e3), - limit: Number(process.env.MEMIND_AGENT_RUN_WORKER_BATCH_SIZE ?? 1) + limit: Number(process.env.MEMIND_AGENT_RUN_WORKER_BATCH_SIZE ?? 1), + staleMs: Number(process.env.MEMIND_AGENT_RUN_STALE_RUNNING_MS ?? process.env.MEMIND_AGENT_RUN_TIMEOUT_MS ?? 15 * 60 * 1e3) }; for (let i = 0; i < argv.length; i += 1) { const item = argv[i]; if (item === "--once") args2.once = true; else if (item === "--status") args2.status = true; + else if (item === "--recover-stale") args2.recoverStale = true; + else if (item === "--apply-recovery") { + args2.recoverStale = true; + args2.applyRecovery = true; + } else if (item === "--run-id") args2.runId = String(argv[++i] ?? "").trim(); else if (item === "--poll-ms") args2.pollMs = Number(argv[++i] ?? args2.pollMs); else if (item === "--limit") args2.limit = Number(argv[++i] ?? args2.limit); + else if (item === "--stale-ms") args2.staleMs = Number(argv[++i] ?? args2.staleMs); else if (item === "--help" || item === "-h") args2.help = true; } return args2; @@ -6844,26 +9268,46 @@ function parseArgs(argv) { function printHelp() { console.log([ "Usage:", - " node scripts/agent-run-worker.mjs [--once] [--status] [--poll-ms 1000] [--limit 1]", + " node scripts/agent-run-worker.mjs [--once] [--status] [--run-id ] [--poll-ms 1000] [--limit 1]", + " node scripts/agent-run-worker.mjs --recover-stale [--apply-recovery] [--stale-ms 900000]", "", "Notes:", " - Processes existing h5_agent_runs queued/retryable rows through the Tool Gateway queue.", + " - Regular dispatch automatically fails stale running rows older than MEMIND_AGENT_RUN_TIMEOUT_MS.", + " - --recover-stale is dry-run by default; --apply-recovery marks stale running rows failed.", " - Does not create agent runs by itself.", + " - --run-id dispatches exactly one known run and avoids scanning the global queue.", " - Set MEMIND_AGENT_RUN_AUTODISPATCH=0 on Portal only when this worker is ready to take over." ].join("\n")); } -loadEnvFile(process.env.MEMIND_ENV_FILE || path10.join(root, ".env")); -loadEnvFile(path10.join(root, ".env.local")); +loadEnvFile(process.env.MEMIND_ENV_FILE || path12.join(root, ".env")); +loadEnvFile(path12.join(root, ".env.local")); var args = parseArgs(process.argv.slice(2)); if (args.help) { printHelp(); process.exit(0); } var pool = createDbPool(); -var userAuth = createUserAuth(pool, { - usersRoot: process.env.H5_USERS_ROOT ?? path10.join(root, "users"), +var baseUserAuth = createUserAuth(pool, { + usersRoot: process.env.H5_USERS_ROOT ?? path12.join(root, "users"), h5Root: root }); +var overrideWorkingDir = String(process.env.MEMIND_AGENT_RUN_WORKDIR_OVERRIDE ?? "").trim(); +var overrideWorkingDirUserId = String(process.env.MEMIND_AGENT_RUN_WORKDIR_USER_ID ?? "").trim(); +var userAuth = overrideWorkingDir ? { + ...baseUserAuth, + async resolveWorkingDir(userId) { + if (!overrideWorkingDirUserId || overrideWorkingDirUserId === userId) { + return path12.resolve(overrideWorkingDir); + } + return baseUserAuth.resolveWorkingDir(userId); + } +} : baseUserAuth; +var llmProviderService = createLlmProviderService(pool, { + apiTarget: process.env.TKMIND_API_TARGET ?? "https://127.0.0.1:18006", + apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? "local-dev-secret" +}); +var toolGateway = createToolGateway({ llmProviderService }); var apiTargets = parseTargets(); var tkmindProxy = createTkmindProxy({ apiTarget: apiTargets[0], @@ -6875,6 +9319,7 @@ var gateway = createAgentRunGateway({ pool, userAuth, tkmindProxy, + toolGateway, autoDispatch: false, maxConcurrentRuns: Number(process.env.MEMIND_AGENT_RUN_QUEUE_CONCURRENCY ?? 1), runTimeoutMs: Number(process.env.MEMIND_AGENT_RUN_TIMEOUT_MS ?? 15 * 60 * 1e3) @@ -6887,15 +9332,40 @@ process.on("SIGTERM", () => { stopping = true; }); async function runOnce() { - const result = await gateway.dispatchQueuedRuns({ limit: args.limit }); + let result = null; + if (args.runId) { + gateway.dispatchRun(args.runId); + result = { + considered: 1, + dispatched: 1, + runId: args.runId + }; + } else { + result = await gateway.dispatchQueuedRuns({ limit: args.limit }); + } await waitForIdle(); console.log(JSON.stringify({ ok: true, mode: args.status ? "status" : "dispatch", + runId: args.runId || null, dispatched: args.status ? 0 : result.dispatched, queue: await gateway.getQueueStatus() })); } +async function runStaleRecovery() { + const result = await gateway.recoverStaleRunningRuns({ + staleMs: args.staleMs, + limit: args.limit, + dryRun: !args.applyRecovery, + reason: args.applyRecovery ? "manual_stale_recovery" : "manual_stale_recovery_dry_run" + }); + console.log(JSON.stringify({ + ok: true, + mode: args.applyRecovery ? "recover-stale-apply" : "recover-stale-dry-run", + recovery: result, + queue: await gateway.getQueueStatus() + }, null, 2)); +} async function waitForIdle() { const startedAt = Date.now(); const maxWaitMs = Number(process.env.MEMIND_AGENT_RUN_WORKER_DRAIN_WAIT_MS ?? 20 * 60 * 1e3); @@ -6912,7 +9382,9 @@ var exitCode = 0; try { if (args.status) { console.log(JSON.stringify({ ok: true, queue: await gateway.getQueueStatus() }, null, 2)); - } else if (args.once) { + } else if (args.recoverStale) { + await runStaleRecovery(); + } else if (args.once || args.runId) { await runOnce(); } else { console.log(JSON.stringify({ ok: true, worker: "agent-run-worker", pollMs: args.pollMs, limit: args.limit })); diff --git a/.runtime/portal/scripts/check-agent-code-run-entry.mjs b/.runtime/portal/scripts/check-agent-code-run-entry.mjs index a4ec79c..df47876 100644 --- a/.runtime/portal/scripts/check-agent-code-run-entry.mjs +++ b/.runtime/portal/scripts/check-agent-code-run-entry.mjs @@ -144,6 +144,8 @@ const buildFlags = { serverCodeRunsEnabledTruthy: truthy(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), enabledEnv: process.env.VITE_AGENT_CODE_RUNS_ENABLED ?? null, autodetectEnv: process.env.VITE_AGENT_CODE_RUNS_AUTODETECT ?? null, + enabledUserIdsEnv: process.env.VITE_AGENT_CODE_RUNS_USER_IDS ?? null, + serverCodeRunUserIdsEnv: process.env.MEMIND_AGENT_CODE_RUNS_USER_IDS ?? null, enabledTruthy: truthy(process.env.VITE_AGENT_CODE_RUNS_ENABLED), autodetectTruthy: truthy(process.env.VITE_AGENT_CODE_RUNS_AUTODETECT), note: 'These env values describe the current shell, not necessarily the already-built dist.', diff --git a/.runtime/portal/scripts/check-agent-run-worker.mjs b/.runtime/portal/scripts/check-agent-run-worker.mjs new file mode 100755 index 0000000..9860231 --- /dev/null +++ b/.runtime/portal/scripts/check-agent-run-worker.mjs @@ -0,0 +1,229 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import mysql from 'mysql2/promise'; + +const execFileAsync = promisify(execFile); +const DEFAULT_LABEL = 'cn.tkmind.memind-agent-run-worker'; + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx < 0) continue; + const key = trimmed.slice(0, idx).trim(); + const value = trimmed.slice(idx + 1).trim(); + if (!process.env[key]) process.env[key] = value; + } +} + +function truthy(value) { + return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase()); +} + +function parseMysqlConfig() { + if (process.env.DATABASE_URL) { + const url = new URL(process.env.DATABASE_URL); + if (url.protocol !== 'mysql:') { + throw new Error(`Unsupported DATABASE_URL scheme for agent run worker check: ${url.protocol}`); + } + return { + host: url.hostname, + port: Number(url.port || 3306), + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: url.pathname.replace(/^\/+/, ''), + charset: 'utf8mb4', + }; + } + return { + host: process.env.MYSQL_HOST, + port: Number(process.env.MYSQL_PORT || 3306), + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + charset: 'utf8mb4', + }; +} + +async function runCommand(command, args) { + try { + const { stdout, stderr } = await execFileAsync(command, args, { + maxBuffer: 1024 * 1024, + }); + return { ok: true, stdout, stderr }; + } catch (err) { + return { + ok: false, + code: err?.code ?? null, + stdout: err?.stdout ?? '', + stderr: err?.stderr ?? '', + message: err instanceof Error ? err.message : String(err), + }; + } +} + +function parseLaunchctlPrint(text) { + const state = text.match(/\bstate = ([^\n]+)/)?.[1]?.trim() ?? null; + const pid = text.match(/\bpid = ([0-9]+)/)?.[1] ?? null; + const program = text.match(/\bprogram = ([^\n]+)/)?.[1]?.trim() ?? null; + const pathValue = text.match(/\bpath = ([^\n]+)/)?.[1]?.trim() ?? null; + return { + loaded: Boolean(state || pid || program || pathValue), + state, + pid: pid == null ? null : Number(pid), + program, + path: pathValue, + }; +} + +function parseDisabled(text, label) { + const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = text.match(new RegExp(`"${escaped}"\\s*=>\\s*(disabled|enabled)`)); + return match?.[1] === 'disabled'; +} + +async function readQueueSummary() { + const conn = await mysql.createConnection(parseMysqlConfig()); + try { + const [statusRows] = await conn.query( + `SELECT status, COUNT(*) AS count + FROM h5_agent_runs + WHERE status IN ('queued', 'running', 'retryable') + GROUP BY status`, + ); + const statusCounts = {}; + for (const row of statusRows) statusCounts[row.status] = Number(row.count ?? 0); + + const [lagRows] = await conn.query( + `SELECT MIN(updated_at) AS oldest_updated_at + FROM h5_agent_runs + WHERE status IN ('queued', 'retryable')`, + ); + const oldestUpdatedAt = lagRows[0]?.oldest_updated_at == null + ? null + : Number(lagRows[0].oldest_updated_at); + + const [runningRows] = await conn.query( + `SELECT + r.id, + r.request_id, + r.started_at, + r.updated_at, + h.latest_heartbeat_at + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' + ORDER BY COALESCE(h.latest_heartbeat_at, r.started_at) ASC + LIMIT 1`, + ); + const oldestRunningStartedAt = runningRows[0]?.started_at == null + ? null + : Number(runningRows[0].started_at); + const oldestRunningHeartbeatAt = runningRows[0]?.latest_heartbeat_at == null + ? null + : Number(runningRows[0].latest_heartbeat_at); + const oldestRunningHeartbeatAgeMs = runningRows[0] + ? Math.max(0, Date.now() - Number(runningRows[0].latest_heartbeat_at ?? runningRows[0].started_at ?? Date.now())) + : 0; + const [runningWithoutHeartbeatRows] = await conn.query( + `SELECT COUNT(*) AS count + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' AND h.latest_heartbeat_at IS NULL`, + ); + + const [failedRows] = await conn.query( + `SELECT id, request_id, error_message, updated_at + FROM h5_agent_runs + WHERE status = 'failed' + ORDER BY updated_at DESC + LIMIT 1`, + ); + + return { + statusCounts, + oldestPendingUpdatedAt: oldestUpdatedAt, + oldestPendingAgeMs: oldestUpdatedAt == null ? 0 : Math.max(0, Date.now() - oldestUpdatedAt), + oldestRunningStartedAt, + oldestRunningAgeMs: oldestRunningStartedAt == null ? 0 : Math.max(0, Date.now() - oldestRunningStartedAt), + oldestRunningHeartbeatAt, + oldestRunningHeartbeatAgeMs, + runningWithoutHeartbeatCount: Number(runningWithoutHeartbeatRows[0]?.count ?? 0), + latestRunningRun: runningRows[0] ? { + id: runningRows[0].id, + requestId: runningRows[0].request_id, + startedAt: oldestRunningStartedAt, + updatedAt: runningRows[0].updated_at == null ? null : Number(runningRows[0].updated_at), + heartbeatAt: oldestRunningHeartbeatAt, + heartbeatAgeMs: oldestRunningHeartbeatAgeMs, + } : null, + latestFailedRun: failedRows[0] ? { + id: failedRows[0].id, + requestId: failedRows[0].request_id, + error: failedRows[0].error_message, + updatedAt: Number(failedRows[0].updated_at ?? 0), + } : null, + }; + } finally { + await conn.end(); + } +} + +const root = path.join(path.dirname(new URL(import.meta.url).pathname), '..'); +loadEnvFile(process.env.MEMIND_ENV_FILE || path.join(root, '.env')); + +const label = process.env.MEMIND_AGENT_RUN_WORKER_LABEL || DEFAULT_LABEL; +const gui = `gui/${process.getuid()}`; +const plist = path.join(process.env.HOME || '', 'Library', 'LaunchAgents', `${label}.plist`); +const expectRunning = truthy(process.env.MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING); + +const printResult = await runCommand('launchctl', ['print', `${gui}/${label}`]); +const disabledResult = await runCommand('launchctl', ['print-disabled', gui]); +const pgrepResult = await runCommand('pgrep', ['-fl', 'agent-run-worker.mjs']); +const launchd = parseLaunchctlPrint(`${printResult.stdout}\n${printResult.stderr}`); +const disabled = disabledResult.ok ? parseDisabled(disabledResult.stdout, label) : null; +const pgrepLines = pgrepResult.ok + ? pgrepResult.stdout.split('\n').map((line) => line.trim()).filter(Boolean) + : []; +const queue = await readQueueSummary().catch((err) => ({ + error: err instanceof Error ? (err.message || err.code || err.name) : String(err), +})); + +const installed = fs.existsSync(plist); +const running = launchd.state === 'running' || pgrepLines.length > 0; +const ok = Boolean( + installed && + !queue.error && + (expectRunning ? running : disabled === true && !running), +); + +console.log(JSON.stringify({ + ok, + checkedAt: new Date().toISOString(), + expected: expectRunning ? 'running' : 'disabled', + label, + plist, + installed, + disabled, + running, + launchd, + processes: pgrepLines, + queue, +}, null, 2)); + +process.exit(ok ? 0 : 1); diff --git a/.runtime/portal/scripts/check-mindspace-public-links.mjs b/.runtime/portal/scripts/check-mindspace-public-links.mjs new file mode 100644 index 0000000..2f0155b --- /dev/null +++ b/.runtime/portal/scripts/check-mindspace-public-links.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +/** + * Scan MindSpace public/*.html for broken relative links. + * + * Default (--downloads-only): attachment / download links only (release gate). + * --all-links: also check img/src and mindspace-cover assets. + * + * Usage: + * node scripts/check-mindspace-public-links.mjs + * node scripts/check-mindspace-public-links.mjs --user + * node scripts/check-mindspace-public-links.mjs --root /path/to/MindSpace + * node scripts/check-mindspace-public-links.mjs --all-links + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { scanPublicHtmlLinks } from '../mindspace-public-links.mjs'; + +const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +const DEFAULT_PUBLISH_ROOT = 'MindSpace'; + +function parseArgs(argv) { + let root = path.join(repoRoot, DEFAULT_PUBLISH_ROOT); + let userId = null; + let allLinks = false; + for (let i = 2; i < argv.length; i += 1) { + if (argv[i] === '--root' && argv[i + 1]) { + root = path.resolve(argv[i + 1]); + i += 1; + } else if (argv[i] === '--user' && argv[i + 1]) { + userId = argv[i + 1]; + i += 1; + } else if (argv[i] === '--all-links') { + allLinks = true; + } else if (argv[i] === '--downloads-only') { + allLinks = false; + } else if (argv[i] === '--help' || argv[i] === '-h') { + console.log( + `Usage: node scripts/check-mindspace-public-links.mjs [--root ${DEFAULT_PUBLISH_ROOT}] [--user ] [--downloads-only|--all-links]`, + ); + process.exit(0); + } + } + return { root, userId, downloadsOnly: !allLinks }; +} + +const { root, userId, downloadsOnly } = parseArgs(process.argv); +const issues = scanPublicHtmlLinks(root, { userId, downloadsOnly }); +const modeLabel = downloadsOnly ? 'download/attachment links' : 'all relative links'; + +if (issues.length === 0) { + console.log(`OK: no missing ${modeLabel} under ${root}${userId ? ` (user ${userId})` : ''}`); + process.exit(0); +} + +console.error(`Found ${issues.length} missing ${modeLabel} under ${root}:`); +for (const issue of issues) { + const relHtml = path.relative(root, issue.htmlPath); + console.error(`- ${relHtml}: ${issue.ref} (${issue.source})`); +} +process.exit(1); diff --git a/.runtime/portal/scripts/check-tool-runtime.mjs b/.runtime/portal/scripts/check-tool-runtime.mjs index 9958de5..4a5b7ba 100755 --- a/.runtime/portal/scripts/check-tool-runtime.mjs +++ b/.runtime/portal/scripts/check-tool-runtime.mjs @@ -97,6 +97,19 @@ try { chatInjectsCodeTools: false, aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000), openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000), + toolGateway: { + enabled: ['1', 'true', 'yes', 'on'].includes( + String(process.env.MEMIND_TOOL_GATEWAY_ENABLED ?? '').trim().toLowerCase(), + ), + dryRun: ['1', 'true', 'yes', 'on'].includes( + String(process.env.MEMIND_TOOL_GATEWAY_DRY_RUN ?? '').trim().toLowerCase(), + ), + protocol: 'agent-run-v1', + defaultExecutor: process.env.MEMIND_TOOL_GATEWAY_DEFAULT_EXECUTOR ?? 'aider', + openhandsTaskTypes: + process.env.MEMIND_TOOL_GATEWAY_OPENHANDS_TASK_TYPES ?? + 'repo_refactor,multi_file,complex_repo', + }, }, }, null, 2)); process.exit(ok ? 0 : 1); diff --git a/.runtime/portal/scripts/install-agent-run-guard-agent.sh b/.runtime/portal/scripts/install-agent-run-guard-agent.sh new file mode 100755 index 0000000..9f87abd --- /dev/null +++ b/.runtime/portal/scripts/install-agent-run-guard-agent.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" +SCRIPT="${MEMIND_AGENT_RUN_GUARD_SCRIPT:-$ROOT/scripts/agent-run-guard.mjs}" +LABEL="${MEMIND_AGENT_RUN_GUARD_LABEL:-cn.tkmind.memind-agent-run-guard}" +PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" +LOG="${MEMIND_AGENT_RUN_GUARD_LOG:-$HOME/Library/Logs/memind-agent-run-guard.log}" +GUI="gui/$(id -u)" +INTERVAL="${MEMIND_AGENT_RUN_GUARD_INTERVAL_SECONDS:-60}" +START="${MEMIND_AGENT_RUN_GUARD_START:-1}" + +mkdir -p "$HOME/Library/LaunchAgents" "$(dirname "$LOG")" + +if [[ ! -x "$NODE_BIN" ]]; then + NODE_BIN="$(command -v node)" +fi +if [[ ! -f "$SCRIPT" ]]; then + echo "agent run guard script not found: $SCRIPT" >&2 + exit 1 +fi + +cat > "$PLIST" < + + + + Label + $LABEL + ProgramArguments + + $NODE_BIN + $SCRIPT + --apply + + WorkingDirectory + $ROOT + StartInterval + $INTERVAL + RunAtLoad + + StandardOutPath + $LOG + StandardErrorPath + $LOG + EnvironmentVariables + + PATH + /opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin + MEMIND_AGENT_RUN_GUARD_ENABLED + ${MEMIND_AGENT_RUN_GUARD_ENABLED:-1} + MEMIND_AGENT_RUN_GUARD_FAILED_WINDOW_MS + ${MEMIND_AGENT_RUN_GUARD_FAILED_WINDOW_MS:-600000} + MEMIND_AGENT_RUN_GUARD_MAX_RECENT_FAILURES + ${MEMIND_AGENT_RUN_GUARD_MAX_RECENT_FAILURES:-3} + MEMIND_AGENT_RUN_GUARD_MAX_PENDING_AGE_MS + ${MEMIND_AGENT_RUN_GUARD_MAX_PENDING_AGE_MS:-300000} + MEMIND_AGENT_RUN_GUARD_MAX_PENDING_COUNT + ${MEMIND_AGENT_RUN_GUARD_MAX_PENDING_COUNT:-10} + MEMIND_AGENT_RUN_GUARD_MAX_RUNNING_AGE_MS + ${MEMIND_AGENT_RUN_GUARD_MAX_RUNNING_AGE_MS:-900000} + + + +EOF + +plutil -lint "$PLIST" +launchctl bootout "$GUI/$LABEL" 2>/dev/null || true +launchctl bootstrap "$GUI" "$PLIST" + +if [[ "$START" == "1" || "$START" == "true" || "$START" == "yes" ]]; then + launchctl enable "$GUI/$LABEL" + launchctl kickstart -k "$GUI/$LABEL" + state="started" +else + launchctl disable "$GUI/$LABEL" 2>/dev/null || true + state="installed-disabled" +fi + +echo "installed $PLIST" +echo "state: $state" +echo "script: $SCRIPT" +echo "interval_seconds: $INTERVAL" +echo "log: $LOG" +echo "manual stop: launchctl bootout $GUI/$LABEL" diff --git a/.runtime/portal/scripts/install-agent-run-worker-agent.sh b/.runtime/portal/scripts/install-agent-run-worker-agent.sh new file mode 100755 index 0000000..c843d68 --- /dev/null +++ b/.runtime/portal/scripts/install-agent-run-worker-agent.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" +SCRIPT="${MEMIND_AGENT_RUN_WORKER_SCRIPT:-$ROOT/scripts/agent-run-worker.mjs}" +LABEL="${MEMIND_AGENT_RUN_WORKER_LABEL:-cn.tkmind.memind-agent-run-worker}" +PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" +LOG="${MEMIND_AGENT_RUN_WORKER_LOG:-$HOME/Library/Logs/memind-agent-run-worker.log}" +GUI="gui/$(id -u)" +POLL_MS="${MEMIND_AGENT_RUN_WORKER_POLL_MS:-3000}" +BATCH_SIZE="${MEMIND_AGENT_RUN_WORKER_BATCH_SIZE:-1}" +CONCURRENCY="${MEMIND_AGENT_RUN_QUEUE_CONCURRENCY:-1}" +RUN_TIMEOUT_MS="${MEMIND_AGENT_RUN_TIMEOUT_MS:-900000}" +TOOL_GATEWAY_ENABLED="${MEMIND_TOOL_GATEWAY_ENABLED:-0}" +TOOL_GATEWAY_DRY_RUN="${MEMIND_TOOL_GATEWAY_DRY_RUN:-0}" +WORKDIR_OVERRIDE="${MEMIND_AGENT_RUN_WORKDIR_OVERRIDE:-}" +WORKDIR_OVERRIDE_USER_ID="${MEMIND_AGENT_RUN_WORKDIR_USER_ID:-}" +START="${MEMIND_AGENT_RUN_WORKER_START:-0}" + +mkdir -p "$HOME/Library/LaunchAgents" "$(dirname "$LOG")" + +if [[ ! -x "$NODE_BIN" ]]; then + NODE_BIN="$(command -v node)" +fi +if [[ ! -f "$SCRIPT" ]]; then + echo "agent run worker script not found: $SCRIPT" >&2 + exit 1 +fi + +cat > "$PLIST" < + + + + Label + $LABEL + ProgramArguments + + $NODE_BIN + $SCRIPT + --poll-ms + $POLL_MS + --limit + $BATCH_SIZE + + WorkingDirectory + $ROOT + RunAtLoad + + KeepAlive + + ThrottleInterval + 10 + StandardOutPath + $LOG + StandardErrorPath + $LOG + EnvironmentVariables + + PATH + /opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin + MEMIND_AGENT_RUN_AUTODISPATCH + 0 + MEMIND_AGENT_RUN_WORKER_POLL_MS + $POLL_MS + MEMIND_AGENT_RUN_WORKER_BATCH_SIZE + $BATCH_SIZE + MEMIND_AGENT_RUN_QUEUE_CONCURRENCY + $CONCURRENCY + MEMIND_AGENT_RUN_TIMEOUT_MS + $RUN_TIMEOUT_MS + MEMIND_TOOL_GATEWAY_ENABLED + $TOOL_GATEWAY_ENABLED + MEMIND_TOOL_GATEWAY_DRY_RUN + $TOOL_GATEWAY_DRY_RUN + MEMIND_AGENT_RUN_WORKDIR_OVERRIDE + $WORKDIR_OVERRIDE + MEMIND_AGENT_RUN_WORKDIR_USER_ID + $WORKDIR_OVERRIDE_USER_ID + + + +EOF + +plutil -lint "$PLIST" +launchctl bootout "$GUI/$LABEL" 2>/dev/null || true +launchctl bootstrap "$GUI" "$PLIST" + +if [[ "$START" == "1" || "$START" == "true" || "$START" == "yes" ]]; then + launchctl enable "$GUI/$LABEL" + launchctl kickstart -k "$GUI/$LABEL" + state="started" +else + launchctl disable "$GUI/$LABEL" 2>/dev/null || true + state="installed-disabled" +fi + +echo "installed $PLIST" +echo "state: $state" +echo "script: $SCRIPT" +echo "poll_ms: $POLL_MS" +echo "batch_size: $BATCH_SIZE" +echo "concurrency: $CONCURRENCY" +echo "tool_gateway_enabled: $TOOL_GATEWAY_ENABLED" +echo "tool_gateway_dry_run: $TOOL_GATEWAY_DRY_RUN" +echo "workdir_override: $WORKDIR_OVERRIDE" +echo "workdir_override_user_id: $WORKDIR_OVERRIDE_USER_ID" +echo "log: $LOG" +echo "manual start: launchctl enable $GUI/$LABEL && launchctl kickstart -k $GUI/$LABEL" +echo "manual stop: launchctl bootout $GUI/$LABEL" diff --git a/.runtime/portal/scripts/install-runtime-heartbeat-agent.sh b/.runtime/portal/scripts/install-runtime-heartbeat-agent.sh new file mode 100755 index 0000000..178d36e --- /dev/null +++ b/.runtime/portal/scripts/install-runtime-heartbeat-agent.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" +SCRIPT="${MEMIND_RUNTIME_HEARTBEAT_SCRIPT:-$ROOT/scripts/runtime-worker-heartbeat.mjs}" +LABEL="${MEMIND_RUNTIME_HEARTBEAT_LABEL:-cn.tkmind.memind-runtime-heartbeat}" +PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" +LOG="$HOME/Library/Logs/memind-runtime-heartbeat.log" +GUI="gui/$(id -u)" +INTERVAL_MS="${MEMIND_RUNTIME_HEARTBEAT_INTERVAL_MS:-15000}" +TIMEOUT_MS="${MEMIND_RUNTIME_HEARTBEAT_TIMEOUT_MS:-5000}" +TTL_MS="${MEMIND_RUNTIME_HEARTBEAT_TTL_MS:-45000}" + +mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs" + +if [[ ! -x "$NODE_BIN" ]]; then + NODE_BIN="$(command -v node)" +fi +if [[ ! -f "$SCRIPT" ]]; then + echo "runtime heartbeat script not found: $SCRIPT" >&2 + exit 1 +fi + +cat > "$PLIST" < + + + + Label + $LABEL + ProgramArguments + + $NODE_BIN + $SCRIPT + serve + + WorkingDirectory + $ROOT + RunAtLoad + + KeepAlive + + ThrottleInterval + 10 + StandardOutPath + $LOG + StandardErrorPath + $LOG + EnvironmentVariables + + PATH + /opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin + MEMIND_RUNTIME_HEARTBEAT_INTERVAL_MS + $INTERVAL_MS + MEMIND_RUNTIME_HEARTBEAT_TIMEOUT_MS + $TIMEOUT_MS + MEMIND_RUNTIME_HEARTBEAT_TTL_MS + $TTL_MS + + + +EOF + +plutil -lint "$PLIST" +launchctl bootout "$GUI/$LABEL" 2>/dev/null || true +launchctl bootstrap "$GUI" "$PLIST" +launchctl enable "$GUI/$LABEL" +launchctl kickstart -k "$GUI/$LABEL" + +echo "installed $PLIST" +echo "script: $SCRIPT" +echo "interval_ms: $INTERVAL_MS" +echo "timeout_ms: $TIMEOUT_MS" +echo "ttl_ms: $TTL_MS" +echo "log: $LOG" diff --git a/.runtime/portal/scripts/install-runtime-slo-report-agent.sh b/.runtime/portal/scripts/install-runtime-slo-report-agent.sh index 336f84c..058b9b2 100755 --- a/.runtime/portal/scripts/install-runtime-slo-report-agent.sh +++ b/.runtime/portal/scripts/install-runtime-slo-report-agent.sh @@ -11,6 +11,7 @@ GUI="gui/$(id -u)" HOUR="${MEMIND_RUNTIME_SLO_HOUR:-23}" MINUTE="${MEMIND_RUNTIME_SLO_MINUTE:-55}" REPORT_DIR="${MEMIND_RUNTIME_REPORT_DIR:-$ROOT/reports/runtime-slo}" +RETENTION_DAYS="${MEMIND_RUNTIME_SLO_RETENTION_DAYS:-30}" mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs" "$REPORT_DIR" @@ -34,6 +35,9 @@ cat > "$PLIST" <$NODE_BIN $SCRIPT --write-report + --prune + --retention-days + $RETENTION_DAYS --report-dir $REPORT_DIR @@ -69,4 +73,5 @@ echo "installed $PLIST" echo "script: $SCRIPT" echo "schedule: daily ${HOUR}:${MINUTE}" echo "report_dir: $REPORT_DIR" +echo "retention_days: $RETENTION_DAYS" echo "log: $LOG" diff --git a/.runtime/portal/scripts/install-runtime-slo-soak-agent.sh b/.runtime/portal/scripts/install-runtime-slo-soak-agent.sh new file mode 100755 index 0000000..93e10e1 --- /dev/null +++ b/.runtime/portal/scripts/install-runtime-slo-soak-agent.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" +SCRIPT="${MEMIND_RUNTIME_SLO_SCRIPT:-$ROOT/scripts/runtime-slo-report.mjs}" +LABEL="${MEMIND_RUNTIME_SLO_SOAK_LABEL:-cn.tkmind.memind-runtime-slo-soak}" +PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" +LOG="$HOME/Library/Logs/memind-runtime-slo-soak.log" +GUI="gui/$(id -u)" +INTERVAL_SECONDS="${MEMIND_RUNTIME_SLO_SOAK_INTERVAL_SECONDS:-3600}" +REPORT_DIR="${MEMIND_RUNTIME_SLO_SOAK_REPORT_DIR:-$ROOT/reports/runtime-slo-soak}" +RETENTION_DAYS="${MEMIND_RUNTIME_SLO_SOAK_RETENTION_DAYS:-7}" + +mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs" "$REPORT_DIR" + +if [[ ! "$INTERVAL_SECONDS" =~ ^[0-9]+$ ]] || [[ "$INTERVAL_SECONDS" -lt 60 ]]; then + echo "invalid MEMIND_RUNTIME_SLO_SOAK_INTERVAL_SECONDS: $INTERVAL_SECONDS" >&2 + exit 1 +fi +if [[ ! "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || [[ "$RETENTION_DAYS" -lt 1 ]]; then + echo "invalid MEMIND_RUNTIME_SLO_SOAK_RETENTION_DAYS: $RETENTION_DAYS" >&2 + exit 1 +fi +if [[ ! -x "$NODE_BIN" ]]; then + NODE_BIN="$(command -v node)" +fi +if [[ ! -f "$SCRIPT" ]]; then + echo "runtime SLO script not found: $SCRIPT" >&2 + exit 1 +fi + +cat > "$PLIST" < + + + + Label + $LABEL + ProgramArguments + + $NODE_BIN + $SCRIPT + --write-report + --prune + --retention-days + $RETENTION_DAYS + --report-dir + $REPORT_DIR + + WorkingDirectory + $ROOT + StartInterval + $INTERVAL_SECONDS + StandardOutPath + $LOG + StandardErrorPath + $LOG + EnvironmentVariables + + PATH + /opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin + + + +EOF + +plutil -lint "$PLIST" +launchctl bootout "$GUI/$LABEL" 2>/dev/null || true +launchctl bootstrap "$GUI" "$PLIST" +launchctl enable "$GUI/$LABEL" +launchctl kickstart -k "$GUI/$LABEL" + +echo "installed $PLIST" +echo "script: $SCRIPT" +echo "schedule: every ${INTERVAL_SECONDS}s" +echo "report_dir: $REPORT_DIR" +echo "retention_days: $RETENTION_DAYS" +echo "log: $LOG" diff --git a/.runtime/portal/scripts/load-env.mjs b/.runtime/portal/scripts/load-env.mjs new file mode 100644 index 0000000..beeecce --- /dev/null +++ b/.runtime/portal/scripts/load-env.mjs @@ -0,0 +1,20 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const separator = trimmed.indexOf('='); + if (separator < 0) continue; + const key = trimmed.slice(0, separator).trim(); + const value = trimmed.slice(separator + 1).trim(); + if (!process.env[key]) process.env[key] = value; + } +} + +export function loadH5Environment(scriptDirectory) { + loadEnvFile(path.join(scriptDirectory, '../../../.env.local')); + loadEnvFile(path.join(scriptDirectory, '../.env')); +} diff --git a/.runtime/portal/scripts/memind-portal-tunnel.sh b/.runtime/portal/scripts/memind-portal-tunnel.sh new file mode 100755 index 0000000..3d543ea --- /dev/null +++ b/.runtime/portal/scripts/memind-portal-tunnel.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# 反向 SSH 隧道:105 通过 127.0.0.1:19081 访问本机 Portal (:8081) +set -euo pipefail + +HOST="${MEMIND_PORTAL_TUNNEL_HOST:-ssh105-public}" +LOCAL_PORT="${MEMIND_PORTAL_TUNNEL_LOCAL_PORT:-8081}" +REMOTE_PORT="${MEMIND_PORTAL_TUNNEL_REMOTE_PORT:-19081}" + +exec ssh -N \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o ExitOnForwardFailure=yes \ + -R "127.0.0.1:${REMOTE_PORT}:127.0.0.1:${LOCAL_PORT}" \ + "${HOST}" diff --git a/.runtime/portal/scripts/runtime-slo-report.mjs b/.runtime/portal/scripts/runtime-slo-report.mjs index 77309cf..4d66980 100755 --- a/.runtime/portal/scripts/runtime-slo-report.mjs +++ b/.runtime/portal/scripts/runtime-slo-report.mjs @@ -90,27 +90,85 @@ function uniquePaths(paths) { function parseArgs(argv) { const args = { writeReport: false, + prune: false, + retentionDays: Number(process.env.MEMIND_RUNTIME_SLO_RETENTION_DAYS || 30), reportDir: process.env.MEMIND_RUNTIME_REPORT_DIR || '', }; for (let i = 0; i < argv.length; i += 1) { const item = argv[i]; if (item === '--write-report') args.writeReport = true; + else if (item === '--prune') args.prune = true; + else if (item === '--retention-days') args.retentionDays = Number(argv[++i] ?? args.retentionDays); else if (item === '--report-dir') args.reportDir = String(argv[++i] ?? args.reportDir); else if (item === '--help' || item === '-h') args.help = true; } + if (!Number.isFinite(args.retentionDays) || args.retentionDays < 1) { + throw new Error(`Invalid --retention-days: ${args.retentionDays}`); + } return args; } function printHelp() { console.log([ 'Usage:', - ' node scripts/runtime-slo-report.mjs [--write-report] [--report-dir ]', + ' node scripts/runtime-slo-report.mjs [--write-report] [--prune] [--retention-days ] [--report-dir ]', '', 'Default behavior is read-only and writes nothing.', '--write-report writes JSON and Markdown snapshots to an operations report directory.', + '--prune deletes old .json/.md snapshots in the report directory only.', ].join('\n')); } +function pruneReportDir(reportDir, retentionDays) { + const resolvedDir = path.resolve(reportDir); + const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000; + const result = { + enabled: true, + reportDir: resolvedDir, + retentionDays, + deleted: [], + kept: 0, + skipped: 0, + errors: [], + }; + let entries = []; + try { + entries = fs.readdirSync(resolvedDir, { withFileTypes: true }); + } catch (err) { + result.errors.push(err instanceof Error ? err.message : String(err)); + return result; + } + for (const entry of entries) { + if (!entry.isFile()) { + result.skipped += 1; + continue; + } + if (!entry.name.endsWith('.json') && !entry.name.endsWith('.md')) { + result.skipped += 1; + continue; + } + const full = path.join(resolvedDir, entry.name); + let stat = null; + try { + stat = fs.statSync(full); + } catch (err) { + result.errors.push(`${entry.name}: ${err instanceof Error ? err.message : String(err)}`); + continue; + } + if (stat.mtimeMs >= cutoffMs) { + result.kept += 1; + continue; + } + try { + fs.unlinkSync(full); + result.deleted.push(entry.name); + } catch (err) { + result.errors.push(`${entry.name}: ${err instanceof Error ? err.message : String(err)}`); + } + } + return result; +} + function tableCountQueries() { return [ ['h5_users', 'users'], @@ -166,13 +224,28 @@ async function readRedisSummary(namespace, redisUrl) { function summarizeRuntime(runtimeJson) { const workers = runtimeJson?.router?.workers ?? []; + const toolQueue = runtimeJson?.toolRuntime?.queue ?? null; const staleMetricMs = 2 * 60 * 1000; + const staleHeartbeatMs = Number(process.env.MEMIND_RUNTIME_HEARTBEAT_STALE_MS || 60 * 1000); const now = Date.now(); return { routerEnabled: Boolean(runtimeJson?.router?.enabled), publicBaseUrl: runtimeJson?.publicBaseUrl ?? null, toolRuntime: runtimeJson?.toolRuntime ?? null, - toolQueue: runtimeJson?.toolRuntime?.queue ?? null, + toolQueue, + toolQueueSlo: toolQueue + ? { + heartbeatMs: Number(toolQueue.heartbeatMs ?? 0), + maxConcurrentRuns: Number(toolQueue.maxConcurrentRuns ?? 0), + statusCounts: toolQueue.statusCounts ?? {}, + oldestRunningStartedAt: toolQueue.oldestRunningStartedAt ?? null, + oldestRunningAgeMs: Number(toolQueue.oldestRunningAgeMs ?? 0), + oldestRunningHeartbeatAt: toolQueue.oldestRunningHeartbeatAt ?? null, + oldestRunningHeartbeatAgeMs: Number(toolQueue.oldestRunningHeartbeatAgeMs ?? 0), + runningWithoutHeartbeatCount: Number(toolQueue.runningWithoutHeartbeatCount ?? 0), + latestRunningRun: toolQueue.latestRunningRun ?? null, + } + : null, workers: workers.map((worker) => ({ id: worker.id, healthy: runtimeJson?.targets?.find((target) => target.target === worker.target)?.healthy ?? null, @@ -191,6 +264,14 @@ function summarizeRuntime(runtimeJson) { fdPressure: worker.fdPressure, fdCount: worker.fdCount, containerHealth: worker.containerHealth, + heartbeat: worker.heartbeat, + heartbeatAgeMs: worker.heartbeat ? now - worker.heartbeat : null, + heartbeatFresh: worker.heartbeat ? now - worker.heartbeat <= staleHeartbeatMs : false, + heartbeatSource: worker.heartbeatSource ?? null, + heartbeatOk: worker.heartbeatOk ?? null, + heartbeatStatusCode: worker.heartbeatStatusCode ?? null, + heartbeatLatencyMs: worker.heartbeatLatencyMs ?? null, + heartbeatError: worker.heartbeatError ?? null, metricsAgeMs: worker.metricsSampledAt ? now - worker.metricsSampledAt : null, metricsFresh: worker.metricsSampledAt ? now - worker.metricsSampledAt <= staleMetricMs : false, score: worker.score, @@ -246,6 +327,8 @@ if (!runtime.ok) failures.push('runtime_status_unavailable'); if (runtimeSummary && !runtimeSummary.routerEnabled) failures.push('router_disabled'); for (const worker of runtimeSummary?.workers ?? []) { if (!worker.healthy) failures.push(`${worker.id}_target_unhealthy`); + if (!worker.heartbeatFresh) failures.push(`${worker.id}_heartbeat_stale`); + if (worker.heartbeatOk === false) failures.push(`${worker.id}_heartbeat_unhealthy`); if (worker.containerHealth && worker.containerHealth !== 'healthy') failures.push(`${worker.id}_container_${worker.containerHealth}`); if (!worker.metricsFresh) failures.push(`${worker.id}_metrics_stale`); if (worker.streamErrorCount > 0) failures.push(`${worker.id}_stream_errors_${worker.streamErrorCount}`); @@ -256,6 +339,32 @@ if (runtimeSummary?.toolQueue?.error) failures.push('tool_queue_status_error'); if (runtimeSummary?.toolQueue?.inFlight > runtimeSummary?.toolQueue?.maxConcurrentRuns) { failures.push('tool_queue_concurrency_exceeded'); } +const agentRunHeartbeatStaleMs = Number( + process.env.MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS || + Math.max(90 * 1000, Number(runtimeSummary?.toolQueueSlo?.heartbeatMs ?? 0) * 3), +); +if ( + runtimeSummary?.toolQueueSlo?.oldestRunningHeartbeatAgeMs > agentRunHeartbeatStaleMs +) { + failures.push(`agent_run_heartbeat_stale_${runtimeSummary.toolQueueSlo.oldestRunningHeartbeatAgeMs}`); +} +if ( + Number(runtimeSummary?.toolQueueSlo?.runningWithoutHeartbeatCount ?? 0) > 0 && + Number(runtimeSummary?.toolQueueSlo?.oldestRunningAgeMs ?? 0) > agentRunHeartbeatStaleMs +) { + failures.push(`agent_run_missing_heartbeat_${runtimeSummary.toolQueueSlo.runningWithoutHeartbeatCount}`); +} + +const reportDir = path.resolve(args.reportDir || path.join(appRoot, 'reports', 'runtime-slo')); +if (args.writeReport || args.prune) { + fs.mkdirSync(reportDir, { recursive: true }); +} +const reportPrune = args.prune + ? pruneReportDir(reportDir, args.retentionDays) + : { enabled: false, reportDir, retentionDays: args.retentionDays }; +for (const error of reportPrune.errors ?? []) { + failures.push(`report_prune_error:${error}`); +} const report = { ok: failures.length === 0, @@ -274,7 +383,9 @@ const report = { mindSpace: false, redis: false, report: Boolean(args.writeReport), + reportPrune: Boolean(args.prune), }, + reportPrune, }; function markdownReport(payload) { @@ -290,8 +401,8 @@ function markdownReport(payload) { '', '## Workers', '', - '| worker | healthy | active | errors | first-token 5m p50/p95 | first-token 1h p50/p95 | metricsFresh | score |', - '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', + '| worker | healthy | active | errors | first-token 5m p50/p95 | first-token 1h p50/p95 | heartbeat | metricsFresh | score |', + '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', ]; for (const worker of workers) { lines.push([ @@ -301,6 +412,7 @@ function markdownReport(payload) { worker.streamErrorCount, `${worker.firstToken5m?.p50Ms ?? 0}/${worker.firstToken5m?.p95Ms ?? 0}`, `${worker.firstToken1h?.p50Ms ?? 0}/${worker.firstToken1h?.p95Ms ?? 0}`, + `${worker.heartbeatSource ?? 'none'}:${worker.heartbeatFresh}`, worker.metricsFresh, worker.score, ].join(' | ').replace(/^/, '| ').replace(/$/, ' |')); @@ -309,6 +421,10 @@ function markdownReport(payload) { '', '## Tool Queue', '', + `- heartbeatMs: ${payload.runtime?.toolQueueSlo?.heartbeatMs ?? 0}`, + `- oldestRunningHeartbeatAgeMs: ${payload.runtime?.toolQueueSlo?.oldestRunningHeartbeatAgeMs ?? 0}`, + `- runningWithoutHeartbeatCount: ${payload.runtime?.toolQueueSlo?.runningWithoutHeartbeatCount ?? 0}`, + '', '```json', JSON.stringify(payload.runtime?.toolQueue ?? null, null, 2), '```', @@ -319,13 +435,17 @@ function markdownReport(payload) { JSON.stringify(payload.writes, null, 2), '```', '', + '## Report Prune', + '', + '```json', + JSON.stringify(payload.reportPrune ?? null, null, 2), + '```', + '', ); return lines.join('\n'); } if (args.writeReport) { - const reportDir = path.resolve(args.reportDir || path.join(appRoot, 'reports', 'runtime-slo')); - fs.mkdirSync(reportDir, { recursive: true }); const stamp = new Date().toISOString().replace(/[:.]/g, '-'); report.reportFiles = { json: path.join(reportDir, `${stamp}.json`), diff --git a/.runtime/portal/scripts/runtime-worker-heartbeat.mjs b/.runtime/portal/scripts/runtime-worker-heartbeat.mjs new file mode 100755 index 0000000..9de4194 --- /dev/null +++ b/.runtime/portal/scripts/runtime-worker-heartbeat.mjs @@ -0,0 +1,164 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { Agent, fetch as undiciFetch } from 'undici'; +import { createClient } from 'redis'; + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx < 0) continue; + const key = trimmed.slice(0, idx).trim(); + const value = trimmed.slice(idx + 1).trim(); + if (!process.env[key]) process.env[key] = value; + } +} + +function positiveInteger(value, fallback) { + const n = Number(value); + if (!Number.isFinite(n) || n <= 0) return fallback; + return Math.floor(n); +} + +function targetWorkers() { + const targets = (process.env.TKMIND_API_TARGETS || process.env.TKMIND_API_TARGET || '') + .split(',') + .map((value) => value.trim()) + .filter(Boolean); + return targets.map((target, index) => ({ + id: `goosed-${index + 1}`, + target, + })); +} + +function workerKey(namespace, id, field) { + return [namespace, 'worker', id, field].join(':'); +} + +async function probeTarget(target, timeoutMs) { + const startedAt = Date.now(); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + const dispatcher = new Agent({ connect: { rejectUnauthorized: false } }); + try { + const url = new URL('/status', target.endsWith('/') ? target : `${target}/`).toString(); + const res = await undiciFetch(url, { + signal: controller.signal, + dispatcher: url.startsWith('https://127.0.0.1') ? dispatcher : undefined, + headers: process.env.TKMIND_SERVER__SECRET_KEY + ? { 'X-Secret-Key': process.env.TKMIND_SERVER__SECRET_KEY } + : undefined, + }); + const body = (await res.text()).trim().slice(0, 80); + return { + ok: res.ok && body === 'ok', + statusCode: res.status, + body, + latencyMs: Date.now() - startedAt, + error: null, + }; + } catch (err) { + return { + ok: false, + statusCode: 0, + body: '', + latencyMs: Date.now() - startedAt, + error: err instanceof Error ? err.message : String(err), + }; + } finally { + clearTimeout(timeout); + dispatcher.close(); + } +} + +async function writeHeartbeats(client, namespace, workers, options) { + const now = Date.now(); + const results = []; + for (const worker of workers) { + const probe = await probeTarget(worker.target, options.timeoutMs); + const ttlSeconds = Math.max(5, Math.ceil(options.ttlMs / 1000)); + const multi = client + .multi() + .set(workerKey(namespace, worker.id, 'heartbeat'), String(now), { EX: ttlSeconds }) + .set(workerKey(namespace, worker.id, 'heartbeat_at'), String(now), { EX: ttlSeconds }) + .set(workerKey(namespace, worker.id, 'heartbeat_source'), 'sidecar', { EX: ttlSeconds }) + .set(workerKey(namespace, worker.id, 'heartbeat_target'), worker.target, { EX: ttlSeconds }) + .set(workerKey(namespace, worker.id, 'heartbeat_ok'), probe.ok ? '1' : '0', { EX: ttlSeconds }) + .set(workerKey(namespace, worker.id, 'heartbeat_status_code'), String(probe.statusCode), { EX: ttlSeconds }) + .set(workerKey(namespace, worker.id, 'heartbeat_latency_ms'), String(probe.latencyMs), { EX: ttlSeconds }); + if (probe.error) { + multi.set(workerKey(namespace, worker.id, 'heartbeat_error'), probe.error, { EX: ttlSeconds }); + } else { + multi.del(workerKey(namespace, worker.id, 'heartbeat_error')); + } + await multi.exec(); + results.push({ ...worker, ...probe, heartbeatAt: now }); + } + return results; +} + +loadEnvFile(process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env')); + +const redisUrl = process.env.MEMIND_RUNTIME_REDIS_URL || 'redis://127.0.0.1:6379/0'; +const namespace = process.env.MEMIND_RUNTIME_REDIS_NAMESPACE || 'memind:runtime'; +const action = process.argv[2] || 'once'; +const intervalMs = positiveInteger(process.env.MEMIND_RUNTIME_HEARTBEAT_INTERVAL_MS, 15_000); +const timeoutMs = positiveInteger(process.env.MEMIND_RUNTIME_HEARTBEAT_TIMEOUT_MS, 5_000); +const ttlMs = positiveInteger(process.env.MEMIND_RUNTIME_HEARTBEAT_TTL_MS, Math.max(45_000, intervalMs * 3)); + +if (!['once', 'serve'].includes(action)) { + console.error('Usage: node scripts/runtime-worker-heartbeat.mjs '); + process.exit(2); +} + +const workers = targetWorkers(); +if (workers.length === 0) { + console.error('No TKMIND_API_TARGETS configured'); + process.exit(2); +} + +const client = createClient({ url: redisUrl }); +client.on('error', (err) => { + console.error(`Redis error: ${err instanceof Error ? err.message : err}`); +}); +await client.connect(); + +let stopping = false; +const stop = async () => { + stopping = true; + await client.quit().catch(() => {}); +}; +process.on('SIGTERM', () => void stop().finally(() => process.exit(0))); +process.on('SIGINT', () => void stop().finally(() => process.exit(0))); + +async function tick() { + const result = await writeHeartbeats(client, namespace, workers, { timeoutMs, ttlMs }); + const payload = { + ok: result.every((worker) => worker.ok), + action, + namespace, + intervalMs: action === 'serve' ? intervalMs : undefined, + timeoutMs, + ttlMs, + workers: result, + }; + console.log(JSON.stringify(payload)); + return payload; +} + +if (action === 'once') { + const payload = await tick(); + await client.quit(); + process.exit(payload.ok ? 0 : 1); +} + +await tick(); +while (!stopping) { + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + if (!stopping) await tick().catch((err) => { + console.error(err instanceof Error ? err.message : String(err)); + }); +} diff --git a/.runtime/portal/scripts/wechat-mp-menu.mjs b/.runtime/portal/scripts/wechat-mp-menu.mjs new file mode 100755 index 0000000..494c6e9 --- /dev/null +++ b/.runtime/portal/scripts/wechat-mp-menu.mjs @@ -0,0 +1,138 @@ +#!/usr/bin/env node +/** + * 微信服务号自定义菜单(menu/create)。 + * + * 变更规范: + * - 只在本仓库修改 MENU 常量,禁止 SSH 到 105 直接改线上脚本。 + * - 流程:本地改码 → Git commit → 正式发布 → 再执行本脚本同步到微信 API。 + * - 详见 docs/105-server-operations.md + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { loadH5Environment } from './load-env.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +loadH5Environment(__dirname); + +const DEFAULT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token'; +const DEFAULT_MENU_CREATE_URL = 'https://api.weixin.qq.com/cgi-bin/menu/create'; +const DEFAULT_MENU_GET_URL = 'https://api.weixin.qq.com/cgi-bin/menu/get'; + +const MENU = { + button: [ + { + type: 'view', + name: 'TKMind', + url: 'https://mm.tkmind.cn', + }, + { + type: 'view', + name: 'M空间', + url: 'https://mm.tkmind.cn/space', + }, + { + type: 'view', + name: 'M发现', + url: 'https://plaza.tkmind.cn', + }, + ], +}; + +function endpointFromTokenUrl(pathname, fallback) { + const tokenUrl = process.env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_TOKEN_URL; + try { + const url = new URL(tokenUrl); + url.pathname = pathname; + url.search = ''; + return url.toString(); + } catch { + return fallback; + } +} + +async function readJson(response) { + const text = await response.text(); + if (!response.ok) { + throw new Error(text || `HTTP ${response.status}`); + } + return text ? JSON.parse(text) : null; +} + +async function getAccessToken() { + const appid = + process.env.H5_WECHAT_MP_APP_ID?.trim() ?? process.env.H5_WECHAT_APP_ID?.trim() ?? ''; + const secret = + process.env.H5_WECHAT_MP_APP_SECRET?.trim() ?? process.env.H5_WECHAT_APP_SECRET?.trim() ?? ''; + if (!appid || !secret) { + throw new Error( + '缺少 H5_WECHAT_MP_APP_ID / H5_WECHAT_MP_APP_SECRET(或 H5_WECHAT_APP_ID / H5_WECHAT_APP_SECRET)', + ); + } + + const tokenUrl = process.env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_TOKEN_URL; + const payload = await readJson( + await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + grant_type: 'client_credential', + appid, + secret, + force_refresh: false, + }), + }), + ); + if (!payload?.access_token) { + throw new Error(payload?.errmsg || `获取 access_token 失败 (${payload?.errcode ?? 'unknown'})`); + } + return payload.access_token; +} + +async function createMenu(accessToken) { + const baseUrl = + process.env.H5_WECHAT_MP_MENU_CREATE_URL?.trim() || + endpointFromTokenUrl('/cgi-bin/menu/create', DEFAULT_MENU_CREATE_URL); + const url = `${baseUrl}?access_token=${encodeURIComponent(accessToken)}`; + const payload = await readJson( + await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(MENU), + }), + ); + if (Number(payload?.errcode ?? 0) !== 0) { + throw new Error(payload?.errmsg || `创建菜单失败 (${payload?.errcode})`); + } + return payload; +} + +async function getMenu(accessToken) { + const baseUrl = + process.env.H5_WECHAT_MP_MENU_GET_URL?.trim() || + endpointFromTokenUrl('/cgi-bin/menu/get', DEFAULT_MENU_GET_URL); + const url = `${baseUrl}?access_token=${encodeURIComponent(accessToken)}`; + return readJson(await fetch(url, { method: 'GET' })); +} + +const dryRun = process.argv.includes('--dry-run'); +if (dryRun) { + console.log(JSON.stringify(MENU, null, 2)); + process.exit(0); +} + +const accessToken = await getAccessToken(); +const created = await createMenu(accessToken); +const current = await getMenu(accessToken).catch((err) => ({ warning: err.message })); + +console.log( + JSON.stringify( + { + ok: true, + created, + menu: MENU, + current, + }, + null, + 2, + ), +); diff --git a/.runtime/portal/server.mjs b/.runtime/portal/server.mjs index 6084dec..ddf5fc5 100644 --- a/.runtime/portal/server.mjs +++ b/.runtime/portal/server.mjs @@ -1355,7 +1355,7 @@ var require_cert_signatures = __commonJS({ var require_sasl = __commonJS({ "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/crypto/sasl.js"(exports, module) { "use strict"; - var crypto36 = require_utils2(); + var crypto37 = require_utils2(); var { signatureAlgorithmHashFromCertificate } = require_cert_signatures(); function saslprep(password) { const nonAsciiSpace = /[\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000]/g; @@ -1373,7 +1373,7 @@ var require_sasl = __commonJS({ if (mechanism === "SCRAM-SHA-256-PLUS" && typeof stream.getPeerCertificate !== "function") { throw new Error("SASL: Mechanism SCRAM-SHA-256-PLUS requires a certificate"); } - const clientNonce = crypto36.randomBytes(18).toString("base64"); + const clientNonce = crypto37.randomBytes(18).toString("base64"); const gs2Header = mechanism === "SCRAM-SHA-256-PLUS" ? "p=tls-server-end-point" : stream ? "y" : "n"; return { mechanism, @@ -1415,20 +1415,20 @@ var require_sasl = __commonJS({ const peerCert = stream.getPeerCertificate().raw; let hashName = signatureAlgorithmHashFromCertificate(peerCert); if (hashName === "MD5" || hashName === "SHA-1") hashName = "SHA-256"; - const certHash = await crypto36.hashByName(hashName, peerCert); + const certHash = await crypto37.hashByName(hashName, peerCert); const bindingData = Buffer.concat([Buffer.from("p=tls-server-end-point,,"), Buffer.from(certHash)]); channelBinding = bindingData.toString("base64"); } const clientFinalMessageWithoutProof = "c=" + channelBinding + ",r=" + sv.nonce; const authMessage = clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof; const saltBytes = Buffer.from(sv.salt, "base64"); - const saltedPassword = await crypto36.deriveKey(saslprep(password), saltBytes, sv.iteration); - const clientKey = await crypto36.hmacSha256(saltedPassword, "Client Key"); - const storedKey = await crypto36.sha256(clientKey); - const clientSignature = await crypto36.hmacSha256(storedKey, authMessage); + const saltedPassword = await crypto37.deriveKey(saslprep(password), saltBytes, sv.iteration); + const clientKey = await crypto37.hmacSha256(saltedPassword, "Client Key"); + const storedKey = await crypto37.sha256(clientKey); + const clientSignature = await crypto37.hmacSha256(storedKey, authMessage); const clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString("base64"); - const serverKey = await crypto36.hmacSha256(saltedPassword, "Server Key"); - const serverSignatureBytes = await crypto36.hmacSha256(serverKey, authMessage); + const serverKey = await crypto37.hmacSha256(saltedPassword, "Server Key"); + const serverSignatureBytes = await crypto37.hmacSha256(serverKey, authMessage); session.message = "SASLResponse"; session.serverSignature = Buffer.from(serverSignatureBytes).toString("base64"); session.response = clientFinalMessageWithoutProof + ",p=" + clientProof; @@ -1631,15 +1631,15 @@ var require_pg_connection_string = __commonJS({ if (config.sslnegotiation === "direct" && config.ssl === void 0) { config.ssl = true; } - const fs29 = config.sslcert || config.sslkey || config.sslrootcert ? __require("fs") : null; + const fs33 = config.sslcert || config.sslkey || config.sslrootcert ? __require("fs") : null; if (config.sslcert) { - config.ssl.cert = fs29.readFileSync(config.sslcert).toString(); + config.ssl.cert = fs33.readFileSync(config.sslcert).toString(); } if (config.sslkey) { - config.ssl.key = fs29.readFileSync(config.sslkey).toString(); + config.ssl.key = fs33.readFileSync(config.sslkey).toString(); } if (config.sslrootcert) { - config.ssl.ca = fs29.readFileSync(config.sslrootcert).toString(); + config.ssl.ca = fs33.readFileSync(config.sslrootcert).toString(); } if (options.useLibpqCompat && config.uselibpqcompat) { throw new Error("Both useLibpqCompat and uselibpqcompat are set. Please use only one of them."); @@ -2005,10 +2005,10 @@ var require_result = __commonJS({ var require_query = __commonJS({ "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/query.js"(exports, module) { "use strict"; - var { EventEmitter } = __require("events"); + var { EventEmitter: EventEmitter2 } = __require("events"); var Result2 = require_result(); var utils = require_utils(); - var Query2 = class extends EventEmitter { + var Query2 = class extends EventEmitter2 { constructor(config, values, callback) { super(); config = utils.normalizeQueryConfig(config, values, callback); @@ -3160,14 +3160,14 @@ var require_stream = __commonJS({ var require_connection = __commonJS({ "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/connection.js"(exports, module) { "use strict"; - var EventEmitter = __require("events").EventEmitter; + var EventEmitter2 = __require("events").EventEmitter; var { parse, serialize } = require_dist(); var stream = require_stream(); var { getStream } = stream; var flushBuffer = serialize.flush(); var syncBuffer = serialize.sync(); var endBuffer = serialize.end(); - var Connection2 = class extends EventEmitter { + var Connection2 = class extends EventEmitter2 { constructor(config) { super(); config = config || {}; @@ -3458,7 +3458,7 @@ var require_split2 = __commonJS({ var require_helper = __commonJS({ "node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/helper.js"(exports, module) { "use strict"; - var path31 = __require("path"); + var path35 = __require("path"); var Stream = __require("stream").Stream; var split = require_split2(); var util = __require("util"); @@ -3497,7 +3497,7 @@ var require_helper = __commonJS({ }; module.exports.getFileName = function(rawEnv) { var env = rawEnv || process.env; - var file = env.PGPASSFILE || (isWin ? path31.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path31.join(env.HOME || "./", ".pgpass")); + var file = env.PGPASSFILE || (isWin ? path35.join(env.APPDATA || "./", "postgresql", "pgpass.conf") : path35.join(env.HOME || "./", ".pgpass")); return file; }; module.exports.usePgPass = function(stats, fname) { @@ -3629,16 +3629,16 @@ var require_helper = __commonJS({ var require_lib = __commonJS({ "node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/index.js"(exports, module) { "use strict"; - var path31 = __require("path"); - var fs29 = __require("fs"); + var path35 = __require("path"); + var fs33 = __require("fs"); var helper = require_helper(); module.exports = function(connInfo, cb) { var file = helper.getFileName(); - fs29.stat(file, function(err, stat) { + fs33.stat(file, function(err, stat) { if (err || !helper.usePgPass(stat, file)) { return cb(void 0); } - var st = fs29.createReadStream(file); + var st = fs33.createReadStream(file); helper.getPassword(connInfo, st, cb); }); }; @@ -3649,7 +3649,7 @@ var require_lib = __commonJS({ // node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/client.js var require_client = __commonJS({ "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/client.js"(exports, module) { - var EventEmitter = __require("events").EventEmitter; + var EventEmitter2 = __require("events").EventEmitter; var utils = require_utils(); var nodeUtils = __require("util"); var sasl = require_sasl(); @@ -3658,7 +3658,7 @@ var require_client = __commonJS({ var Query2 = require_query(); var defaults2 = require_defaults(); var Connection2 = require_connection(); - var crypto36 = require_utils2(); + var crypto37 = require_utils2(); var activeQueryDeprecationNotice = nodeUtils.deprecate( () => { }, @@ -3694,7 +3694,7 @@ var require_client = __commonJS({ } return defaultValue; } - var Client2 = class extends EventEmitter { + var Client2 = class extends EventEmitter2 { constructor(config) { super(); this.connectionParameters = new ConnectionParameters(config); @@ -3909,7 +3909,7 @@ var require_client = __commonJS({ _handleAuthMD5Password(msg) { this._getPassword(async () => { try { - const hashedPassword = await crypto36.postgresMd5PasswordHash(this.user, this.password, msg.salt); + const hashedPassword = await crypto37.postgresMd5PasswordHash(this.user, this.password, msg.salt); this.connection.password(hashedPassword); } catch (e) { this.emit("error", e); @@ -4292,7 +4292,7 @@ var require_client = __commonJS({ var require_pg_pool = __commonJS({ "node_modules/.pnpm/pg-pool@3.14.0_pg@8.22.0/node_modules/pg-pool/index.js"(exports, module) { "use strict"; - var EventEmitter = __require("events").EventEmitter; + var EventEmitter2 = __require("events").EventEmitter; var NOOP = function() { }; var removeWhere = (list, predicate) => { @@ -4343,7 +4343,7 @@ var require_pg_pool = __commonJS({ pool2.emit("error", err, client); }; } - var Pool2 = class extends EventEmitter { + var Pool2 = class extends EventEmitter2 { constructor(options, Client2) { super(); this.options = Object.assign({}, options); @@ -4718,11 +4718,11 @@ var require_pg_pool = __commonJS({ var require_query2 = __commonJS({ "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/native/query.js"(exports, module) { "use strict"; - var EventEmitter = __require("events").EventEmitter; + var EventEmitter2 = __require("events").EventEmitter; var util = __require("util"); var utils = require_utils(); var NativeQuery = module.exports = function(config, values, callback) { - EventEmitter.call(this); + EventEmitter2.call(this); config = utils.normalizeQueryConfig(config, values, callback); this.text = config.text; this.values = config.values; @@ -4739,7 +4739,7 @@ var require_query2 = __commonJS({ }.bind(this) ); }; - util.inherits(NativeQuery, EventEmitter); + util.inherits(NativeQuery, EventEmitter2); var errorFieldMap = { sqlState: "code", statementPosition: "position", @@ -4866,7 +4866,7 @@ var require_client2 = __commonJS({ throw e; } var TypeOverrides2 = require_type_overrides(); - var EventEmitter = __require("events").EventEmitter; + var EventEmitter2 = __require("events").EventEmitter; var util = __require("util"); var ConnectionParameters = require_connection_parameters(); var NativeQuery = require_query2(); @@ -4876,7 +4876,7 @@ var require_client2 = __commonJS({ "Calling client.query() when the client is already executing a query is deprecated and will be removed in pg@9.0. Use async/await or an external async flow control mechanism instead." ); var Client2 = module.exports = function(config) { - EventEmitter.call(this); + EventEmitter2.call(this); config = config || {}; this._Promise = config.Promise || global.Promise; this._types = new TypeOverrides2(config.types); @@ -4903,7 +4903,7 @@ var require_client2 = __commonJS({ this.namedQueries = {}; }; Client2.Query = NativeQuery; - util.inherits(Client2, EventEmitter); + util.inherits(Client2, EventEmitter2); Client2.prototype._errorAllQueries = function(err) { const enqueueError = (query) => { process.nextTick(() => { @@ -5228,7 +5228,7 @@ var experience_service_pg_exports = {}; __export(experience_service_pg_exports, { createPgExperienceService: () => createPgExperienceService }); -import crypto34 from "node:crypto"; +import crypto35 from "node:crypto"; async function createPgExperienceService(options = {}) { const connectionString = options.connectionString; if (!connectionString) { @@ -5294,7 +5294,7 @@ async function createPgExperienceService(options = {}) { const body = String(input?.body ?? "").trim(); if (!title) throw experienceError2("\u7ECF\u9A8C\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A", "invalid_experience_input"); if (!body) throw experienceError2("\u7ECF\u9A8C\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A", "invalid_experience_input"); - const id = crypto34.randomUUID(); + const id = crypto35.randomUUID(); const ts = now(); const scope = String(input?.scope ?? "global").trim() || "global"; const kind = String(input?.kind ?? "lesson").trim() || "lesson"; @@ -5414,11 +5414,11 @@ var init_experience_service_pg = __esm({ // server.mjs import express2 from "express"; -import crypto35 from "node:crypto"; -import fs28 from "node:fs"; -import fsPromises3 from "node:fs/promises"; +import crypto36 from "node:crypto"; +import fs32 from "node:fs"; +import fsPromises4 from "node:fs/promises"; import { createProxyMiddleware } from "http-proxy-middleware"; -import path30 from "node:path"; +import path34 from "node:path"; import { fileURLToPath as fileURLToPath6 } from "node:url"; // auth.mjs @@ -6095,6 +6095,69 @@ async function migrateSchema(pool2) { CONSTRAINT fk_h5_agent_run_event_run FOREIGN KEY (run_id) REFERENCES h5_agent_runs(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); + await pool2.query(` + CREATE TABLE IF NOT EXISTS h5_conversation_packages ( + id VARCHAR(64) PRIMARY KEY, + user_id CHAR(36) NOT NULL, + session_id VARCHAR(128) NOT NULL, + title VARCHAR(255) NULL, + status ENUM('active', 'archived', 'deleted') NOT NULL DEFAULT 'active', + storage_prefix VARCHAR(512) NULL, + manifest_asset_id CHAR(36) NULL, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, + UNIQUE KEY uq_h5_conversation_package_session (user_id, session_id), + KEY idx_h5_conversation_package_user_updated (user_id, updated_at), + CONSTRAINT fk_h5_conversation_package_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE, + CONSTRAINT fk_h5_conversation_package_manifest FOREIGN KEY (manifest_asset_id) REFERENCES h5_assets(id) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + await pool2.query(` + CREATE TABLE IF NOT EXISTS h5_conversation_artifacts ( + id VARCHAR(64) PRIMARY KEY, + package_id VARCHAR(64) NOT NULL, + asset_id CHAR(36) NULL, + page_id CHAR(36) NULL, + publication_id CHAR(36) NULL, + agent_run_id CHAR(36) NULL, + message_id VARCHAR(128) NULL, + role VARCHAR(32) NULL, + artifact_kind VARCHAR(64) NOT NULL, + display_name VARCHAR(255) NULL, + mime_type VARCHAR(128) NULL, + size_bytes BIGINT NULL, + storage_key VARCHAR(512) NULL, + canonical_url VARCHAR(512) NULL, + sort_order INT NOT NULL DEFAULT 0, + created_at BIGINT NOT NULL, + KEY idx_h5_conversation_artifact_package (package_id, sort_order, created_at), + KEY idx_h5_conversation_artifact_asset (asset_id), + KEY idx_h5_conversation_artifact_page (page_id), + KEY idx_h5_conversation_artifact_publication (publication_id), + KEY idx_h5_conversation_artifact_run (agent_run_id), + KEY idx_h5_conversation_artifact_message (message_id), + CONSTRAINT fk_h5_conversation_artifact_package FOREIGN KEY (package_id) REFERENCES h5_conversation_packages(id) ON DELETE CASCADE, + CONSTRAINT fk_h5_conversation_artifact_asset FOREIGN KEY (asset_id) REFERENCES h5_assets(id) ON DELETE SET NULL, + CONSTRAINT fk_h5_conversation_artifact_page FOREIGN KEY (page_id) REFERENCES h5_page_records(id) ON DELETE SET NULL, + CONSTRAINT fk_h5_conversation_artifact_publication FOREIGN KEY (publication_id) REFERENCES h5_publish_records(id) ON DELETE SET NULL, + CONSTRAINT fk_h5_conversation_artifact_run FOREIGN KEY (agent_run_id) REFERENCES h5_agent_runs(id) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); + const uploadSessionColumns = [ + ["source_session_id", "VARCHAR(128) NULL AFTER completed_asset_id"], + ["source_message_id", "VARCHAR(128) NULL AFTER source_session_id"] + ]; + for (const [column, definition] of uploadSessionColumns) { + if (!await columnExists(pool2, "h5_upload_sessions", column)) { + await pool2.query(`ALTER TABLE h5_upload_sessions ADD COLUMN \`${column}\` ${definition}`); + } + } + if (!await indexExists(pool2, "h5_upload_sessions", "idx_h5_upload_source_session")) { + await pool2.query( + `ALTER TABLE h5_upload_sessions + ADD KEY idx_h5_upload_source_session (user_id, source_session_id)` + ); + } await pool2.query(` CREATE TABLE IF NOT EXISTS h5_user_memory_items ( id CHAR(64) PRIMARY KEY, @@ -6505,12 +6568,16 @@ async function initSchema(pool2) { // agent-run-gateway.mjs import crypto3 from "node:crypto"; +import fs3 from "node:fs/promises"; +import path2 from "node:path"; var DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5e3, 15e3]; var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed"]); var CODE_TOOL_MODES = /* @__PURE__ */ new Set(["code", "code-task", "code_task", "code-tool", "code_tool", "code_tool_task"]); var RUN_METADATA_KEY = "memindRun"; var DEFAULT_MAX_CONCURRENT_RUNS = 1; var DEFAULT_RUN_TIMEOUT_MS = 15 * 60 * 1e3; +var DEFAULT_RUN_HEARTBEAT_MS = 30 * 1e3; +var TOOL_GATEWAY_SUMMARY_LIMIT = 4096; function nowMs() { return Date.now(); } @@ -6549,6 +6616,98 @@ function normalizeTaskType(value) { const normalized = String(value ?? "").trim(); return normalized || null; } +function summarizeText(value, limit = TOOL_GATEWAY_SUMMARY_LIMIT) { + const text = String(value ?? ""); + if (text.length <= limit) return text; + return text.slice(text.length - limit); +} +function normalizeExpectedFileCheck(value) { + if (typeof value === "string") { + const expectedPath2 = value.trim(); + return expectedPath2 ? { path: expectedPath2 } : null; + } + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const expectedPath = String(value.path ?? value.file ?? value.relativePath ?? "").trim(); + if (!expectedPath) return null; + const contains = value.contains ?? value.expectedContent ?? value.contentIncludes; + return { + path: expectedPath, + ...contains == null ? {} : { contains: String(contains) } + }; +} +function normalizeToolGatewayValidation(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const checks = []; + const single = normalizeExpectedFileCheck(value.expectedFile ?? value.expectedPath); + if (single) checks.push(single); + if (Array.isArray(value.expectedFiles)) { + for (const item of value.expectedFiles) { + const check = normalizeExpectedFileCheck(item); + if (check) checks.push(check); + } + } + return checks.length > 0 ? { expectedFiles: checks } : null; +} +function resolveValidationPath(cwd, expectedPath) { + if (!String(cwd ?? "").trim()) { + const err = new Error("Tool Gateway validation failed: missing working directory"); + err.code = "TOOL_GATEWAY_VALIDATION_FAILED"; + err.retryable = false; + throw err; + } + const root = path2.resolve(String(cwd ?? "")); + const target = path2.resolve(root, String(expectedPath ?? "")); + if (target !== root && !target.startsWith(`${root}${path2.sep}`)) { + const err = new Error(`Tool Gateway validation path escapes working directory: ${expectedPath}`); + err.code = "TOOL_GATEWAY_VALIDATION_FAILED"; + err.retryable = false; + throw err; + } + return { root, target }; +} +async function validateToolGatewayResult({ result, validation, cwd }) { + if (!validation?.expectedFiles?.length || result?.dryRun) { + return null; + } + const checks = []; + for (const expected of validation.expectedFiles) { + const { root, target } = resolveValidationPath(cwd ?? result?.cwd, expected.path); + let content = ""; + let stat = null; + try { + stat = await fs3.stat(target); + content = await fs3.readFile(target, "utf8"); + } catch (cause) { + const err = new Error(`Tool Gateway validation failed: expected file not found: ${expected.path}`); + err.code = "TOOL_GATEWAY_VALIDATION_FAILED"; + err.retryable = false; + err.validation = { + expectedFile: expected.path, + cwd: root, + reason: "missing_file" + }; + err.cause = cause; + throw err; + } + if (expected.contains != null && !content.includes(expected.contains)) { + const err = new Error(`Tool Gateway validation failed: expected content not found in ${expected.path}`); + err.code = "TOOL_GATEWAY_VALIDATION_FAILED"; + err.retryable = false; + err.validation = { + expectedFile: expected.path, + cwd: root, + reason: "missing_content" + }; + throw err; + } + checks.push({ + path: expected.path, + sizeBytes: Number(stat?.size ?? 0), + contains: expected.contains == null ? null : true + }); + } + return { expectedFiles: checks }; +} function withRunMetadata(userMessage, { toolMode = "chat", taskType = null } = {}) { const message = userMessage && typeof userMessage === "object" && !Array.isArray(userMessage) ? { ...userMessage } : { value: userMessage }; const metadata = message.metadata && typeof message.metadata === "object" && !Array.isArray(message.metadata) ? { ...message.metadata } : {}; @@ -6570,7 +6729,8 @@ function getRunOptionsFromMessage(userMessage) { } return { toolMode, - taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType) + taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType), + validation: normalizeToolGatewayValidation(runMetadata?.validation ?? metadata?.toolGatewayValidation) }; } function projectRun(row) { @@ -6593,6 +6753,7 @@ function createAgentRunGateway({ pool: pool2, userAuth: userAuth2, tkmindProxy: tkmindProxy2, + toolGateway: toolGateway2 = null, retryDelaysMs = DEFAULT_RUN_RETRY_DELAYS_MS, autoDispatch = envFlag(process.env.MEMIND_AGENT_RUN_AUTODISPATCH, true), maxConcurrentRuns = positiveInteger( @@ -6602,6 +6763,10 @@ function createAgentRunGateway({ runTimeoutMs = positiveInteger( process.env.MEMIND_AGENT_RUN_TIMEOUT_MS, DEFAULT_RUN_TIMEOUT_MS + ), + heartbeatMs = positiveInteger( + process.env.MEMIND_AGENT_RUN_HEARTBEAT_MS, + DEFAULT_RUN_HEARTBEAT_MS ) }) { const inFlight = /* @__PURE__ */ new Set(); @@ -6713,6 +6878,30 @@ function createAgentRunGateway({ ); await appendEvent(runId, status, fields); } + function startRunHeartbeat(runId, { attempt }) { + let stopped = false; + const writeHeartbeat = async () => { + if (stopped) return; + try { + await appendEvent(runId, "worker_heartbeat", { + attempt, + pid: process.pid, + heartbeatMs + }); + } catch (err) { + console.error("[AgentRun] worker heartbeat failed:", err instanceof Error ? err.message : err); + } + }; + void writeHeartbeat(); + const timer = setInterval(() => { + void writeHeartbeat(); + }, heartbeatMs); + timer.unref?.(); + return () => { + stopped = true; + clearInterval(timer); + }; + } async function runWithTimeout(runId, task) { let timer = null; const timeout = new Promise((_, reject) => { @@ -6727,6 +6916,49 @@ function createAgentRunGateway({ async function executeRun(row, runId) { const userMessage = safeJsonParse(row.user_message_json, {}); const runOptions = getRunOptionsFromMessage(userMessage); + const toolGatewayStatus = toolGateway2?.getStatus ? toolGateway2.getStatus() : null; + if (runOptions.toolMode === "code" && toolGatewayStatus?.enabled) { + const workingDir = userAuth2?.resolveWorkingDir ? await userAuth2.resolveWorkingDir(row.user_id) : void 0; + await appendEvent(runId, "tool_gateway_dispatch", { + protocol: toolGatewayStatus.protocol ?? "agent-run-v1", + taskType: runOptions.taskType, + workingDir: workingDir ?? null + }); + const result = await toolGateway2.executeJob({ + runId, + userId: row.user_id, + requestId: row.request_id, + userMessage, + taskType: runOptions.taskType, + cwd: workingDir, + timeoutMs: runTimeoutMs + }); + await appendEvent(runId, "tool_gateway_result", { + executor: result.executor ?? null, + dryRun: Boolean(result.dryRun), + exitCode: result.exitCode ?? null, + stdoutTail: summarizeText(result.stdout), + stderrTail: summarizeText(result.stderr) + }); + try { + const validation = await validateToolGatewayResult({ + result, + validation: runOptions.validation, + cwd: workingDir + }); + if (validation) { + await appendEvent(runId, "tool_gateway_validation", validation); + } + } catch (err) { + await appendEvent(runId, "tool_gateway_validation_failed", { + code: err?.code ?? null, + message: err instanceof Error ? err.message : String(err), + validation: err?.validation ?? null + }); + throw err; + } + return { sessionId: row.agent_session_id ?? null }; + } let sessionId = row.agent_session_id ?? null; if (!sessionId) { const sessionOptions = {}; @@ -6766,6 +6998,7 @@ function createAgentRunGateway({ ); if (Number(claim?.affectedRows ?? 0) === 0) return; await appendEvent(runId, "running", { attempt: nextAttempt }); + const stopHeartbeat = startRunHeartbeat(runId, { attempt: nextAttempt }); try { const { sessionId } = await runWithTimeout(runId, () => executeRun(row, runId)); await markRun(runId, "succeeded", { @@ -6778,7 +7011,7 @@ function createAgentRunGateway({ if (timedOut) { await appendEvent(runId, "timeout", { timeoutMs: runTimeoutMs }); } - const retryable = !timedOut && nextAttempt < retryDelaysMs.length; + const retryable = !timedOut && err?.retryable !== false && nextAttempt < retryDelaysMs.length; await markRun(runId, retryable ? "retryable" : "failed", { error_message: message, completed_at: retryable ? null : nowMs() @@ -6786,6 +7019,8 @@ function createAgentRunGateway({ if (retryable && autoDispatch) { setTimeout(() => dispatchRun(runId), retryDelaysMs[nextAttempt - 1]); } + } finally { + stopHeartbeat(); } } function drainQueue() { @@ -6814,23 +7049,166 @@ function createAgentRunGateway({ for (const row of rows) { statusCounts[row.status] = Number(row.count ?? 0); } + const [oldestRunningRows] = await pool2.query( + `SELECT + r.id, + r.request_id, + r.started_at, + r.updated_at, + r.attempts, + h.latest_heartbeat_at + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' + ORDER BY COALESCE(h.latest_heartbeat_at, r.started_at) ASC + LIMIT 1` + ); + const oldestRunningStartedAt = oldestRunningRows[0]?.started_at == null ? null : Number(oldestRunningRows[0].started_at); + const oldestRunningAgeMs = oldestRunningStartedAt == null ? 0 : Math.max(0, nowMs() - oldestRunningStartedAt); + const oldestRunningHeartbeatAt = oldestRunningRows[0]?.latest_heartbeat_at == null ? null : Number(oldestRunningRows[0].latest_heartbeat_at); + const oldestRunningHeartbeatAgeMs = oldestRunningRows[0] ? Math.max(0, nowMs() - Number(oldestRunningRows[0].latest_heartbeat_at ?? oldestRunningRows[0].started_at ?? nowMs())) : 0; + const [runningWithoutHeartbeatRows] = await pool2.query( + `SELECT COUNT(*) AS count + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' AND h.latest_heartbeat_at IS NULL` + ); return { autoDispatch, maxConcurrentRuns, runTimeoutMs, + heartbeatMs, inFlight: inFlight.size, pendingDispatches: queuedDispatches.length, statusCounts, - terminalStatuses: [...TERMINAL_STATUSES] + oldestRunningStartedAt, + oldestRunningAgeMs, + oldestRunningHeartbeatAt, + oldestRunningHeartbeatAgeMs, + runningWithoutHeartbeatCount: Number(runningWithoutHeartbeatRows[0]?.count ?? 0), + latestRunningRun: oldestRunningRows[0] ? { + id: oldestRunningRows[0].id, + requestId: oldestRunningRows[0].request_id, + startedAt: oldestRunningStartedAt, + updatedAt: oldestRunningRows[0].updated_at == null ? null : Number(oldestRunningRows[0].updated_at), + attempts: Number(oldestRunningRows[0].attempts ?? 0), + heartbeatAt: oldestRunningHeartbeatAt, + heartbeatAgeMs: oldestRunningHeartbeatAgeMs + } : null, + terminalStatuses: [...TERMINAL_STATUSES], + toolGateway: toolGateway2?.getStatus ? toolGateway2.getStatus() : null + }; + } + async function recoverStaleRunningRuns({ + staleMs = runTimeoutMs, + limit = maxConcurrentRuns, + dryRun = true, + reason = "stale_running_timeout" + } = {}) { + const normalizedStaleMs = positiveInteger(staleMs, runTimeoutMs); + const normalizedLimit = positiveInteger(limit, maxConcurrentRuns); + const cutoff = nowMs() - normalizedStaleMs; + const [rows] = await pool2.query( + `SELECT + r.id, + r.request_id, + r.started_at, + r.updated_at, + r.attempts, + h.latest_heartbeat_at + FROM h5_agent_runs r + LEFT JOIN ( + SELECT run_id, MAX(created_at) AS latest_heartbeat_at + FROM h5_agent_run_events + WHERE event_type = 'worker_heartbeat' + GROUP BY run_id + ) h ON h.run_id = r.id + WHERE r.status = 'running' + AND r.started_at IS NOT NULL + AND COALESCE(h.latest_heartbeat_at, r.started_at) <= ? + ORDER BY COALESCE(h.latest_heartbeat_at, r.started_at) ASC + LIMIT ?`, + [cutoff, normalizedLimit] + ); + const recovered = []; + for (const row of rows) { + const ageMs = Math.max(0, nowMs() - Number(row.started_at ?? 0)); + const heartbeatAt = row.latest_heartbeat_at == null ? null : Number(row.latest_heartbeat_at); + const heartbeatAgeMs = Math.max(0, nowMs() - Number(row.latest_heartbeat_at ?? row.started_at ?? 0)); + const item = { + id: row.id, + requestId: row.request_id, + startedAt: Number(row.started_at ?? 0), + updatedAt: Number(row.updated_at ?? 0), + attempts: Number(row.attempts ?? 0), + heartbeatAt, + heartbeatAgeMs, + ageMs, + reason + }; + if (!dryRun) { + const message = heartbeatAt == null ? `agent run recovered from stale running state after ${ageMs}ms without heartbeat` : `agent run recovered from stale running state after heartbeat was stale for ${heartbeatAgeMs}ms`; + const completedAt = nowMs(); + const [update] = await pool2.query( + `UPDATE h5_agent_runs + SET status = 'failed', error_message = ?, updated_at = ?, completed_at = ? + WHERE id = ? + AND status = 'running' + AND started_at IS NOT NULL + AND COALESCE( + (SELECT MAX(created_at) + FROM h5_agent_run_events + WHERE run_id = h5_agent_runs.id AND event_type = 'worker_heartbeat'), + started_at + ) <= ?`, + [message, completedAt, completedAt, row.id, cutoff] + ); + if (Number(update?.affectedRows ?? 0) === 0) continue; + await appendEvent(row.id, "stale_recovered", { + reason, + staleMs: normalizedStaleMs, + ageMs, + heartbeatAt, + heartbeatAgeMs, + status: "failed", + error: message + }); + } + recovered.push(item); + } + return { + dryRun, + staleMs: normalizedStaleMs, + cutoff, + considered: rows.length, + recovered: dryRun ? 0 : recovered.length, + runs: recovered }; } async function dispatchQueuedRuns({ limit = maxConcurrentRuns } = {}) { + const staleRecovery = await recoverStaleRunningRuns({ + staleMs: runTimeoutMs, + limit: Math.max(maxConcurrentRuns, Number(limit) || maxConcurrentRuns), + dryRun: false, + reason: "worker_dispatch_stale_recovery" + }); const dispatchLimit = Math.max(1, Number(limit) || maxConcurrentRuns); const available = Math.max(0, maxConcurrentRuns - inFlight.size - queuedDispatches.length); if (available <= 0) { return { considered: 0, dispatched: 0, + staleRecovery, queue: await getQueueStatus() }; } @@ -6849,6 +7227,7 @@ function createAgentRunGateway({ return { considered: rows.length, dispatched: rows.length, + staleRecovery, queue: await getQueueStatus() }; } @@ -6857,18 +7236,233 @@ function createAgentRunGateway({ getRunForUser, dispatchRun, getQueueStatus, - dispatchQueuedRuns + dispatchQueuedRuns, + recoverStaleRunningRuns + }; +} + +// tool-gateway.mjs +import { spawn as nodeSpawn } from "node:child_process"; +import { EventEmitter } from "node:events"; +var CODE_EXECUTORS = /* @__PURE__ */ new Set(["aider", "openhands"]); +var DEFAULT_STDIO_LIMIT = 64 * 1024; +function envFlag2(value, fallback = false) { + const raw = String(value ?? "").trim().toLowerCase(); + if (!raw) return fallback; + return ["1", "true", "yes", "on"].includes(raw); +} +function positiveInteger2(value, fallback) { + const n = Number(value); + if (!Number.isFinite(n) || n <= 0) return fallback; + return Math.floor(n); +} +function normalizeExecutor(value, fallback = "aider") { + const normalized = String(value ?? fallback).trim().toLowerCase(); + if (CODE_EXECUTORS.has(normalized)) return normalized; + return fallback; +} +function csvSet(value) { + return new Set( + String(value ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean) + ); +} +function appendLimited(current, chunk, limit) { + const next = `${current}${Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk ?? "")}`; + if (next.length <= limit) return next; + return next.slice(next.length - limit); +} +function extractToolInstruction(userMessage) { + const content = userMessage?.content; + if (typeof content === "string") return content.trim(); + if (!Array.isArray(content)) { + return String(userMessage?.text ?? userMessage?.value ?? "").trim(); + } + return content.map((item) => { + if (typeof item === "string") return item; + if (item?.type === "text") return item.text ?? ""; + return ""; + }).map((item) => String(item ?? "").trim()).filter(Boolean).join("\n").trim(); +} +function createToolGateway({ + llmProviderService: llmProviderService2, + env = process.env, + spawnImpl = nodeSpawn +} = {}) { + const enabled = envFlag2(env.MEMIND_TOOL_GATEWAY_ENABLED, false); + const dryRun = envFlag2(env.MEMIND_TOOL_GATEWAY_DRY_RUN, false); + const defaultExecutor = normalizeExecutor(env.MEMIND_TOOL_GATEWAY_DEFAULT_EXECUTOR, "aider"); + const openhandsTaskTypes = csvSet( + env.MEMIND_TOOL_GATEWAY_OPENHANDS_TASK_TYPES ?? "repo_refactor,multi_file,complex_repo" + ); + const stdioLimit = positiveInteger2(env.MEMIND_TOOL_GATEWAY_STDIO_LIMIT, DEFAULT_STDIO_LIMIT); + function getStatus() { + return { + enabled, + dryRun, + protocol: "agent-run-v1", + executors: ["aider", "openhands"], + defaultExecutor, + openhandsTaskTypes: [...openhandsTaskTypes] + }; + } + function selectExecutor({ userMessage, taskType } = {}) { + const metadata = userMessage?.metadata ?? {}; + const runMetadata = metadata.memindRun ?? metadata.agentRun ?? {}; + const requested = normalizeExecutor(runMetadata.executor, ""); + if (requested) return requested; + const normalizedTaskType = String(taskType ?? runMetadata.taskType ?? "").trim().toLowerCase(); + if (openhandsTaskTypes.has(normalizedTaskType)) return "openhands"; + return defaultExecutor; + } + async function executeJob({ + runId, + requestId, + userId, + userMessage, + taskType, + cwd, + timeoutMs = 15 * 60 * 1e3 + } = {}) { + if (!enabled) { + throw new Error("Tool Gateway is disabled"); + } + if (!llmProviderService2?.getExecutorLaunchPlan) { + throw new Error("Tool Gateway missing llm provider service"); + } + const instruction = extractToolInstruction(userMessage); + if (!instruction) { + throw new Error("Tool Gateway job missing instruction"); + } + const executor = selectExecutor({ userMessage, taskType }); + const plan = await llmProviderService2.getExecutorLaunchPlan(executor, { + cwd, + mode: "headless", + instruction, + purpose: "default", + includeSecret: true + }); + if (!plan?.ok) { + throw new Error(plan?.message ?? `Tool Gateway launch plan unavailable for ${executor}`); + } + if (dryRun) { + return { + ok: true, + dryRun: true, + executor, + protocol: "agent-run-v1", + cwd: plan.cwd, + command: plan.command, + args: plan.args ?? [], + runId, + requestId, + userId + }; + } + return await new Promise((resolve, reject) => { + let stdout = ""; + let stderr = ""; + const child = spawnImpl(plan.command, plan.args ?? [], { + cwd: plan.cwd, + env: { ...process.env, ...plan.env ?? {} }, + stdio: ["ignore", "pipe", "pipe"] + }); + const cleanup = () => { + if (timer) clearTimeout(timer); + }; + const timer = setTimeout(() => { + child.kill("SIGTERM"); + const err = new Error(`Tool Gateway job timed out after ${timeoutMs}ms`); + err.code = "TOOL_GATEWAY_TIMEOUT"; + reject(err); + }, timeoutMs); + if (child.stdout instanceof EventEmitter) { + child.stdout.on("data", (chunk) => { + stdout = appendLimited(stdout, chunk, stdioLimit); + }); + } + if (child.stderr instanceof EventEmitter) { + child.stderr.on("data", (chunk) => { + stderr = appendLimited(stderr, chunk, stdioLimit); + }); + } + child.on("error", (err) => { + cleanup(); + reject(err); + }); + child.on("exit", (code, signal) => { + cleanup(); + if (code === 0) { + resolve({ + ok: true, + executor, + protocol: "agent-run-v1", + cwd: plan.cwd, + command: plan.command, + args: plan.args ?? [], + exitCode: code, + signal: signal ?? null, + stdout, + stderr + }); + return; + } + const err = new Error(`Tool Gateway executor ${executor} exited with ${signal ?? code}`); + err.code = "TOOL_GATEWAY_EXECUTOR_FAILED"; + err.executor = executor; + err.exitCode = code; + err.signal = signal ?? null; + err.stdout = stdout; + err.stderr = stderr; + reject(err); + }); + }); + } + return { + getStatus, + selectExecutor, + executeJob }; } // agent-run-routes.mjs -function envFlag2(value) { +function envFlag3(value) { return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase()); } +function parseUserIdSet(value) { + return new Set( + String(value ?? "").split(",").map((item) => item.trim()).filter(Boolean) + ); +} +function parseTaskTypeSet(value) { + return new Set( + String(value ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean) + ); +} +function hasExpectedFileValidation(userMessage) { + const metadata = userMessage?.metadata; + const runMetadata = metadata?.memindRun ?? metadata?.agentRun ?? {}; + const validation = runMetadata.validation ?? metadata?.toolGatewayValidation; + if (!validation || typeof validation !== "object" || Array.isArray(validation)) return false; + const expectedFile = validation.expectedFile ?? validation.expectedPath; + if (typeof expectedFile === "string" && expectedFile.trim()) return true; + if (expectedFile && typeof expectedFile === "object" && !Array.isArray(expectedFile)) { + const filePath = String(expectedFile.path ?? expectedFile.file ?? expectedFile.relativePath ?? "").trim(); + if (filePath) return true; + } + if (!Array.isArray(validation.expectedFiles)) return false; + return validation.expectedFiles.some((item) => { + if (typeof item === "string") return Boolean(item.trim()); + if (!item || typeof item !== "object" || Array.isArray(item)) return false; + return Boolean(String(item.path ?? item.file ?? item.relativePath ?? "").trim()); + }); +} function createPostAgentRunsHandler({ userAuth: userAuth2, agentRunGateway: agentRunGateway2, - codeRunsEnabled = envFlag2(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED) + codeRunsEnabled = envFlag3(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), + codeRunUserIds = parseUserIdSet(process.env.MEMIND_AGENT_CODE_RUNS_USER_IDS), + codeRunTaskTypes = parseTaskTypeSet(process.env.MEMIND_AGENT_CODE_RUN_TASK_TYPES), + requireCodeRunValidation = envFlag3(process.env.MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION) }) { return async function postAgentRuns(request, response) { try { @@ -6898,6 +7492,18 @@ function createPostAgentRunsHandler({ response.status(403).json({ message: "\u4EE3\u7801\u4EFB\u52A1\u7070\u5EA6\u672A\u5F00\u542F" }); return; } + if (toolMode === "code" && codeRunUserIds.size > 0 && !codeRunUserIds.has(request.currentUser.id)) { + response.status(403).json({ message: "\u5F53\u524D\u7528\u6237\u672A\u5F00\u542F\u4EE3\u7801\u4EFB\u52A1\u7070\u5EA6" }); + return; + } + if (toolMode === "code" && codeRunTaskTypes.size > 0 && (!taskType || !codeRunTaskTypes.has(taskType.toLowerCase()))) { + response.status(403).json({ message: "\u5F53\u524D\u4EE3\u7801\u4EFB\u52A1\u7C7B\u578B\u672A\u5F00\u542F\u7070\u5EA6" }); + return; + } + if (toolMode === "code" && requireCodeRunValidation && !hasExpectedFileValidation(userMessage)) { + response.status(400).json({ message: "\u4EE3\u7801\u4EFB\u52A1\u5FC5\u987B\u58F0\u660E\u4EA7\u7269\u6821\u9A8C\u89C4\u5219" }); + return; + } if (sessionId) { const owns = await userAuth2.ownsSession(request.currentUser.id, sessionId); if (!owns) { @@ -7018,8 +7624,8 @@ function createAgentRunEventsHandler({ } // tkmind-proxy.mjs -import fs5 from "node:fs"; -import path6 from "node:path"; +import fs6 from "node:fs"; +import path7 from "node:path"; import { Readable, Transform as Transform2, Writable } from "node:stream"; import { pipeline } from "node:stream/promises"; import { Agent, fetch as undiciFetch } from "undici"; @@ -7104,7 +7710,7 @@ data: ${JSON.stringify(data)} } // capabilities.mjs -import path2 from "node:path"; +import path3 from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; // policies.mjs @@ -7293,8 +7899,8 @@ var USER_API_ALLOWLIST = [ { method: "POST", pattern: /^\/agent\/harness_remember$/ } ]; function isNativeH5ApiPath(pathname) { - const path31 = String(pathname ?? ""); - return path31.startsWith("/mindspace/") || path31.startsWith("/plaza/"); + const path35 = String(pathname ?? ""); + return path35.startsWith("/mindspace/") || path35.startsWith("/plaza/"); } function evaluateProxyRequest(method, pathname, policies, { unrestricted = false } = {}) { if (unrestricted) return { allowed: true }; @@ -7320,7 +7926,7 @@ function evaluateProxyRequest(method, pathname, policies, { unrestricted = false function resolveSandboxMcpServerPath(overridePath) { const normalized = String(overridePath ?? "").trim(); if (normalized) return normalized; - return path2.join(path2.dirname(fileURLToPath2(import.meta.url)), "mindspace-sandbox-mcp.mjs"); + return path3.join(path3.dirname(fileURLToPath2(import.meta.url)), "mindspace-sandbox-mcp.mjs"); } function resolveSandboxMcpNodeExecPath(overridePath) { const normalized = String(overridePath ?? "").trim(); @@ -7526,7 +8132,7 @@ function sandboxDeveloperTools(capabilities) { function sandboxMcpTools(capabilities) { const tools = []; if (capabilities.static_publish) { - tools.push("read_file", "write_file", "edit_file", "create_dir"); + tools.push("read_file", "write_file", "edit_file", "create_dir", "generate_long_image"); if (capabilities.shell || capabilities.code_browse) tools.push("list_dir"); } if (capabilities.private_data_space) { @@ -7710,16 +8316,81 @@ function normalizeCapabilityPatch(patch) { } // user-publish.mjs -import fs3 from "node:fs"; -import path3 from "node:path"; +import fs4 from "node:fs"; +import path4 from "node:path"; import { fileURLToPath as fileURLToPath3 } from "node:url"; -var __dirname2 = path3.dirname(fileURLToPath3(import.meta.url)); + +// mindspace-canonical-url.mjs +var MINDSPACE_PUBLIC_ROOT = "MindSpace"; +var MINDSPACE_PUBLIC_ZONE = "public"; +function urlError(message, code, details = {}) { + return Object.assign(new Error(message), { code, ...details }); +} +function requiredSegment(value, field) { + const normalized = String(value ?? "").trim(); + if (!normalized) { + throw urlError(`${field} is required`, "invalid_mindspace_url_segment", { field }); + } + if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) { + throw urlError(`${field} must be a single URL segment`, "invalid_mindspace_url_segment", { + field + }); + } + return normalized; +} +function normalizeMindSpacePublicBaseUrl(publicBaseUrl) { + const normalized = String(publicBaseUrl ?? "").trim().replace(/\/+$/, ""); + if (!normalized) { + throw urlError("publicBaseUrl is required", "invalid_mindspace_public_base_url"); + } + return normalized; +} +function normalizeMindSpacePublicRelativePath(relativePath) { + const raw = String(relativePath ?? "").trim(); + if (!raw) { + throw urlError("relativePath is required", "invalid_mindspace_public_relative_path"); + } + if (raw.includes("\0") || raw.includes("\\")) { + throw urlError("relativePath must use URL-style forward slashes", "invalid_mindspace_public_relative_path"); + } + if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw) || raw.startsWith("//")) { + throw urlError("relativePath must not be an absolute URL", "invalid_mindspace_public_relative_path"); + } + const clean = raw.replace(/^\/+/, ""); + const parts = clean.split("/").filter(Boolean); + if (parts.length === 0 || parts.includes("..")) { + throw urlError("relativePath must not traverse parent directories", "invalid_mindspace_public_relative_path"); + } + return parts.join("/"); +} +function createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath }) { + const base = normalizeMindSpacePublicBaseUrl(publicBaseUrl); + const owner = requiredSegment(ownerKey, "ownerKey"); + const relative = normalizeMindSpacePublicRelativePath(relativePath); + const encodedRelative = relative.split("/").map(encodeURIComponent).join("/"); + return `${base}/${MINDSPACE_PUBLIC_ROOT}/${encodeURIComponent(owner)}/${encodedRelative}`; +} +function createMindSpaceUserRootUrl({ publicBaseUrl, ownerKey }) { + const base = normalizeMindSpacePublicBaseUrl(publicBaseUrl); + const owner = requiredSegment(ownerKey, "ownerKey"); + return `${base}/${MINDSPACE_PUBLIC_ROOT}/${encodeURIComponent(owner)}/`; +} +function createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey, filename }) { + const clean = normalizeMindSpacePublicRelativePath(filename); + const relative = clean.startsWith(`${MINDSPACE_PUBLIC_ZONE}/`) ? clean : `${MINDSPACE_PUBLIC_ZONE}/${clean}`; + return createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath: relative }); +} + +// user-publish.mjs +var __dirname2 = path4.dirname(fileURLToPath3(import.meta.url)); var PUBLISH_SKILL_NAME = "static-page-publish"; var DOCX_SKILL_NAME = "docx-generate"; +var LONG_IMAGE_SKILL_NAME = "long-image-download"; var PUBLISH_ROOT_DIR = "MindSpace"; var PUBLIC_ZONE_DIR = "public"; -var PUBLISH_SKILL_DIR = path3.join(__dirname2, "skills", PUBLISH_SKILL_NAME); -var DOCX_SKILL_DIR = path3.join(__dirname2, "skills", DOCX_SKILL_NAME); +var PUBLISH_SKILL_DIR = path4.join(__dirname2, "skills", PUBLISH_SKILL_NAME); +var DOCX_SKILL_DIR = path4.join(__dirname2, "skills", DOCX_SKILL_NAME); +var LONG_IMAGE_SKILL_DIR = path4.join(__dirname2, "skills", LONG_IMAGE_SKILL_NAME); var WORKSPACE_HINTS_FILENAME = ".tkmindhints"; var LEGACY_WORKSPACE_HINTS_FILENAME = ".goosehints"; function renderBrandingBlock(userAddressName) { @@ -7756,57 +8427,55 @@ function resolveUsernameSlug(user) { return String(user.username).trim().toLowerCase(); } function resolvePublishDir(h5Root, user) { - return path3.join(h5Root, PUBLISH_ROOT_DIR, resolvePublishKey(user)); + return path4.join(h5Root, PUBLISH_ROOT_DIR, resolvePublishKey(user)); } function resolveGoosedSandboxRoot(h5Root, user, env = process.env) { const canonicalRoot = String( env.GOOSED_SANDBOX_PUBLISH_ROOT ?? env.MEMIND_SHARED_PUBLISH_ROOT ?? "" ).trim(); if (canonicalRoot) { - return path3.join(path3.resolve(canonicalRoot), resolvePublishKey(user)); + return path4.join(path4.resolve(canonicalRoot), resolvePublishKey(user)); } return resolvePublishDir(h5Root, user); } function resolveLegacyPublishDir(h5Root, user) { if (!user?.username) return null; - return path3.join(h5Root, PUBLISH_ROOT_DIR, resolveUsernameSlug(user)); + return path4.join(h5Root, PUBLISH_ROOT_DIR, resolveUsernameSlug(user)); } function mergePublishTrees(sourceDir, targetDir) { - if (!fs3.existsSync(sourceDir)) return; - for (const entry of fs3.readdirSync(sourceDir, { withFileTypes: true })) { - const from = path3.join(sourceDir, entry.name); - const to = path3.join(targetDir, entry.name); + if (!fs4.existsSync(sourceDir)) return; + for (const entry of fs4.readdirSync(sourceDir, { withFileTypes: true })) { + const from = path4.join(sourceDir, entry.name); + const to = path4.join(targetDir, entry.name); if (entry.isDirectory()) { - fs3.mkdirSync(to, { recursive: true }); + fs4.mkdirSync(to, { recursive: true }); mergePublishTrees(from, to); - } else if (!fs3.existsSync(to)) { - fs3.copyFileSync(from, to); + } else if (!fs4.existsSync(to)) { + fs4.copyFileSync(from, to); } } } function migrateUserPublishDir(h5Root, user, legacyUsersRoot = null) { const target = resolvePublishDir(h5Root, user); - fs3.mkdirSync(target, { recursive: true }); + fs4.mkdirSync(target, { recursive: true }); const legacyDirs = [ resolveLegacyPublishDir(h5Root, user), - legacyUsersRoot ? path3.join(legacyUsersRoot, resolveUsernameSlug(user)) : null + legacyUsersRoot ? path4.join(legacyUsersRoot, resolveUsernameSlug(user)) : null ].filter(Boolean); for (const legacyDir of legacyDirs) { - if (path3.resolve(legacyDir) === path3.resolve(target)) continue; + if (path4.resolve(legacyDir) === path4.resolve(target)) continue; mergePublishTrees(legacyDir, target); } return target; } function buildPublicUrl(publicBaseUrl, publishKey, filename = "") { - const base = `${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(publishKey)}`; - if (!filename) return `${base}/`; - const clean = filename.replace(/^\/+/, ""); - return `${base}/${clean.split("/").map(encodeURIComponent).join("/")}`; + if (!filename) { + return createMindSpaceUserRootUrl({ publicBaseUrl, ownerKey: publishKey }); + } + return createMindSpacePublicUrl({ publicBaseUrl, ownerKey: publishKey, relativePath: filename }); } function buildPublicZonePageUrl(publicBaseUrl, publishKey, filename) { - const clean = String(filename ?? "").replace(/^\/+/, ""); - const relative = clean.startsWith(`${PUBLIC_ZONE_DIR}/`) ? clean : `${PUBLIC_ZONE_DIR}/${clean}`; - return buildPublicUrl(publicBaseUrl, publishKey, relative); + return createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey: publishKey, filename }); } function renderPublishSkill({ slug, username, publicBaseUrl, publishDir, displayName }) { const addressName = resolveUserAddressName({ displayName, username, slug }); @@ -7824,7 +8493,7 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u 1. **\u552F\u4E00\u53EF\u5199\u76EE\u5F55**\uFF1A\`${publishDir}\` 2. **\u7981\u6B62**\u4F7F\u7528\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u5982 \`/Users/...\`\u3001\`../\` \u8DF3\u51FA\u76EE\u5F55\uFF09 -3. **\u5141\u8BB8**\u5728\u672C\u76EE\u5F55\u5185\u4F7F\u7528 \`write_file\`\u3001\`edit_file\`\u3001\`read_file\`\u3001\`list_dir\`\u3001\`generate_docx\`\uFF1B**\u7981\u6B62**\u8BBF\u95EE\u6B64\u76EE\u5F55\u5916\u7684\u8DEF\u5F84\uFF08\u542B \`../\`\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\u9879\u76EE\u6839\u76EE\u5F55\uFF1B\u7CFB\u7EDF\u4F1A\u5728 OS \u5C42\u62E6\u622A\u8D8A\u754C\u8BBF\u95EE\uFF09 +3. **\u5141\u8BB8**\u5728\u672C\u76EE\u5F55\u5185\u4F7F\u7528 \`write_file\`\u3001\`edit_file\`\u3001\`read_file\`\u3001\`list_dir\`\u3001\`generate_docx\`\u3001\`generate_long_image\`\uFF1B**\u7981\u6B62**\u8BBF\u95EE\u6B64\u76EE\u5F55\u5916\u7684\u8DEF\u5F84\uFF08\u542B \`../\`\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\u9879\u76EE\u6839\u76EE\u5F55\uFF1B\u7CFB\u7EDF\u4F1A\u5728 OS \u5C42\u62E6\u622A\u8D8A\u754C\u8BBF\u95EE\uFF09 4. **\u7981\u6B62**\u5B50 Agent\u3001\u6269\u5C55\u7BA1\u7406\u3001\u4FEE\u6539\u5DE5\u4F5C\u533A\u5916\u6587\u4EF6 5. \u9875\u9762\u9ED8\u8BA4\u5199\u5165 \`public/\` \u5206\u533A\uFF0C\u4F7F\u7528\u76F8\u5BF9\u8DEF\u5F84\u5982 \`public/report.html\`\u3001\`public/assets/chart.png\` 6. \u751F\u6210 HTML \u540E\uFF0C\u5411\u7528\u6237\u63D0\u4F9B\u53EF\u8BBF\u95EE\u94FE\u63A5\uFF0C\u683C\u5F0F\uFF1A @@ -7838,17 +8507,20 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u 3. \u5728 \`\` \u5199\u5165 **mindspace-cover** \u5143\u6570\u636E\uFF08\u89C1\u4E0B\u6587\uFF0C\u5FC5\u987B\u4E0E\u9875\u9762\u4E3B\u9898\u4E00\u81F4\uFF09 4. \u9875\u9762\u5185\u8D44\u6E90\u4F7F\u7528**\u76F8\u5BF9\u8DEF\u5F84**\uFF08\`assets/foo.png\`\uFF09\uFF0C\u4E0D\u8981\u7528\u78C1\u76D8\u7EDD\u5BF9\u8DEF\u5F84 5. \u4FDD\u5B58 HTML \u540E\uFF0C\u670D\u52A1\u7AEF\u4F1A**\u7ACB\u5373**\u751F\u6210\u540C\u540D\u9884\u89C8\u56FE \`<\u6587\u4EF6\u540D>.thumbnail.svg\`\uFF08Agent \u4EA4\u4E92\u9636\u6BB5\u5373\u751F\u6548\uFF0C\u65E0\u9700\u7B49\u7528\u6237\u4FDD\u5B58\u5230\u300C\u6211\u7684\u7A7A\u95F4\u300D\uFF09 -6. \u4E0B\u8F7D\u6309\u94AE\u7684\u76F8\u5BF9\u94FE\u63A5\u76EE\u6807\uFF08\u5982 \`public/report.docx\`\uFF09\u5FC5\u987B\u901A\u8FC7 \`generate_docx\` \u843D\u76D8\uFF0C\u4E14 basename \u4E0E HTML \u4E00\u81F4 -7. \u5B8C\u6210\u540E\u6309\u300C\u56DE\u590D\u683C\u5F0F\u300D\u8FD4\u56DE\u53EF\u70B9\u51FB\u94FE\u63A5 +6. \u9ED8\u8BA4\u53EA\u751F\u6210 HTML\uFF1B\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6 +7. \u82E5\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u4E0B\u8F7D\u6309\u94AE\u7684\u76F8\u5BF9\u94FE\u63A5\u76EE\u6807\uFF08\u5982 \`public/report.docx\`\uFF09\u5FC5\u987B\u901A\u8FC7 \`generate_docx\` \u843D\u76D8\uFF0C\u4E14 basename \u4E0E HTML \u4E00\u81F4 +8. \u82E5\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`long-image-download\`\uFF0C\u8C03\u7528 \`generate_long_image\` \u751F\u6210\u5E76\u786E\u8BA4 \`public/<\u9875\u9762\u540D>.long.png\` \u5DF2\u5B58\u5728 +9. \u5B8C\u6210\u540E\u6309\u300C\u56DE\u590D\u683C\u5F0F\u300D\u8FD4\u56DE\u53EF\u70B9\u51FB\u94FE\u63A5 -## \u4F34\u751F\u4E0B\u8F7D\u6587\u4EF6\uFF08\u5FC5\u987B\uFF09 +## \u6309\u9700\u4F34\u751F\u4E0B\u8F7D\u6587\u4EF6 +- \u9ED8\u8BA4\u4E0D\u751F\u6210\u4F34\u751F\u6587\u4EF6\uFF1B\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u4E0B\u8F7D\u9644\u4EF6\u65F6\u624D\u751F\u6210 - \u76F8\u5BF9\u8DEF\u5F84\u4E0B\u8F7D\u94FE\u63A5\uFF08\`.docx\` / \`.pdf\` \u7B49\uFF09\u5FC5\u987B\u5728 HTML \u540C\u76EE\u5F55\u6216\u5B50\u76EE\u5F55\u771F\u5B9E\u5B58\u5728 - Word \u6587\u6863\u5FC5\u987B\u7528 sandbox-fs \u7684 \`generate_docx\` \u751F\u6210\uFF1B\u7981\u6B62\u7528 \`computercontroller\` / shell \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6\u4F5C\u4E3A\u4EA4\u4ED8\u4F9D\u636E - \u6539 HTML \u6587\u4EF6\u540D\u65F6\u540C\u6B65 rename/copy \u4F34\u751F\u6587\u4EF6\uFF1B\u4ECE \`oa/\` \u590D\u5236\u5230 \`public/\` \u518D\u94FE\u63A5 - \u53EF\u8DD1 \`node scripts/check-mindspace-public-links.mjs\` \u81EA\u68C0 -## Word \u4E0B\u8F7D\u9875\uFF08\u5FC5\u987B\uFF09 +## Word \u4E0B\u8F7D\u9875\uFF08\u6309\u9700\uFF09 - \u5982\u679C\u7528\u6237\u660E\u786E\u8981\u6C42\u9875\u9762\u91CC**\u53EF\u4E0B\u8F7D Word / docx**\uFF0C\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\` - \u7528 \`.agents/skills/docx-generate/generate_docx.py\` \u751F\u6210 \`public/\u6587\u4EF6\u540D.docx\` @@ -7869,6 +8541,7 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u - \u6807\u9898\u7528\u9875\u9762\u771F\u5B9E\u4E3B\u9898\uFF08\u4E0D\u8981\u7528\u300C\u70B9\u51FB\u8FD9\u91CC\u300D\u300C\u94FE\u63A5\u300D\uFF09 - \u53EF\u540C\u65F6\u7ED9\u51FA\u672C\u5730\u76F8\u5BF9\u8DEF\u5F84\uFF08\u5982 \`public/pilates-618.html\`\uFF09\u4E0E\u516C\u7F51\u94FE\u63A5 - \u8BF4\u660E\uFF1A\u9759\u6001\u6587\u4EF6\u4FDD\u5B58\u5373\u751F\u6548\uFF0C**\u65E0\u9700\u91CD\u542F** +- \u82E5\u751F\u6210\u4E86\u957F\u56FE\uFF0C\u540C\u65F6\u7ED9 \`[\u957F\u56FE\u9884\u89C8](.../public/pilates-618.long.png)\` \u548C \`[\u4E0B\u8F7D\u957F\u56FE](.../public/pilates-618.html?download=long-image)\` ## \u4FE1\u606F\u6D41\u9884\u89C8\u56FE\uFF08\u5FC5\u987B\uFF09 @@ -7903,10 +8576,10 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u - \u5B8C\u6574 \`\`\uFF0C\`lang="zh-CN"\` - \u79FB\u52A8\u7AEF\u53CB\u597D\uFF1A\`\` - \u6DF1\u8272/\u6D45\u8272\u4E0E\u5185\u5BB9\u4E00\u81F4\uFF1B\u4E3B\u8272\u5728 CSS \u4E0E mindspace-cover \u4E2D\u4FDD\u6301\u4E00\u81F4 -- \u9875\u811A\u5E73\u53F0\u8054\u7CFB\u884C**\u5FC5\u987B**\u4F7F\u7528 \`data-mindspace-page-tag="platform-brand"\` \u6807\u8BB0\uFF0C\u4E14\u90AE\u7BB1/\u57DF\u540D\u53EA\u7528 **tkmind.cn**\uFF08\u5982 \`contact@tkmind.cn\`\uFF09\uFF0C**\u7981\u6B62** \`tkmind.ai\` +- \u9875\u811A\u5E73\u53F0\u54C1\u724C\u884C**\u5FC5\u987B**\u4F7F\u7528 \`data-mindspace-page-tag="platform-brand"\` \u6807\u8BB0\uFF0C\u663E\u793A\u4E3A **TKMind \xB7 \u667A\u8DA3**\uFF0C**\u7981\u6B62**\u4F7F\u7528\u90AE\u7BB1\u6216 \`tkmind.ai\` \`\`\`html -

\u{1F4E7} contact@tkmind.cn

+

TKMind \xB7 \u667A\u8DA3

\`\`\` - \u5E26 \`data-mindspace-page-tag\` \u7684\u533A\u57DF\u4E3A\u5E73\u53F0\u56FA\u5B9A\u4FE1\u606F\uFF1A\u7528\u6237\u5728\u7F16\u8F91\u6A21\u5F0F\u4E2D\u4E0D\u53EF\u89C1\u3001\u4E0D\u53EF\u6539\uFF1B\u9884\u89C8\u4E0E\u53D1\u5E03\u540E\u6B63\u5E38\u663E\u793A @@ -7924,6 +8597,7 @@ description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u - shell \u4EC5\u7528\u4E8E\u672C\u76EE\u5F55\u5185\u6574\u7406\u6587\u4EF6/\u7B80\u5355\u811A\u672C\uFF1B\u4E0D\u8981 \`rm -rf\` \u8D8A\u754C\u8DEF\u5F84\u3001\u4E0D\u8981\u5B89\u88C5\u7CFB\u7EDF\u7EA7\u4F9D\u8D56 - **\u7981\u6B62**\u5728 HTML \u4E2D\u7528 \`data:...;base64,...\` \u5185\u5D4C Word/PDF\uFF1B\u4E8C\u8FDB\u5236\u6587\u4EF6\u5355\u72EC\u843D\u76D8\u540E\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\uFF08\u89C1 \`docx-generate\` \u6280\u80FD\uFF09 - \u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF0C\u7981\u6B62\u8DF3\u8FC7 \`docx-generate\` \u76F4\u63A5\u5728 HTML \u4E2D\u4F2A\u9020\u4E0B\u8F7D\u94FE\u63A5 +- \u7528\u6237\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\u65F6\uFF0C\u7981\u6B62\u8DF3\u8FC7 \`long-image-download\` \u6216\u628A \`.thumbnail.svg\` \u5F53\u6210\u957F\u56FE ## \u793A\u4F8B @@ -7964,6 +8638,13 @@ ${renderBrandingBlock(addressName)} - \u82E5\u7528\u6237\u8981\u6C42\u9875\u9762\u91CC\u53EF\u4E0B\u8F7D Word / docx\uFF1A\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\` - \u5148\u751F\u6210\u5E76\u786E\u8BA4 \`public/*.docx\` \u5DF2\u5B58\u5728\uFF0C\u518D\u5199 \`public/*.html\` - HTML \u4E2D\u53EA\u80FD\u7528\u540C\u76EE\u5F55\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\uFF0C\u7981\u6B62\u53EA\u5199\u4E0B\u8F7D\u6309\u94AE\u5374\u6CA1\u6709\u5148\u628A \`.docx\` \u843D\u76D8 + +## \u957F\u56FE\u4E0B\u8F7D + +- \u82E5\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\uFF1A\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`long-image-download\` +- \u8C03\u7528 \`generate_long_image\` \u751F\u6210 \`public/<\u9875\u9762\u540D>.long.png\` +- \u5148\u786E\u8BA4 \`public/*.long.png\` \u5DF2\u5B58\u5728\uFF0C\u518D\u56DE\u590D \`[\u957F\u56FE\u9884\u89C8](...long.png)\` \u548C \`[\u4E0B\u8F7D\u957F\u56FE](...html?download=long-image)\` +- \`.thumbnail.svg\` \u53EA\u662F\u4FE1\u606F\u6D41\u5C01\u9762\uFF0C\u4E0D\u80FD\u5F53\u6210\u957F\u56FE `; } function buildSandboxSessionConstraints({ baseConstraints, developerTools = [] }) { @@ -7998,12 +8679,15 @@ function buildSandboxSessionConstraints({ baseConstraints, developerTools = [] } "## \u751F\u6210 / \u53D1\u5E03 HTML \u9875\u9762", "- \u4F60\u6709 write_file/edit_file \u5DE5\u5177\uFF1A**\u5FC5\u987B\u7531\u4F60**\u5199\u5165 `public/xxx.html`\uFF08\u6216\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55 `.html`\uFF09", "- \u5F00\u59CB\u524D\u6267\u884C load_skill \u2192 `static-page-publish`\uFF0C\u6309\u6280\u80FD\u8BF4\u660E\u5199\u5165 mindspace-cover \u5143\u6570\u636E", - "- \u5982\u679C\u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF1A\u5148 `load_skill` \u2192 `docx-generate`\uFF0C\u751F\u6210\u5E76\u786E\u8BA4 `public/*.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u5199 HTML \u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5", + "- \u9ED8\u8BA4\u53EA\u751F\u6210 HTML\uFF1B\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6", + "- \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF1A\u5148 `load_skill` \u2192 `docx-generate`\uFF0C\u751F\u6210\u5E76\u786E\u8BA4 `public/*.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u5199 HTML \u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5", + "- \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\u65F6\uFF1A\u5148 `load_skill` \u2192 `long-image-download`\uFF0C\u8C03\u7528 `generate_long_image` \u751F\u6210\u5E76\u786E\u8BA4 `public/*.long.png` \u5DF2\u5B58\u5728\uFF0C\u518D\u56DE\u590D\u957F\u56FE\u9884\u89C8\u4E0E\u4E0B\u8F7D\u94FE\u63A5", "- **\u7981\u6B62**\u7528 shell / cat / heredoc / echo / cp \u5199\u5165 HTML\uFF1Bshell \u5728\u5BB9\u5668\u5185\u6267\u884C\uFF0C\u6587\u4EF6\u4E0D\u4F1A\u51FA\u73B0\u5728\u516C\u7F51 MindSpace \u8DEF\u5F84", "- \u7528 `apps__create_app` \u8BBE\u8BA1\u9875\u9762\u65F6\uFF0C\u6700\u540E\u4ECD\u8981\u6309 `static-page-publish` skill \u628A\u5185\u5BB9 write_file \u843D\u5230 `public/`\uFF0C\u624D\u6709\u516C\u7F51\u94FE\u63A5", "- **\u7981\u6B62**\u8BA9\u7528\u6237\u624B\u52A8\u4FDD\u5B58\u5230 public \u6216\u8BF4\u65E0\u6CD5\u751F\u6210\u9875\u9762\uFF08\u9664\u975E write_file \u8C03\u7528\u5931\u8D25\uFF09", "- \u5B8C\u6210\u540E\u56DE\u590D `[\u9875\u9762\u6807\u9898](\u516C\u7F51URL)` \u53EF\u70B9\u51FB\u94FE\u63A5\uFF1B\u5199\u5165 `public/` \u65F6 URL \u5FC5\u987B\u542B `/public/` \u8DEF\u5F84\u6BB5\uFF1B\u6309\u672C\u4F1A\u8BDD\u7ED9\u51FA\u7684\u516C\u7F51\u524D\u7F00\u6A21\u677F\u62FC\u63A5\u771F\u5B9E\u5730\u5740", - "- \u4E0B\u8F7D\u9644\u4EF6\uFF1AWord \u7528 `generate_docx` \u5199\u5165 `public/*.docx`\uFF0C\u76F8\u5BF9\u94FE\u63A5\u6587\u4EF6\u5FC5\u987B\u5DF2\u5728 `public/` \u843D\u76D8\uFF0Cbasename \u4E0E HTML \u4E00\u81F4" + "- \u6309\u9700\u4E0B\u8F7D\u9644\u4EF6\uFF1AWord \u7528 `generate_docx` \u5199\u5165 `public/*.docx`\uFF0C\u76F8\u5BF9\u94FE\u63A5\u6587\u4EF6\u5FC5\u987B\u5DF2\u5728 `public/` \u843D\u76D8\uFF0Cbasename \u4E0E HTML \u4E00\u81F4", + "- \u6309\u9700\u957F\u56FE\u9644\u4EF6\uFF1A\u7528 `generate_long_image` \u5199\u5165 `public/*.long.png`\uFF0C\u4E0D\u8981\u628A `.thumbnail.svg` \u5F53\u6210\u957F\u56FE" ); return lines.join("\n"); } @@ -8021,7 +8705,9 @@ function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, di "- **\u7981\u6B62**\uFF1A\u8BBF\u95EE `assets/` \u5185\u90E8\u8DEF\u5F84\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\u4E3B\u673A\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u7981\u6B62\u7528\u516C\u7F51 URL \u5217\u76EE\u5F55\u6216\u8BFB CSV", "- **\u8DEF\u5F84\u89C4\u5219**\uFF1A\u53EA\u7528\u76F8\u5BF9\u8DEF\u5F84\uFF1B\u7981\u6B62 `../`\uFF1B\u5DE5\u4F5C\u533A\u5916\u7684\u8DEF\u5F84\u4F1A\u88AB\u7CFB\u7EDF\u62D2\u7EDD\uFF08OS \u5C42\u5F3A\u5236\uFF0C\u975E\u8F6F\u7EA6\u675F\uFF09", "- **\u751F\u6210\u9875\u9762\uFF08\u5FC5\u987B\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u5148 `load_skill` \u2192 `static-page-publish`\uFF0C\u518D\u7528 `write_file`/`edit_file` \u5199\u5165 `public/\u9875\u9762.html`", - "- **Word \u4E0B\u8F7D\u9875\uFF08\u5FC5\u987B\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u82E5\u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate`\uFF0C\u751F\u6210\u5E76\u786E\u8BA4 `public/*.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u5728 HTML \u4E2D\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5", + "- **\u9ED8\u8BA4\u53EA\u751F\u6210 HTML**\uFF1A\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6", + "- **Word \u4E0B\u8F7D\u9875\uFF08\u6309\u9700\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u82E5\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate`\uFF0C\u751F\u6210\u5E76\u786E\u8BA4 `public/*.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u5728 HTML \u4E2D\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5", + "- **\u957F\u56FE\u4E0B\u8F7D\uFF08\u6309\u9700\u4EB2\u81EA\u5B8C\u6210\uFF09**\uFF1A\u82E5\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `long-image-download`\uFF0C\u8C03\u7528 `generate_long_image` \u751F\u6210\u5E76\u786E\u8BA4 `public/*.long.png` \u5DF2\u5B58\u5728", "- **\u7528\u6237\u53EF\u89C1\u56DE\u590D**\uFF1A\u4E0D\u8981\u5411\u7528\u6237\u590D\u8FF0 load_skill\u3001\u6280\u80FD\u66F4\u65B0\u3001\u9875\u811A\u6807\u8BB0\u3001mindspace-cover \u7B49\u5185\u90E8\u5B9E\u73B0\uFF1B\u5B8C\u6210\u540E\u76F4\u63A5\u7ED9\u51FA\u9875\u9762\u94FE\u63A5\u6216\u7ED3\u679C", "- **\u7981\u6B62**\u7528 shell \u5199\u5165 HTML\uFF1B**\u7981\u6B62**\u8BA9\u7528\u6237\u300C\u624B\u52A8\u4FDD\u5B58\u5230 public \u76EE\u5F55\u300D\u6216\u8BF4\u300C\u6211\u65E0\u6CD5\u751F\u6210\u9875\u9762\u300D\u2014\u2014\u9664\u975E write_file \u5DF2\u5931\u8D25\u5E76\u62A5\u544A\u9519\u8BEF", "- \u5B8C\u6210\u540E\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 `[\u6807\u9898](URL)`\uFF1B\u5199\u5165 `public/\u9875\u9762.html` \u65F6 URL \u4E3A `.../MindSpace/<\u7528\u6237ID>/public/\u9875\u9762.html`", @@ -8029,37 +8715,47 @@ function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, di "- **\u9700\u8981\u67E5\u5B9E\u65F6/\u771F\u5B9E\u4E16\u754C\u4FE1\u606F\u65F6**\uFF1A\u5148 `load_skill` \u2192 `web`\uFF0C\u4F18\u5148 `web_search`/`fetch_url`\uFF1B\u56FD\u5185 google.com \u4E0D\u53EF\u8FBE\uFF0C\u591A\u6B21\u65E0\u679C\u65F6\u6539\u8D70 Bing/360 \u7B49\u53EF\u8FBE\u641C\u7D22\u6E90", "- **\u7981\u6B62**\u8BA9\u7528\u6237\u300C\u624B\u52A8\u4FDD\u5B58\u5230 public \u76EE\u5F55\u300D\u6216\u8BF4\u300C\u6211\u65E0\u6CD5\u751F\u6210\u9875\u9762\u300D\u2014\u2014\u9664\u975E write_file \u5DF2\u5931\u8D25\u5E76\u62A5\u544A\u9519\u8BEF", "- \u5B8C\u6210\u540E\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 `[\u6807\u9898](URL)`\uFF1B\u5199\u5165 `public/\u9875\u9762.html` \u65F6 URL \u4E3A `.../MindSpace/<\u7528\u6237ID>/public/\u9875\u9762.html`\uFF0C\u6309\u6A21\u677F\u62FC\u771F\u5B9E\u5730\u5740", - "- \u4E0B\u8F7D\u94FE\u63A5\uFF08\u5982 `report.docx`\uFF09\u5FC5\u987B\u4E0E HTML \u540C\u76EE\u5F55\u4E14\u6587\u4EF6\u540D\u4E00\u81F4\uFF1BWord \u5FC5\u987B\u7528 sandbox-fs `generate_docx` \u751F\u6210\uFF0C\u4E0D\u8981\u7528 `computercontroller` / shell \u4F5C\u4E3A\u4EA4\u4ED8\u4F9D\u636E", + "- \u6309\u9700\u4E0B\u8F7D\u94FE\u63A5\uFF08\u5982 `report.docx`\uFF09\u5FC5\u987B\u4E0E HTML \u540C\u76EE\u5F55\u4E14\u6587\u4EF6\u540D\u4E00\u81F4\uFF1BWord \u5FC5\u987B\u7528 sandbox-fs `generate_docx` \u751F\u6210\uFF0C\u4E0D\u8981\u7528 `computercontroller` / shell \u4F5C\u4E3A\u4EA4\u4ED8\u4F9D\u636E", + "- \u6309\u9700\u957F\u56FE\u94FE\u63A5\uFF1A\u751F\u6210 `report.long.png` \u540E\u7ED9 `[\u957F\u56FE\u9884\u89C8](.../report.long.png)`\uFF0C\u4E0B\u8F7D\u7528 `[\u4E0B\u8F7D\u957F\u56FE](.../report.html?download=long-image)`", `- \u53D1\u5E03\u6280\u80FD\uFF1A\`${PUBLISH_SKILL_NAME}\`\uFF08\u751F\u6210\u9875\u9762\u524D\u5E94 load_skill\uFF09` ].join("\n"); } function ensureWorkspaceHintsInstalled(publishDir, context) { - const hintsPath = path3.join(publishDir, WORKSPACE_HINTS_FILENAME); - const legacyPath = path3.join(publishDir, LEGACY_WORKSPACE_HINTS_FILENAME); + const hintsPath = path4.join(publishDir, WORKSPACE_HINTS_FILENAME); + const legacyPath = path4.join(publishDir, LEGACY_WORKSPACE_HINTS_FILENAME); const content = renderWorkspaceHints(context); - fs3.writeFileSync(hintsPath, content, "utf8"); - if (fs3.existsSync(legacyPath)) { - fs3.unlinkSync(legacyPath); + fs4.writeFileSync(hintsPath, content, "utf8"); + if (fs4.existsSync(legacyPath)) { + fs4.unlinkSync(legacyPath); } return hintsPath; } function ensureDocxSkillInstalled(publishDir) { - if (!fs3.existsSync(DOCX_SKILL_DIR)) return null; - const skillRoot = path3.join(publishDir, ".agents", "skills", DOCX_SKILL_NAME); - fs3.rmSync(skillRoot, { recursive: true, force: true }); - fs3.mkdirSync(path3.dirname(skillRoot), { recursive: true }); - fs3.cpSync(DOCX_SKILL_DIR, skillRoot, { recursive: true }); - return path3.join(skillRoot, "SKILL.md"); + if (!fs4.existsSync(DOCX_SKILL_DIR)) return null; + const skillRoot = path4.join(publishDir, ".agents", "skills", DOCX_SKILL_NAME); + fs4.rmSync(skillRoot, { recursive: true, force: true }); + fs4.mkdirSync(path4.dirname(skillRoot), { recursive: true }); + fs4.cpSync(DOCX_SKILL_DIR, skillRoot, { recursive: true }); + return path4.join(skillRoot, "SKILL.md"); +} +function ensureLongImageSkillInstalled(publishDir) { + if (!fs4.existsSync(LONG_IMAGE_SKILL_DIR)) return null; + const skillRoot = path4.join(publishDir, ".agents", "skills", LONG_IMAGE_SKILL_NAME); + fs4.rmSync(skillRoot, { recursive: true, force: true }); + fs4.mkdirSync(path4.dirname(skillRoot), { recursive: true }); + fs4.cpSync(LONG_IMAGE_SKILL_DIR, skillRoot, { recursive: true }); + return path4.join(skillRoot, "SKILL.md"); } function ensurePublishSkillInstalled(publishDir, context) { ensureDocxSkillInstalled(publishDir); - const skillRoot = path3.join(publishDir, ".agents", "skills", PUBLISH_SKILL_NAME); - fs3.mkdirSync(skillRoot, { recursive: true }); - const skillPath = path3.join(skillRoot, "SKILL.md"); + ensureLongImageSkillInstalled(publishDir); + const skillRoot = path4.join(publishDir, ".agents", "skills", PUBLISH_SKILL_NAME); + fs4.mkdirSync(skillRoot, { recursive: true }); + const skillPath = path4.join(skillRoot, "SKILL.md"); const content = renderPublishSkill(context); - const existing = fs3.existsSync(skillPath) ? fs3.readFileSync(skillPath, "utf8") : ""; + const existing = fs4.existsSync(skillPath) ? fs4.readFileSync(skillPath, "utf8") : ""; if (existing !== content) { - fs3.writeFileSync(skillPath, content, "utf8"); + fs4.writeFileSync(skillPath, content, "utf8"); } return skillPath; } @@ -8075,7 +8771,7 @@ function ensureUserPublishLayout({ const username = user.username ? resolveUsernameSlug(user) : slug; const displayName = user.displayName ?? user.display_name ?? null; const publishDir = resolvePublishDir(h5Root, user); - fs3.mkdirSync(path3.join(publishDir, "assets"), { recursive: true }); + fs4.mkdirSync(path4.join(publishDir, "assets"), { recursive: true }); const context = { slug, username, displayName, publicBaseUrl, publishDir }; if (installWorkspaceHints) { ensureWorkspaceHintsInstalled(publishDir, context); @@ -8094,8 +8790,8 @@ function ensureUserPublishLayout({ } // user-memory-profile.mjs -import fs4 from "node:fs"; -import path4 from "node:path"; +import fs5 from "node:fs"; +import path5 from "node:path"; var USER_MEMORY_PROFILE_FILENAME = ".tkmind-profile.json"; var USER_MEMORY_PROFILE_VERSION = 1; var DEFAULT_TIMEZONE = "Asia/Shanghai"; @@ -8119,13 +8815,13 @@ function buildInitialUserMemoryProfile({ }; } function resolveUserMemoryProfilePath(workspaceRoot) { - return path4.join(workspaceRoot, USER_MEMORY_PROFILE_FILENAME); + return path5.join(workspaceRoot, USER_MEMORY_PROFILE_FILENAME); } function loadUserMemoryProfile(workspaceRoot) { const profilePath = resolveUserMemoryProfilePath(workspaceRoot); - if (!fs4.existsSync(profilePath)) return null; + if (!fs5.existsSync(profilePath)) return null; try { - const raw = fs4.readFileSync(profilePath, "utf8"); + const raw = fs5.readFileSync(profilePath, "utf8"); const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return null; return parsed; @@ -8145,7 +8841,7 @@ function ensureUserMemoryProfile(workspaceRoot, context) { displayName: nextDisplayName, updatedAt: Date.now() }; - fs4.writeFileSync(profilePath, `${JSON.stringify(updated, null, 2)} + fs5.writeFileSync(profilePath, `${JSON.stringify(updated, null, 2)} `, "utf8"); return updated; } @@ -8157,7 +8853,7 @@ function ensureUserMemoryProfile(workspaceRoot, context) { username: context.username, slug: context.slug }); - fs4.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)} + fs5.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)} `, "utf8"); return profile; } @@ -8503,7 +9199,7 @@ function renderCodeExecutorGuidance(sessionPolicy) { } // session-reconcile.mjs -import path5 from "node:path"; +import path6 from "node:path"; function extensionName(config) { return config?.name ?? null; } @@ -8549,7 +9245,7 @@ function extensionsNeedingRefresh(currentExtensions, desiredExtensions) { } function samePath(left, right) { if (!left || !right) return false; - return path5.resolve(left) === path5.resolve(right); + return path6.resolve(left) === path6.resolve(right); } async function readJson(upstream) { const text = await upstream.text(); @@ -8683,11 +9379,11 @@ import crypto4 from "node:crypto"; function createImgproxySigner(key, salt) { const keyBuf = Buffer.from(key, "hex"); const saltBuf = Buffer.from(salt, "hex"); - function signPath(path31) { - const signaturePayload = Buffer.concat([saltBuf, Buffer.from(path31)]); + function signPath(path35) { + const signaturePayload = Buffer.concat([saltBuf, Buffer.from(path35)]); const signature = crypto4.createHmac("sha256", keyBuf).update(signaturePayload).digest(); const signatureB64 = signature.toString("base64url"); - return `/${signatureB64}${path31}`; + return `/${signatureB64}${path35}`; } function buildUrl(baseUrl, storagePath, preset = "display") { if (!baseUrl || !storagePath) { @@ -8959,7 +9655,12 @@ function createRuntimeRouter({ key("worker", workerId, "metrics_sampled_at"), key("worker", workerId, "last_first_token_ms"), key("worker", workerId, "last_first_token_at"), - key("worker", workerId, "first_token_count") + key("worker", workerId, "first_token_count"), + key("worker", workerId, "heartbeat_source"), + key("worker", workerId, "heartbeat_ok"), + key("worker", workerId, "heartbeat_status_code"), + key("worker", workerId, "heartbeat_latency_ms"), + key("worker", workerId, "heartbeat_error") ]).catch(() => []) : []; const firstToken5m = await firstTokenWindowStats(client, workerId, 5 * 60 * 1e3); const firstToken1h = await firstTokenWindowStats(client, workerId, 60 * 60 * 1e3); @@ -8987,6 +9688,11 @@ function createRuntimeRouter({ lastFirstTokenMs: readNumber(values?.[18]), lastFirstTokenAt: values?.[19] ? Number(values[19]) : null, firstTokenCount: readNumber(values?.[20]), + heartbeatSource: values?.[21] ?? null, + heartbeatOk: values?.[22] == null ? null : /^(1|true|yes)$/i.test(String(values[22])), + heartbeatStatusCode: readNumber(values?.[23]), + heartbeatLatencyMs: readNumber(values?.[24]), + heartbeatError: values?.[25] || null, firstToken5m, firstToken1h, score: workerScoreFromValues(values) @@ -9073,10 +9779,10 @@ function publicHtmlExistsForUser(owner, relativePath, currentUser) { if (normalizedOwner !== normalizedUserId && normalizedOwner !== normalizedUsername) return true; const normalizedRelativePath = normalizeStaticHtmlRelativePath(relativePath); if (!normalizedRelativePath || !normalizedRelativePath.toLowerCase().endsWith(".html")) return false; - const root = path6.resolve(process.cwd(), PUBLISH_ROOT_DIR, normalizedOwner); - const target = path6.resolve(root, normalizedRelativePath); - if (target !== root && !target.startsWith(`${root}${path6.sep}`)) return false; - return fs5.existsSync(target) && fs5.statSync(target).isFile(); + const root = path7.resolve(process.cwd(), PUBLISH_ROOT_DIR, normalizedOwner); + const target = path7.resolve(root, normalizedRelativePath); + if (target !== root && !target.startsWith(`${root}${path7.sep}`)) return false; + return fs6.existsSync(target) && fs6.statSync(target).isFile(); } function sanitizeOwnPublicHtmlUrl(publicUrl2, owner, rawRelativePath, currentUser) { const relativePath = decodePathSegment(rawRelativePath); @@ -9084,7 +9790,7 @@ function sanitizeOwnPublicHtmlUrl(publicUrl2, owner, rawRelativePath, currentUse if (!publicHtmlExistsForUser(owner, canonicalRelativePath, currentUser)) { return { ok: false, - notice: buildMissingPublicHtmlNotice(path6.posix.basename(canonicalRelativePath || relativePath)) + notice: buildMissingPublicHtmlNotice(path7.posix.basename(canonicalRelativePath || relativePath)) }; } return { @@ -10297,9 +11003,9 @@ function createTkmindProxy({ // user-auth.mjs import crypto6 from "node:crypto"; -import fs8 from "node:fs"; +import fs9 from "node:fs"; import net from "node:net"; -import path9 from "node:path"; +import path10 from "node:path"; import { Algorithm as Argon2Algorithm, hashRawSync as argon2HashRawSync } from "@node-rs/argon2"; // billing.mjs @@ -10668,11 +11374,11 @@ function createRechargeService(pool2, { userAuth: userAuth2, wechatPay, config = } // user-space.mjs -import fs6 from "node:fs"; -import path7 from "node:path"; +import fs7 from "node:fs"; +import path8 from "node:path"; var UPLOAD_ZONE_CODES = ["oa", "public"]; function resolveMindspaceStorageRoot(h5Root, env = process.env) { - return path7.resolve(env.MINDSPACE_STORAGE_ROOT ?? path7.join(h5Root, "data", "mindspace")); + return path8.resolve(env.MINDSPACE_STORAGE_ROOT ?? path8.join(h5Root, "data", "mindspace")); } function resolveUserWorkspaceRoot(h5Root, user) { return resolvePublishDir(h5Root, user); @@ -10691,37 +11397,37 @@ function renderUserSpaceBrandingBlock(userAddressName) { `; } function resolveZoneDir(workspaceRoot, categoryCode) { - return path7.join(workspaceRoot, categoryCode); + return path8.join(workspaceRoot, categoryCode); } function resolveZoneFilePath(workspaceRoot, categoryCode, filename) { - return path7.join(resolveZoneDir(workspaceRoot, categoryCode), filename); + return path8.join(resolveZoneDir(workspaceRoot, categoryCode), filename); } function zoneLabel(categoryCode) { return SYSTEM_CATEGORIES.find((item) => item.code === categoryCode)?.name ?? categoryCode; } function ensureUserZoneDirs(workspaceRoot) { for (const code of UPLOAD_ZONE_CODES) { - fs6.mkdirSync(resolveZoneDir(workspaceRoot, code), { recursive: true }); + fs7.mkdirSync(resolveZoneDir(workspaceRoot, code), { recursive: true }); } } function mirrorAssetToZone2({ workspaceRoot, categoryCode, filename, sourcePath }) { if (!UPLOAD_ZONE_CODES.includes(categoryCode)) return null; - if (!sourcePath || !fs6.existsSync(sourcePath)) return null; + if (!sourcePath || !fs7.existsSync(sourcePath)) return null; ensureUserZoneDirs(workspaceRoot); const dest = resolveZoneFilePath(workspaceRoot, categoryCode, filename); - fs6.mkdirSync(path7.dirname(dest), { recursive: true }); - fs6.copyFileSync(sourcePath, dest); + fs7.mkdirSync(path8.dirname(dest), { recursive: true }); + fs7.copyFileSync(sourcePath, dest); return dest; } function removeZoneMirror({ workspaceRoot, categoryCode, filename }) { if (!UPLOAD_ZONE_CODES.includes(categoryCode)) return; const target = resolveZoneFilePath(workspaceRoot, categoryCode, filename); - if (fs6.existsSync(target)) fs6.unlinkSync(target); + if (fs7.existsSync(target)) fs7.unlinkSync(target); } function isPathInsideUserWorkspace(workspaceRoot, requestedPath) { - const base = path7.resolve(workspaceRoot); - const resolved = path7.resolve(requestedPath); - return resolved === base || resolved.startsWith(`${base}${path7.sep}`); + const base = path8.resolve(workspaceRoot); + const resolved = path8.resolve(requestedPath); + return resolved === base || resolved.startsWith(`${base}${path8.sep}`); } function renderUserSpaceHints({ username, workspaceRoot, displayName, slug }) { const addressName = resolveUserAddressName({ displayName, username, slug }); @@ -10752,8 +11458,9 @@ ${zoneLines.join("\n")} - \u7528\u6237\u8981\u7F51\u9875 / HTML / \u62A5\u544A / \u5206\u4EAB\u94FE\u63A5\u65F6\uFF1A**\u4F60\u5FC5\u987B\u4EB2\u81EA\u5B8C\u6210**\uFF0C\u4E0D\u8981\u63A8\u7ED9\u7528\u6237\u624B\u52A8\u64CD\u4F5C - \u5148 \`load_skill\` \u2192 \`static-page-publish\`\uFF0C\u518D\u4F7F\u7528 \`write_file\` \u521B\u5EFA \`public/\u9875\u9762\u540D.html\`\uFF08\u9700\u8981\u8C03\u6574\u5DF2\u6709\u9875\u9762\u65F6\u7528 \`edit_file\`\uFF09 -- \u5982\u679C\u9875\u9762\u91CC\u8FD8\u8981\u63D0\u4F9B Word / docx \u4E0B\u8F7D\uFF1A\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\`\uFF0C\u751F\u6210 \`public/\u6587\u4EF6\u540D.docx\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5199 HTML \u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863 -- \u9875\u9762\u9700\u8981 Word / docx \u4E0B\u8F7D\u65F6\uFF0C\u5FC5\u987B\u7528 sandbox-fs \u7684 \`generate_docx\` \u5199\u5165 \`public/*.docx\`\uFF0C\u4E0D\u8981\u7528 shell/computercontroller \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6 +- \u9ED8\u8BA4\u53EA\u751F\u6210 HTML\uFF1B\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6 +- \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF1A\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\`\uFF0C\u751F\u6210 \`public/\u6587\u4EF6\u540D.docx\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5199 HTML \u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863 +- \u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF0C\u5FC5\u987B\u7528 sandbox-fs \u7684 \`generate_docx\` \u5199\u5165 \`public/*.docx\`\uFF0C\u4E0D\u8981\u7528 shell/computercontroller \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6 - \u82E5\u7528 \`apps__create_app\` \u8BBE\u8BA1/\u9884\u89C8\u9875\u9762\uFF1A\u8FD9\u4E00\u6B65\u53EA\u662F\u5728 Apps \u7A97\u53E3\u5185\u521B\u5EFA\u4EA4\u4E92\u5F0F App\uFF0C**\u8FD8\u6CA1\u6709\u516C\u7F51\u94FE\u63A5**\uFF1B\u5FC5\u987B\u7D27\u63A5\u7740\u6309 \`static-page-publish\` skill \u628A\u6700\u7EC8\u5185\u5BB9 \`write_file\` \u843D\u5230 \`public/*.html\`\uFF0C\u624D\u80FD\u7ED9\u51FA\u771F\u5B9E\u53EF\u8BBF\u95EE\u7684\u94FE\u63A5 - \u5199\u5165 \`\` \u7684 **mindspace-cover** \u5143\u6570\u636E\uFF08\u8BE6\u89C1 \`.agents/skills/static-page-publish/SKILL.md\`\uFF09 - \u4FDD\u5B58\u540E\u7ACB\u5373\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 \`[\u6807\u9898](URL)\`\uFF1B\u5199\u5165 \`public/\u9875\u9762.html\` \u65F6 URL \u4E3A \`.../MindSpace/<\u7528\u6237ID>/public/\u9875\u9762.html\` @@ -10782,20 +11489,21 @@ function buildUserSpaceConstraints({ username, workspaceRoot, publicBaseUrl, slu "- **\u7981\u6B62**\uFF1A\u8BBF\u95EE\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001MindSpace \u6839\u76EE\u5F55\u3001data/mindspace \u5185\u90E8\u8DEF\u5F84\u3001\u4E3B\u673A\u7EDD\u5BF9\u8DEF\u5F84", "- **\u7981\u6B62**\u7528\u516C\u7F51 URL \u5217\u76EE\u5F55\u6216\u8BFB CSV\uFF1B\u751F\u6210 HTML \u5199\u5165 `public/` \u5E76\u7ED9\u51FA\u516C\u7F51\u94FE\u63A5", "- **\u751F\u6210\u9875\u9762**\uFF1A\u5148 `load_skill` \u2192 `static-page-publish`\uFF0C\u518D\u7528 `write_file` / `edit_file` \u5199\u5165 `public/*.html`\uFF1B**\u7981\u6B62**\u8BA9\u7528\u6237\u624B\u52A8\u4FDD\u5B58\u6587\u4EF6", - "- **Word \u4E0B\u8F7D\u9875**\uFF1A\u82E5\u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate` \u751F\u6210 `public/*.docx` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5728 HTML \u91CC\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863", + "- **\u9ED8\u8BA4\u53EA\u751F\u6210 HTML**\uFF1A\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210 Word\u3001PDF\u3001\u957F\u56FE\u7B49\u4F34\u751F\u6587\u4EF6", + "- **Word \u4E0B\u8F7D\u9875**\uFF1A\u82E5\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate` \u751F\u6210 `public/*.docx` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5728 HTML \u91CC\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863", "- \u7528 `apps__create_app` \u8BBE\u8BA1\u9875\u9762\u65F6\uFF0C\u6700\u540E\u4E00\u6B65\u4ECD\u8981\u6309 `static-page-publish` skill \u628A\u5185\u5BB9 `write_file` \u843D\u5230 `public/*.html`\uFF0C\u518D\u7ED9\u51FA\u4E0B\u65B9\u524D\u7F00\u62FC\u51FA\u7684\u771F\u5B9E\u94FE\u63A5\uFF0C\u4E0D\u8981\u505C\u5728 App \u9636\u6BB5\u5C31\u56DE\u590D\u94FE\u63A5", "- **\u9700\u8981\u67E5\u5B9E\u65F6/\u771F\u5B9E\u4E16\u754C\u4FE1\u606F\uFF08\u5982\u673A\u6784\u540D\u5355\u3001\u65B0\u95FB\u3001\u884C\u60C5\uFF09\u65F6**\uFF1A\u5148 `load_skill` \u2192 `web`\uFF0C\u4F18\u5148\u7528\u5176 `web_search`/`fetch_url`\uFF1B\u56FD\u5185\u73AF\u5883\u4E0B google.com \u4E0D\u53EF\u8FBE\uFF0C`web_search` \u591A\u6B21\u65E0\u679C\u65F6\u6539\u8D70 Bing/360 \u7B49\u53EF\u8FBE\u641C\u7D22\u6E90\uFF0C\u907F\u514D\u5BF9\u53CD\u722C\u7AD9\u70B9\u53CD\u590D\u786C\u6293", - "- **Word \u4E0B\u8F7D\u9875**\uFF1A\u82E5\u7528\u6237\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate` \u751F\u6210 `public/*.docx` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5728 HTML \u91CC\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\uFF1B\u4E0D\u8981\u7528 shell/computercontroller \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6", + "- **Word \u4E0B\u8F7D\u9875**\uFF1A\u82E5\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 `load_skill` \u2192 `docx-generate` \u751F\u6210 `public/*.docx` \u5E76\u786E\u8BA4\u6587\u4EF6\u5B58\u5728\uFF0C\u518D\u5728 HTML \u91CC\u7528\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\uFF1B\u4E0D\u8981\u7528 shell/computercontroller \u751F\u6210\u751F\u4EA7\u4E0B\u8F7D\u6587\u4EF6", publicBaseUrl && slug ? `- \u516C\u7F51 HTML \u524D\u7F00\uFF08\u516C\u5F00\u533A\uFF09\uFF1A\`${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${slug}/public/\`\uFF08\u5199\u5165 \`public/\u9875\u9762.html\` \u65F6\u5206\u4EAB\u94FE\u63A5\u5FC5\u987B\u542B \`public/\`\uFF09` : null ].filter(Boolean).join("\n"); } function ensureUserSpaceHints(workspaceRoot, context) { - const hintsPath = path7.join(workspaceRoot, WORKSPACE_HINTS_FILENAME2); - const legacyPath = path7.join(workspaceRoot, LEGACY_WORKSPACE_HINTS_FILENAME2); + const hintsPath = path8.join(workspaceRoot, WORKSPACE_HINTS_FILENAME2); + const legacyPath = path8.join(workspaceRoot, LEGACY_WORKSPACE_HINTS_FILENAME2); const content = renderUserSpaceHints({ ...context, workspaceRoot }); - fs6.writeFileSync(hintsPath, content, "utf8"); - if (fs6.existsSync(legacyPath)) { - fs6.unlinkSync(legacyPath); + fs7.writeFileSync(hintsPath, content, "utf8"); + if (fs7.existsSync(legacyPath)) { + fs7.unlinkSync(legacyPath); } return hintsPath; } @@ -10811,7 +11519,7 @@ async function syncUserZonesFromAssets(pool2, storageRoot, userId, workspaceRoot [userId, ...UPLOAD_ZONE_CODES] ); for (const row of rows) { - const sourcePath = path7.join(storageRoot, row.storage_key); + const sourcePath = path8.join(storageRoot, row.storage_key); mirrorAssetToZone2({ workspaceRoot, categoryCode: row.category_code, @@ -10853,10 +11561,10 @@ async function ensureUserSpaceLayout({ } // skills-registry.mjs -import fs7 from "node:fs"; -import path8 from "node:path"; +import fs8 from "node:fs"; +import path9 from "node:path"; import { fileURLToPath as fileURLToPath4 } from "node:url"; -var __dirname3 = path8.dirname(fileURLToPath4(import.meta.url)); +var __dirname3 = path9.dirname(fileURLToPath4(import.meta.url)); var DEFAULT_USER_SKILLS = { web: true, search: true, @@ -10865,6 +11573,7 @@ var DEFAULT_USER_SKILLS = { "table-viewer": true, "product-campaign-page": true, "docx-generate": true, + "long-image-download": true, [PUBLISH_SKILL_NAME]: false }; var USER_ROLE_SKILL_PRESETS = { @@ -10892,14 +11601,14 @@ function parseSkillFrontmatter(content) { return { name, description }; } function listPlatformSkillCatalog(h5Root = __dirname3) { - const skillsRoot = path8.join(h5Root, "skills"); - if (!fs7.existsSync(skillsRoot)) return []; + const skillsRoot = path9.join(h5Root, "skills"); + if (!fs8.existsSync(skillsRoot)) return []; const catalog = []; - for (const entry of fs7.readdirSync(skillsRoot, { withFileTypes: true })) { + for (const entry of fs8.readdirSync(skillsRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; - const skillPath = path8.join(skillsRoot, entry.name, "SKILL.md"); - if (!fs7.existsSync(skillPath)) continue; - const raw = fs7.readFileSync(skillPath, "utf8"); + const skillPath = path9.join(skillsRoot, entry.name, "SKILL.md"); + if (!fs8.existsSync(skillPath)) continue; + const raw = fs8.readFileSync(skillPath, "utf8"); const meta = parseSkillFrontmatter(raw); const name = meta.name || entry.name; catalog.push({ @@ -10953,14 +11662,14 @@ function applySkillGrantsToCapabilities(capabilities, skillMap) { return effective; } function copySkillTree(srcDir, destDir) { - fs7.mkdirSync(destDir, { recursive: true }); - for (const entry of fs7.readdirSync(srcDir, { withFileTypes: true })) { - const from = path8.join(srcDir, entry.name); - const to = path8.join(destDir, entry.name); + fs8.mkdirSync(destDir, { recursive: true }); + for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) { + const from = path9.join(srcDir, entry.name); + const to = path9.join(destDir, entry.name); if (entry.isDirectory()) { copySkillTree(from, to); } else { - fs7.copyFileSync(from, to); + fs8.copyFileSync(from, to); } } } @@ -10974,22 +11683,22 @@ function syncSkillsToWorkspace({ }) { const enabled = new Set(grantedSkillNames(skillMap)); const platformNames = new Set(catalog.map((item) => item.name)); - const agentsSkills = path8.join(publishDir, ".agents", "skills"); - if (fs7.existsSync(agentsSkills)) { - for (const entry of fs7.readdirSync(agentsSkills, { withFileTypes: true })) { + const agentsSkills = path9.join(publishDir, ".agents", "skills"); + if (fs8.existsSync(agentsSkills)) { + for (const entry of fs8.readdirSync(agentsSkills, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const skillName = entry.name; const catalogItem = catalog.find((item) => item.name === skillName || item.dirName === skillName); const resolvedName = catalogItem?.name ?? skillName; if (platformNames.has(resolvedName) && !enabled.has(resolvedName)) { - fs7.rmSync(path8.join(agentsSkills, entry.name), { recursive: true, force: true }); + fs8.rmSync(path9.join(agentsSkills, entry.name), { recursive: true, force: true }); } } } for (const item of catalog) { if (!enabled.has(item.name)) continue; - const srcDir = path8.join(h5Root, "skills", item.dirName); - const destDir = path8.join(agentsSkills, item.name); + const srcDir = path9.join(h5Root, "skills", item.dirName); + const destDir = path9.join(agentsSkills, item.name); if (item.name === PUBLISH_SKILL_NAME && user) { ensurePublishSkillInstalled(publishDir, { slug: String(user.id).trim().toLowerCase(), @@ -10999,8 +11708,8 @@ function syncSkillsToWorkspace({ }); continue; } - if (fs7.existsSync(srcDir)) { - if (fs7.existsSync(destDir)) fs7.rmSync(destDir, { recursive: true, force: true }); + if (fs8.existsSync(srcDir)) { + if (fs8.existsSync(destDir)) fs8.rmSync(destDir, { recursive: true, force: true }); copySkillTree(srcDir, destDir); } } @@ -11067,8 +11776,8 @@ function hashSessionToken(token) { return crypto6.createHash("sha256").update(token).digest("hex"); } function createUserAuth(pool2, options = {}) { - const usersRoot = path9.resolve(options.usersRoot ?? "/tmp/tkmind_go_users"); - const h5Root = path9.resolve(options.h5Root ?? path9.join(usersRoot, "..")); + const usersRoot = path10.resolve(options.usersRoot ?? "/tmp/tkmind_go_users"); + const h5Root = path10.resolve(options.h5Root ?? path10.join(usersRoot, "..")); const env = options.env ?? process.env; const storageRoot = resolveMindspaceStorageRoot(h5Root, env); const publicBaseUrl = resolvePublicBaseUrl(env); @@ -11116,7 +11825,7 @@ function createUserAuth(pool2, options = {}) { ); }; const ensureWorkspace = (workspaceRoot) => { - fs8.mkdirSync(workspaceRoot, { recursive: true }); + fs9.mkdirSync(workspaceRoot, { recursive: true }); }; const isAdminRole = (user) => user?.role === "admin"; const getUserById = async (userId) => { @@ -11164,7 +11873,7 @@ function createUserAuth(pool2, options = {}) { return { ...base, publishSlug: publishKey, - publishUrl: `${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(publishKey)}/`, + publishUrl: buildPublicUrl(publicBaseUrl, publishKey), publishSkillName: PUBLISH_SKILL_NAME }; }; @@ -11195,7 +11904,7 @@ function createUserAuth(pool2, options = {}) { ensurePublishSkillInstalled(web.publishDir, hintsContext); ensureWorkspaceHintsInstalled(web.publishDir, hintsContext); const legacyPublishDir = resolveLegacyPublishDir(h5Root, user); - if (legacyPublishDir && legacyPublishDir !== web.publishDir && fs8.existsSync(legacyPublishDir)) { + if (legacyPublishDir && legacyPublishDir !== web.publishDir && fs9.existsSync(legacyPublishDir)) { ensureWorkspaceHintsInstalled(legacyPublishDir, { ...hintsContext, publishDir: legacyPublishDir }); } ensureUserMemoryProfile(web.publishDir, { @@ -11244,8 +11953,8 @@ function createUserAuth(pool2, options = {}) { const syncUserPublishWorkspace = async (user) => { if (!user) return null; const layout = await publishLayoutFor(user); - const current = path9.resolve(user.workspace_root); - const target = path9.resolve(layout.publishDir); + const current = path10.resolve(user.workspace_root); + const target = path10.resolve(layout.publishDir); if (current !== target) { const now = Date.now(); await pool2.query(`UPDATE h5_users SET workspace_root = ?, updated_at = ? WHERE id = ?`, [ @@ -11849,7 +12558,7 @@ function createUserAuth(pool2, options = {}) { values.push(patch.status); } if (patch.workspaceRoot !== void 0) { - const root = path9.resolve(patch.workspaceRoot); + const root = path10.resolve(patch.workspaceRoot); fields.push("workspace_root = ?"); values.push(root); ensureWorkspace(root); @@ -13715,30 +14424,30 @@ function resolveCookieDomainForRequest(req) { // wiki-auth.mjs import crypto7 from "node:crypto"; -import fs9 from "node:fs"; -import path10 from "node:path"; +import fs10 from "node:fs"; +import path11 from "node:path"; var DB_DIR = ""; var USERS_FILE = ""; var PAGES_DIR = ""; function initPaths(dataDir) { DB_DIR = dataDir; - USERS_FILE = path10.join(DB_DIR, "users.json"); - PAGES_DIR = path10.join(DB_DIR, "pages"); + USERS_FILE = path11.join(DB_DIR, "users.json"); + PAGES_DIR = path11.join(DB_DIR, "pages"); } function ensureDb() { - fs9.mkdirSync(DB_DIR, { recursive: true }); - fs9.mkdirSync(PAGES_DIR, { recursive: true }); - if (!fs9.existsSync(USERS_FILE)) { - fs9.writeFileSync(USERS_FILE, JSON.stringify({ users: [] }, null, 2), "utf-8"); + fs10.mkdirSync(DB_DIR, { recursive: true }); + fs10.mkdirSync(PAGES_DIR, { recursive: true }); + if (!fs10.existsSync(USERS_FILE)) { + fs10.writeFileSync(USERS_FILE, JSON.stringify({ users: [] }, null, 2), "utf-8"); } } function readUsers() { ensureDb(); - return JSON.parse(fs9.readFileSync(USERS_FILE, "utf-8")); + return JSON.parse(fs10.readFileSync(USERS_FILE, "utf-8")); } function writeUsers(data) { ensureDb(); - fs9.writeFileSync(USERS_FILE, JSON.stringify(data, null, 2), "utf-8"); + fs10.writeFileSync(USERS_FILE, JSON.stringify(data, null, 2), "utf-8"); } function safeEqual3(a, b) { const ba = Buffer.from(a); @@ -13809,10 +14518,10 @@ function createWikiAuth(dataDir) { } function listPages(username) { ensureDb(); - const userPagesDir = path10.join(PAGES_DIR, username); - if (!fs9.existsSync(userPagesDir)) return []; - return fs9.readdirSync(userPagesDir).filter((f) => f.endsWith(".json")).map((f) => { - const data = JSON.parse(fs9.readFileSync(path10.join(userPagesDir, f), "utf-8")); + const userPagesDir = path11.join(PAGES_DIR, username); + if (!fs10.existsSync(userPagesDir)) return []; + return fs10.readdirSync(userPagesDir).filter((f) => f.endsWith(".json")).map((f) => { + const data = JSON.parse(fs10.readFileSync(path11.join(userPagesDir, f), "utf-8")); return { id: data.id, title: data.title, @@ -13825,16 +14534,16 @@ function createWikiAuth(dataDir) { } function getPage(username, slug) { ensureDb(); - const filePath = path10.join(PAGES_DIR, username, `${slug}.json`); - if (!fs9.existsSync(filePath)) return null; - return JSON.parse(fs9.readFileSync(filePath, "utf-8")); + const filePath = path11.join(PAGES_DIR, username, `${slug}.json`); + if (!fs10.existsSync(filePath)) return null; + return JSON.parse(fs10.readFileSync(filePath, "utf-8")); } function savePage(username, slug, title, content, tags) { ensureDb(); - const userPagesDir = path10.join(PAGES_DIR, username); - fs9.mkdirSync(userPagesDir, { recursive: true }); - const filePath = path10.join(userPagesDir, `${slug}.json`); - const existing = fs9.existsSync(filePath) ? JSON.parse(fs9.readFileSync(filePath, "utf-8")) : null; + const userPagesDir = path11.join(PAGES_DIR, username); + fs10.mkdirSync(userPagesDir, { recursive: true }); + const filePath = path11.join(userPagesDir, `${slug}.json`); + const existing = fs10.existsSync(filePath) ? JSON.parse(fs10.readFileSync(filePath, "utf-8")) : null; const page = { id: existing?.id || crypto7.randomUUID(), slug, @@ -13845,14 +14554,14 @@ function createWikiAuth(dataDir) { createdAt: existing?.createdAt || Date.now(), updatedAt: Date.now() }; - fs9.writeFileSync(filePath, JSON.stringify(page, null, 2), "utf-8"); + fs10.writeFileSync(filePath, JSON.stringify(page, null, 2), "utf-8"); return page; } function deletePage(username, slug) { ensureDb(); - const filePath = path10.join(PAGES_DIR, username, `${slug}.json`); - if (fs9.existsSync(filePath)) { - fs9.unlinkSync(filePath); + const filePath = path11.join(PAGES_DIR, username, `${slug}.json`); + if (fs10.existsSync(filePath)) { + fs10.unlinkSync(filePath); return true; } return false; @@ -13893,10 +14602,10 @@ var PUBLIC_SCHEME = (process.env.LOCAL_TEST_SCHEME ?? (String(process.env.LOCAL_ var PUBLIC_PORT = Number( process.env.LOCAL_TEST_PUBLIC_PORT ?? (USES_LOCALHOST ? 8443 : PUBLIC_SCHEME === "https" ? 443 : 80) ); -function publicUrl(host, { path: path31 = "" } = {}) { +function publicUrl(host, { path: path35 = "" } = {}) { const defaultPort = PUBLIC_SCHEME === "https" ? 443 : 80; const portSuffix = PUBLIC_PORT === defaultPort ? "" : `:${PUBLIC_PORT}`; - const normalizedPath = path31.startsWith("/") ? path31 : path31 ? `/${path31}` : ""; + const normalizedPath = path35.startsWith("/") ? path35 : path35 ? `/${path35}` : ""; return `${PUBLIC_SCHEME}://${host}${portSuffix}${normalizedPath}`; } var H5_PUBLIC_BASE = (process.env.H5_PUBLIC_BASE_URL ?? publicUrl(H5_HOST)).replace(/\/$/, ""); @@ -13908,13 +14617,13 @@ function isLocalDevHostname(hostname) { } // mindspace-workspace-thumbnails.mjs -import fs11 from "node:fs"; +import fs12 from "node:fs"; import fsPromises from "node:fs/promises"; -import path12 from "node:path"; +import path13 from "node:path"; // mindspace-thumbnails.mjs -import fs10 from "node:fs/promises"; -import path11 from "node:path"; +import fs11 from "node:fs/promises"; +import path12 from "node:path"; var FEED_WIDTH = 540; var FEED_HEIGHT = 720; var MAX_COVER_BYTES = 1.5 * 1024 * 1024; @@ -13985,7 +14694,7 @@ function bufferToImageDataUri(buffer) { return bytesToDataUri(buffer, mimeFromImageBytes(buffer)); } async function readLocalImageDataUri(filePath) { - const buffer = await fs10.readFile(filePath); + const buffer = await fs11.readFile(filePath); if (buffer.length === 0 || buffer.length > MAX_COVER_BYTES) return null; return bytesToDataUri(buffer, mimeFromImageBytes(buffer)); } @@ -14007,15 +14716,15 @@ function resolveLocalImagePath({ storageRoot, contentStorageKey, contentBaseDir, if (!imageUrl || /^data:|^https?:/i.test(imageUrl)) return null; let baseDir = null; if (contentStorageKey) { - baseDir = path11.dirname(path11.resolve(storageRoot, contentStorageKey)); - const root = path11.resolve(storageRoot); - if (baseDir !== root && !baseDir.startsWith(`${root}${path11.sep}`)) return null; + baseDir = path12.dirname(path12.resolve(storageRoot, contentStorageKey)); + const root = path12.resolve(storageRoot); + if (baseDir !== root && !baseDir.startsWith(`${root}${path12.sep}`)) return null; } else if (contentBaseDir) { - baseDir = path11.resolve(contentBaseDir); + baseDir = path12.resolve(contentBaseDir); } if (!baseDir) return null; - const target = path11.resolve(baseDir, imageUrl.replace(/^\.\//, "")); - if (target !== baseDir && !target.startsWith(`${baseDir}${path11.sep}`)) return null; + const target = path12.resolve(baseDir, imageUrl.replace(/^\.\//, "")); + if (target !== baseDir && !target.startsWith(`${baseDir}${path12.sep}`)) return null; return target; } async function resolveCoverDataUri({ @@ -14275,19 +14984,19 @@ function buildFeedThumbnailSvg(signals, options = {}) { `; } function assetThumbnailKey(userId, assetId) { - return path11.posix.join("users", userId, "assets", assetId, "thumbnail.svg"); + return path12.posix.join("users", userId, "assets", assetId, "thumbnail.svg"); } function pageThumbnailKey(userId, pageId) { - return path11.posix.join("users", userId, "pages", pageId, "thumbnail.svg"); + return path12.posix.join("users", userId, "pages", pageId, "thumbnail.svg"); } async function writeThumbnail(storageRoot, storageKey, svg) { - const target = path11.resolve(storageRoot, storageKey); - const root = path11.resolve(storageRoot); - if (target !== root && !target.startsWith(`${root}${path11.sep}`)) { + const target = path12.resolve(storageRoot, storageKey); + const root = path12.resolve(storageRoot); + if (target !== root && !target.startsWith(`${root}${path12.sep}`)) { throw new Error("\u7F29\u7565\u56FE\u8DEF\u5F84\u8D8A\u754C"); } - await fs10.mkdir(path11.dirname(target), { recursive: true }); - await fs10.writeFile(target, svg, "utf8"); + await fs11.mkdir(path12.dirname(target), { recursive: true }); + await fs11.writeFile(target, svg, "utf8"); return target; } function isModernFeedThumbnail(svg) { @@ -14336,7 +15045,7 @@ async function ensurePageThumbnail({ } async function readThumbnailIfExists(storageRoot, storageKey) { try { - return await fs10.readFile(path11.resolve(storageRoot, storageKey), "utf8"); + return await fs11.readFile(path12.resolve(storageRoot, storageKey), "utf8"); } catch { return null; } @@ -14351,26 +15060,26 @@ function scheduleHtmlThumbnail(storageRoot, storageKey, html, meta = {}) { // mindspace-workspace-thumbnails.mjs function workspaceThumbnailRelativePath(htmlRelativePath) { const normalized = String(htmlRelativePath ?? "").replace(/^\/+/, ""); - const dir = path12.posix.dirname(normalized); - const base = path12.basename(normalized, path12.extname(normalized)); + const dir = path13.posix.dirname(normalized); + const base = path13.basename(normalized, path13.extname(normalized)); const thumb = `${base}.thumbnail.svg`; - return dir === "." ? thumb : path12.posix.join(dir, thumb); + return dir === "." ? thumb : path13.posix.join(dir, thumb); } async function ensureWorkspaceHtmlThumbnail(publishDir, htmlRelativePath, html, meta = {}) { - const htmlPath = path12.join(publishDir, htmlRelativePath); + const htmlPath = path13.join(publishDir, htmlRelativePath); const content = html ?? await fsPromises.readFile(htmlPath, "utf8"); const thumbRel = workspaceThumbnailRelativePath(htmlRelativePath); return ensureHtmlThumbnail(publishDir, thumbRel, content, { ...meta, - contentBaseDir: path12.dirname(htmlPath) + contentBaseDir: path13.dirname(htmlPath) }); } async function scanPublishTree(publishRoot) { - if (!fs11.existsSync(publishRoot)) return; + if (!fs12.existsSync(publishRoot)) return; const entries = await fsPromises.readdir(publishRoot, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory() || entry.name === "wiki" || entry.name.startsWith(".")) continue; - const userDir = path12.join(publishRoot, entry.name); + const userDir = path13.join(publishRoot, entry.name); await scanUserHtmlFiles(userDir); } } @@ -14378,14 +15087,14 @@ async function scanUserHtmlFiles(userDir) { const walk = async (dir) => { const entries = await fsPromises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { - const full = path12.join(dir, entry.name); + const full = path13.join(dir, entry.name); if (entry.isDirectory()) { if (entry.name === ".agents" || entry.name === "node_modules") continue; await walk(full); continue; } if (!entry.name.endsWith(".html") || entry.name.endsWith(".thumbnail.svg")) continue; - const rel = path12.relative(userDir, full); + const rel = path13.relative(userDir, full); await ensureWorkspaceHtmlThumbnail(userDir, rel).catch(() => { }); } @@ -14393,13 +15102,13 @@ async function scanUserHtmlFiles(userDir) { await walk(userDir); } function startWorkspaceThumbnailWatcher(publishRoot) { - if (!fs11.existsSync(publishRoot)) { - fs11.mkdirSync(publishRoot, { recursive: true }); + if (!fs12.existsSync(publishRoot)) { + fs12.mkdirSync(publishRoot, { recursive: true }); } void scanPublishTree(publishRoot); const pending = /* @__PURE__ */ new Map(); const schedule = (userDir, relativePath) => { - const key = path12.join(userDir, relativePath); + const key = path13.join(userDir, relativePath); const existing = pending.get(key); if (existing) clearTimeout(existing); pending.set( @@ -14412,10 +15121,10 @@ function startWorkspaceThumbnailWatcher(publishRoot) { ); }; const attachUserWatcher = (userDir) => { - if (!fs11.existsSync(userDir)) return; + if (!fs12.existsSync(userDir)) return; void scanUserHtmlFiles(userDir); try { - fs11.watch(userDir, { recursive: true }, (_event, filename) => { + fs12.watch(userDir, { recursive: true }, (_event, filename) => { if (!filename || !String(filename).endsWith(".html")) return; if (String(filename).endsWith(".thumbnail.svg")) return; schedule(userDir, filename); @@ -14423,16 +15132,16 @@ function startWorkspaceThumbnailWatcher(publishRoot) { } catch { } }; - for (const entry of fs11.readdirSync(publishRoot, { withFileTypes: true })) { + for (const entry of fs12.readdirSync(publishRoot, { withFileTypes: true })) { if (entry.isDirectory() && entry.name !== "wiki" && !entry.name.startsWith(".")) { - attachUserWatcher(path12.join(publishRoot, entry.name)); + attachUserWatcher(path13.join(publishRoot, entry.name)); } } try { - fs11.watch(publishRoot, (_event, filename) => { + fs12.watch(publishRoot, (_event, filename) => { if (!filename) return; - const userDir = path12.join(publishRoot, filename); - if (fs11.existsSync(userDir) && fs11.statSync(userDir).isDirectory()) { + const userDir = path13.join(publishRoot, filename); + if (fs12.existsSync(userDir) && fs12.statSync(userDir).isDirectory()) { attachUserWatcher(userDir); } }); @@ -14442,14 +15151,14 @@ function startWorkspaceThumbnailWatcher(publishRoot) { // mindspace-workspace-sync.mjs import crypto9 from "node:crypto"; -import fs13 from "node:fs"; +import fs14 from "node:fs"; import fsPromises2 from "node:fs/promises"; -import path17 from "node:path"; +import path18 from "node:path"; // mindspace-assets.mjs import crypto8 from "node:crypto"; -import fs12 from "node:fs/promises"; -import path16 from "node:path"; +import fs13 from "node:fs/promises"; +import path17 from "node:path"; // mindspace-scan.mjs var SCRIPT_PATTERNS = [ @@ -14670,7 +15379,7 @@ function extractAttachmentText(buffer, mimeType, filename = "") { } // user-image-url.mjs -import path13 from "node:path"; +import path14 from "node:path"; var PUBLIC_IMAGES_DIR = "images"; var cachedSigner = null; var signerInitAttempted = false; @@ -14698,7 +15407,7 @@ function formatImageDateFolder(date = /* @__PURE__ */ new Date()) { return `${year}-${month}-${day}`; } function extensionForMime(mimeType, fallbackFilename = "") { - const ext = path13.extname(String(fallbackFilename)).toLowerCase(); + const ext = path14.extname(String(fallbackFilename)).toLowerCase(); if (ext && /^\.[a-z0-9]{1,8}$/.test(ext)) return ext === ".jpeg" ? ".jpg" : ext; if (mimeType === "image/jpeg") return ".jpg"; if (mimeType === "image/png") return ".png"; @@ -14707,7 +15416,7 @@ function extensionForMime(mimeType, fallbackFilename = "") { return ".img"; } function sanitizeImageBasename(filename, assetId) { - const raw = path13.basename(String(filename ?? ""), path13.extname(String(filename ?? ""))).trim(); + const raw = path14.basename(String(filename ?? ""), path14.extname(String(filename ?? ""))).trim(); const normalized = raw.normalize("NFKC").replace(/[^\w\u4e00-\u9fff.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80); return normalized || assetId.slice(0, 8); } @@ -14724,8 +15433,8 @@ function buildPublicImagePaths({ const basename = sanitizeImageBasename(originalFilename, assetId); const suffix = uniqueSuffix ? `-${uniqueSuffix}` : ""; const filename = `${basename}${suffix}${extension}`; - const storageKey = path13.posix.join("users", userId, PUBLIC_IMAGES_DIR, dateFolder, filename); - const workspaceRelativePath = path13.posix.join("public", PUBLIC_IMAGES_DIR, dateFolder, filename); + const storageKey = path14.posix.join("users", userId, PUBLIC_IMAGES_DIR, dateFolder, filename); + const workspaceRelativePath = path14.posix.join("public", PUBLIC_IMAGES_DIR, dateFolder, filename); return { storageKey, workspaceRelativePath, filename, dateFolder }; } function buildImgproxyDisplayUrl(storageKey, options = {}) { @@ -15039,7 +15748,7 @@ function renderAssetPreviewHtml({ asset, buffer, downloadUrl }) { } // workspace-storage.mjs -import path14 from "node:path"; +import path15 from "node:path"; var WORKSPACE_STORAGE_PREFIX = "workspace://"; function buildWorkspaceStorageKey(userId, categoryCode, relativeFilename) { const category = String(categoryCode ?? "").trim(); @@ -15068,16 +15777,16 @@ function resolveWorkspaceStoragePath(h5Root, storageKey) { const relativeFilename = remainder.slice(slash2 + 1); if (!relativeFilename) return null; const workspaceRoot = resolveUserWorkspaceRoot(h5Root, { id: userId }); - const resolved = path14.resolve(resolveZoneFilePath(workspaceRoot, categoryCode, relativeFilename)); - const zoneRoot = path14.resolve(resolveZoneFilePath(workspaceRoot, categoryCode, "")); - if (resolved !== zoneRoot && !resolved.startsWith(`${zoneRoot}${path14.sep}`)) { + const resolved = path15.resolve(resolveZoneFilePath(workspaceRoot, categoryCode, relativeFilename)); + const zoneRoot = path15.resolve(resolveZoneFilePath(workspaceRoot, categoryCode, "")); + if (resolved !== zoneRoot && !resolved.startsWith(`${zoneRoot}${path15.sep}`)) { return null; } return resolved; } // user-image-normalize.mjs -import path15 from "node:path"; +import path16 from "node:path"; import sharp from "sharp"; var DEFAULT_IMAGE_UPLOAD_MAX_BYTES = 4 * 1024 * 1024; var DEFAULT_IMAGE_MASTER_MAX_BYTES = 6 * 1024 * 1024; @@ -15141,7 +15850,7 @@ function buildOutputProfile(mimeType) { }; } function deriveOutputFilename(filename, extension) { - const raw = path15.basename(String(filename ?? ""), path15.extname(String(filename ?? ""))).trim() || "image"; + const raw = path16.basename(String(filename ?? ""), path16.extname(String(filename ?? ""))).trim() || "image"; return `${raw}${extension}`; } async function renderVariant(buffer, metadata, profile, { width, height, quality, maxPixels }) { @@ -15263,7 +15972,7 @@ function normalizeFilename(filename) { return normalized.slice(0, 255); } function expectedMimeType(filename) { - return ALLOWED_EXTENSIONS.get(path16.extname(filename).toLowerCase()) ?? null; + return ALLOWED_EXTENSIONS.get(path17.extname(filename).toLowerCase()) ?? null; } function extractImageDimensions(buffer, mimeType) { if (mimeType === "image/png" && buffer.length >= 24) { @@ -15326,7 +16035,7 @@ function detectMimeType(buffer, filename) { const extensionMime = expectedMimeType(filename); if (buffer.subarray(0, 2).toString("ascii") === "PK") return extensionMime; if (extensionMime?.startsWith("text/")) return extensionMime; - if ([".doc", ".xls", ".ppt"].includes(path16.extname(filename).toLowerCase())) { + if ([".doc", ".xls", ".ppt"].includes(path17.extname(filename).toLowerCase())) { return extensionMime; } return null; @@ -15378,13 +16087,17 @@ function publicUrlForAsset(row) { return null; } function publicTempImageFilename(assetId, mimeType, fallbackFilename = "") { - const fallbackExtension = path16.extname(String(fallbackFilename)).toLowerCase(); + const fallbackExtension = path17.extname(String(fallbackFilename)).toLowerCase(); const extension = PUBLIC_IMAGE_EXTENSIONS.get(mimeType) || fallbackExtension || ".img"; return `${assetId}${extension}`; } function publicTempImageUrl(userId, assetId, mimeType, fallbackFilename = "") { const filename = publicTempImageFilename(assetId, mimeType, fallbackFilename); - return `${resolvePublicBaseUrl()}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(userId)}/${PUBLIC_ZONE_DIR}/${PUBLIC_TEMP_IMAGE_DIR}/${encodeURIComponent(filename)}`; + return createMindSpacePublicUrl({ + publicBaseUrl: resolvePublicBaseUrl(), + ownerKey: userId, + relativePath: `${PUBLIC_ZONE_DIR}/${PUBLIC_TEMP_IMAGE_DIR}/${filename}` + }); } function assetResponse(row) { return { @@ -15443,18 +16156,20 @@ function validateUploadRequest({ filename, sizeBytes, maxFileBytes = DEFAULT_MAX return { filename: normalizedFilename, sizeBytes: normalizedSize, expectedMimeType: mimeType }; } function createAssetService(pool2, options = {}) { - const storageRoot = path16.resolve(options.storageRoot ?? path16.join(process.cwd(), "data", "mindspace")); - const h5Root = options.h5Root ? path16.resolve(options.h5Root) : null; + const storageRoot = path17.resolve(options.storageRoot ?? path17.join(process.cwd(), "data", "mindspace")); + const h5Root = options.h5Root ? path17.resolve(options.h5Root) : null; const maxFileBytes = Number(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES); const uploadTtlMs = Number(options.uploadTtlMs ?? 30 * 60 * 1e3); const idFactory = options.idFactory ?? (() => crypto8.randomUUID()); const normalizeStoredImage = options.normalizeStoredImage ?? normalizeImageForStorage; + const conversationPackageRegistry = options.conversationPackageRegistry ?? null; const workspaceSync = createWorkspaceAssetSync({ pool: pool2, storageRoot, h5Root, maxFileBytes, - idFactory + idFactory, + conversationPackageRegistry }); const mirrorToUserWorkspace = async (userId, { categoryCode, filename, sourcePath }) => { if (!h5Root) return null; @@ -15468,16 +16183,16 @@ function createAssetService(pool2, options = {}) { const writePublicImageMirror = async (userId, workspaceRelativePath, sourcePath) => { if (!h5Root) return null; const workspaceRoot = resolveUserWorkspaceRoot(h5Root, { id: userId }); - const target = path16.join(workspaceRoot, workspaceRelativePath); - await fs12.mkdir(path16.dirname(target), { recursive: true }); - await fs12.copyFile(sourcePath, target); + const target = path17.join(workspaceRoot, workspaceRelativePath); + await fs13.mkdir(path17.dirname(target), { recursive: true }); + await fs13.copyFile(sourcePath, target); return target; }; const removePublicImageMirror = async (userId, workspaceRelativePath) => { if (!h5Root || !workspaceRelativePath) return; const workspaceRoot = resolveUserWorkspaceRoot(h5Root, { id: userId }); - const target = path16.join(workspaceRoot, workspaceRelativePath); - await fs12.rm(target, { force: true }); + const target = path17.join(workspaceRoot, workspaceRelativePath); + await fs13.rm(target, { force: true }); }; const resolvePublicCategory = async (conn, userId) => { const [rows] = await conn.query( @@ -15490,8 +16205,8 @@ function createAssetService(pool2, options = {}) { return rows[0] ?? null; }; const absoluteStoragePath2 = (storageKey) => { - const resolved = path16.resolve(storageRoot, storageKey); - if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path16.sep}`)) { + const resolved = path17.resolve(storageRoot, storageKey); + if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path17.sep}`)) { throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C"); } return resolved; @@ -15513,7 +16228,7 @@ function createAssetService(pool2, options = {}) { const workspacePath = resolveWorkspaceStoragePath(h5Root, storageKey); if (workspacePath) { try { - await fs12.stat(workspacePath); + await fs13.stat(workspacePath); return workspacePath; } catch (error) { if (error?.code !== "ENOENT") throw error; @@ -15524,7 +16239,7 @@ function createAssetService(pool2, options = {}) { for (const candidate of resolveStoragePathCandidates(storageKey)) { const absolutePath = absoluteStoragePath2(candidate); try { - await fs12.stat(absolutePath); + await fs13.stat(absolutePath); return absolutePath; } catch (error) { if (error?.code === "ENOENT") { @@ -15596,7 +16311,7 @@ function createAssetService(pool2, options = {}) { } const uploadId = idFactory(); const now = Date.now(); - const temporaryStorageKey = path16.posix.join("users", userId, "temp", `${uploadId}.upload`); + const temporaryStorageKey = path17.posix.join("users", userId, "temp", `${uploadId}.upload`); await conn.query( `UPDATE h5_user_spaces SET reserved_bytes = reserved_bytes + ?, updated_at = ? @@ -15606,8 +16321,9 @@ function createAssetService(pool2, options = {}) { await conn.query( `INSERT INTO h5_upload_sessions (id, user_id, space_id, category_id, filename, expected_size, declared_mime_type, - reserved_bytes, temporary_storage_key, status, expires_at, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'reserved', ?, ?)`, + reserved_bytes, temporary_storage_key, status, expires_at, created_at, + source_session_id, source_message_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'reserved', ?, ?, ?, ?)`, [ uploadId, userId, @@ -15619,7 +16335,9 @@ function createAssetService(pool2, options = {}) { validated.sizeBytes, temporaryStorageKey, now + uploadTtlMs, - now + now, + input.sourceSessionId ?? input.source_session_id ?? null, + input.sourceMessageId ?? input.source_message_id ?? null ] ); await conn.commit(); @@ -15680,10 +16398,10 @@ function createAssetService(pool2, options = {}) { validateImagePixels(buffer, detectedMimeType); } const target = absoluteStoragePath2(upload.temporary_storage_key); - await fs12.mkdir(path16.dirname(target), { recursive: true }); - await fs12.writeFile(target, buffer, { flag: "wx" }).catch(async (error) => { + await fs13.mkdir(path17.dirname(target), { recursive: true }); + await fs13.writeFile(target, buffer, { flag: "wx" }).catch(async (error) => { if (error?.code !== "EEXIST") throw error; - await fs12.writeFile(target, buffer); + await fs13.writeFile(target, buffer); }); const checksum = crypto8.createHash("sha256").update(buffer).digest("hex"); await pool2.query( @@ -15732,7 +16450,7 @@ function createAssetService(pool2, options = {}) { const assetId = idFactory(); const versionId = idFactory(); temporaryPath = absoluteStoragePath2(upload.temporary_storage_key); - const sourceBuffer = await fs12.readFile(temporaryPath); + const sourceBuffer = await fs13.readFile(temporaryPath); const normalizedImage = await normalizeStoredAssetImage({ buffer: sourceBuffer, mimeType: upload.detected_mime_type, @@ -15767,10 +16485,10 @@ function createAssetService(pool2, options = {}) { } finalPath = absoluteStoragePath2(finalStorageKey); if (finalStorageKey !== upload.temporary_storage_key) { - await fs12.mkdir(path16.dirname(finalPath), { recursive: true }); - await fs12.writeFile(finalPath, storedBuffer); + await fs13.mkdir(path17.dirname(finalPath), { recursive: true }); + await fs13.writeFile(finalPath, storedBuffer); } else if (isImage) { - await fs12.writeFile(finalPath, storedBuffer); + await fs13.writeFile(finalPath, storedBuffer); } if (shouldPublishImage && workspaceRelativePath) { publicMirror = await writePublicImageMirror(userId, workspaceRelativePath, finalPath); @@ -15860,21 +16578,125 @@ function createAssetService(pool2, options = {}) { }) : null }; if (finalStorageKey !== upload.temporary_storage_key) { - await fs12.rm(temporaryPath, { force: true }).catch(() => { + await fs13.rm(temporaryPath, { force: true }).catch(() => { }); } + await registerUploadArtifactForConversation({ + registry: conversationPackageRegistry, + userId, + upload, + asset: result, + assetId, + storageKey: finalStorageKey, + canonicalUrl: result.publicUrl, + now + }); return result; } catch (error) { await conn.rollback(); - if (finalPath && finalPath !== temporaryPath) await fs12.rm(finalPath, { force: true }).catch(() => { + if (finalPath && finalPath !== temporaryPath) await fs13.rm(finalPath, { force: true }).catch(() => { }); - if (publicMirror) await fs12.rm(publicMirror, { force: true }).catch(() => { + if (publicMirror) await fs13.rm(publicMirror, { force: true }).catch(() => { }); throw error; } finally { conn.release(); } }; + const claimUploadArtifactsForConversation = async (userId, { sessionId, messageId: messageId2 } = {}) => { + const normalizedSessionId = String(sessionId ?? "").trim(); + const normalizedMessageId = String(messageId2 ?? "").trim(); + if (!normalizedSessionId || !normalizedMessageId) return { claimedCount: 0 }; + const conn = await pool2.getConnection(); + try { + await conn.beginTransaction(); + const [rows] = await conn.query( + `SELECT + u.*, + a.id AS asset_id, + a.mime_type AS asset_mime_type, + a.original_filename AS asset_original_filename, + a.display_name AS asset_display_name, + a.size_bytes AS asset_size_bytes, + a.updated_at AS asset_updated_at, + v.storage_key AS asset_storage_key, + c.category_code + FROM h5_upload_sessions u + JOIN h5_assets a + ON a.id = u.completed_asset_id AND a.user_id = u.user_id + LEFT JOIN h5_asset_versions v + ON v.id = a.current_version_id AND v.asset_id = a.id + LEFT JOIN h5_space_categories c + ON c.id = a.category_id + WHERE u.user_id = ? + AND u.source_message_id = ? + AND u.status = 'completed' + AND (u.source_session_id IS NULL OR u.source_session_id = '') + LIMIT 20 + FOR UPDATE`, + [userId, normalizedMessageId] + ); + if (rows.length === 0) { + await conn.commit(); + return { claimedCount: 0 }; + } + await conn.query( + `UPDATE h5_upload_sessions + SET source_session_id = ? + WHERE user_id = ? + AND source_message_id = ? + AND status = 'completed' + AND (source_session_id IS NULL OR source_session_id = '')`, + [normalizedSessionId, userId, normalizedMessageId] + ); + await conn.commit(); + const now = Date.now(); + let claimedCount = 0; + for (const row of rows) { + const upload = { + ...row, + source_session_id: normalizedSessionId, + source_message_id: normalizedMessageId, + detected_mime_type: row.asset_mime_type, + actual_size: row.asset_size_bytes + }; + const asset = { + id: row.asset_id, + user_id: userId, + categoryCode: row.category_code, + mimeType: row.asset_mime_type, + filename: row.asset_original_filename, + displayName: row.asset_display_name, + sizeBytes: asNumber2(row.asset_size_bytes), + updatedAt: asNumber2(row.asset_updated_at) + }; + const artifact = await registerUploadArtifactForConversation({ + registry: conversationPackageRegistry, + userId, + upload, + asset, + assetId: row.asset_id, + storageKey: row.asset_storage_key, + canonicalUrl: publicUrlForAsset({ + id: row.asset_id, + user_id: userId, + category_code: row.category_code, + mime_type: row.asset_mime_type, + storage_key: row.asset_storage_key, + original_filename: row.asset_original_filename + }), + now + }); + if (artifact) claimedCount += 1; + } + return { claimedCount }; + } catch (error) { + await conn.rollback(); + throw error; + } finally { + conn.release(); + } + }; const cancelUpload = async (userId, uploadId) => { const conn = await pool2.getConnection(); let storageKey; @@ -15907,7 +16729,7 @@ function createAssetService(pool2, options = {}) { [uploadId, userId] ); await conn.commit(); - if (storageKey) await fs12.rm(absoluteStoragePath2(storageKey), { force: true }); + if (storageKey) await fs13.rm(absoluteStoragePath2(storageKey), { force: true }); return { cancelled: true }; } catch (error) { await conn.rollback(); @@ -16051,7 +16873,7 @@ function createAssetService(pool2, options = {}) { } if (storageCleanup) { try { - await fs12.rm(absoluteStoragePath2(storageCleanup), { force: true }); + await fs13.rm(absoluteStoragePath2(storageCleanup), { force: true }); } catch { } } @@ -16174,7 +16996,7 @@ function createAssetService(pool2, options = {}) { const assetStatus = scan.scanStatus === "passed" ? "ready" : "quarantined"; const versionScanStatus = scan.scanStatus === "passed" ? "passed" : "blocked"; const shouldPublishImage = isImage && scan.scanStatus === "passed"; - let finalStorageKey = path16.posix.join("users", userId, "assets", assetId, "versions", versionId); + let finalStorageKey = path17.posix.join("users", userId, "assets", assetId, "versions", versionId); let workspaceRelativePath = null; let storedFilename = storedFilenameInput; if (shouldPublishImage) { @@ -16189,8 +17011,8 @@ function createAssetService(pool2, options = {}) { storedFilename = imagePaths.workspaceRelativePath; } finalPath = absoluteStoragePath2(finalStorageKey); - await fs12.mkdir(path16.dirname(finalPath), { recursive: true }); - await fs12.writeFile(finalPath, storedBuffer, { flag: "wx" }); + await fs13.mkdir(path17.dirname(finalPath), { recursive: true }); + await fs13.writeFile(finalPath, storedBuffer, { flag: "wx" }); if (shouldPublishImage && workspaceRelativePath) { publicMirror = await writePublicImageMirror(userId, workspaceRelativePath, finalPath); } @@ -16210,7 +17032,7 @@ function createAssetService(pool2, options = {}) { assetTypeForMime(storedMimeType), storedMimeType, storedFilename, - displayName || path16.basename(storedFilename), + displayName || path17.basename(storedFilename), versionId, storedSizeBytes, storedChecksum, @@ -16260,7 +17082,7 @@ function createAssetService(pool2, options = {}) { asset_type: assetTypeForMime(storedMimeType), mime_type: storedMimeType, original_filename: storedFilename, - display_name: displayName || path16.basename(storedFilename), + display_name: displayName || path17.basename(storedFilename), storage_key: finalStorageKey, size_bytes: storedSizeBytes, checksum: storedChecksum, @@ -16275,9 +17097,9 @@ function createAssetService(pool2, options = {}) { }); } catch (error) { await conn.rollback(); - if (finalPath) await fs12.rm(finalPath, { force: true }).catch(() => { + if (finalPath) await fs13.rm(finalPath, { force: true }).catch(() => { }); - if (publicMirror) await fs12.rm(publicMirror, { force: true }).catch(() => { + if (publicMirror) await fs13.rm(publicMirror, { force: true }).catch(() => { }); throw error; } finally { @@ -16295,7 +17117,7 @@ function createAssetService(pool2, options = {}) { LIMIT 1`, [assetId] ); - const html = await fs12.readFile(assetPath, "utf8"); + const html = await fs13.readFile(assetPath, "utf8"); return ensureHtmlThumbnail(storageRoot, assetThumbnailKey(userId, assetId), html, { title: asset.displayName, subtitle: asset.categoryCode?.toUpperCase?.() ?? "HTML", @@ -16307,13 +17129,13 @@ function createAssetService(pool2, options = {}) { if (!canPreviewAsset(asset.mimeType)) { throw Object.assign(new Error("\u8BE5\u8D44\u4EA7\u4E0D\u652F\u6301\u9884\u89C8"), { code: "preview_not_supported" }); } - const buffer = await fs12.readFile(assetPath); + const buffer = await fs13.readFile(assetPath); const downloadUrl = downloadPath ?? `/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download?inline=1`; return renderAssetPreviewHtml({ asset, buffer, downloadUrl }); }; const extractAssetText = async (userId, assetId) => { const { asset, path: assetPath } = await readAsset(userId, assetId); - const buffer = await fs12.readFile(assetPath); + const buffer = await fs13.readFile(assetPath); const extracted = extractAttachmentText(buffer, asset.mimeType, asset.filename); return { asset: assetResponse(asset), @@ -16359,7 +17181,7 @@ function createAssetService(pool2, options = {}) { ); await conn.commit(); if (row.temporary_storage_key) { - await fs12.rm(absoluteStoragePath2(row.temporary_storage_key), { force: true }); + await fs13.rm(absoluteStoragePath2(row.temporary_storage_key), { force: true }); } expired += 1; } catch { @@ -16375,6 +17197,7 @@ function createAssetService(pool2, options = {}) { createUpload, writeUploadContent, completeUpload, + claimUploadArtifactsForConversation, cancelUpload, createChatAsset, listAssets, @@ -16388,11 +17211,56 @@ function createAssetService(pool2, options = {}) { expireStaleUploads }; } +async function registerUploadArtifactForConversation({ + registry, + userId, + upload, + asset, + assetId, + storageKey, + canonicalUrl, + now +}) { + const sessionId = String(upload?.source_session_id ?? "").trim(); + if (!registry || !sessionId || !assetId) return null; + try { + const packageRecord = await registry.ensurePackage({ + userId, + sessionId, + now + }); + const isImage = String(asset?.mimeType ?? "").startsWith("image/"); + const artifact = await registry.recordArtifact({ + id: `ca_${assetId}`, + packageId: packageRecord.id, + artifactKind: isImage ? "input_image" : "input_file", + role: "user", + assetId, + messageId: upload.source_message_id ?? null, + displayName: asset?.displayName ?? asset?.filename ?? upload.filename, + mimeType: asset?.mimeType ?? upload.detected_mime_type ?? null, + sizeBytes: asset?.sizeBytes ?? upload.actual_size ?? null, + storageKey, + canonicalUrl, + sortOrder: Number.isFinite(Number(now)) ? Number(now) : 0, + now + }); + await registry.writeManifestForSession({ userId, sessionId }); + return artifact; + } catch (error) { + console.warn( + "[MindSpace] conversation package upload artifact registration failed:", + error instanceof Error ? error.message : error + ); + return null; + } +} var assetInternals = { detectMimeType, normalizeFilename, expectedMimeType, - assetTypeForMime + assetTypeForMime, + registerUploadArtifactForConversation }; // mindspace-workspace-sync.mjs @@ -16421,6 +17289,17 @@ var NESTED_SKIP_BASENAMES = /* @__PURE__ */ new Set([ function asNumber3(value) { return Number(value ?? 0); } +function generatedArtifactKindForMime(mimeType, filename) { + const normalizedMime = String(mimeType ?? "").toLowerCase(); + const normalizedName = String(filename ?? "").toLowerCase(); + if (normalizedMime.startsWith("image/")) return "generated_image"; + if (normalizedName.endsWith(".docx") || normalizedName.endsWith(".doc")) return "docx"; + if (normalizedName.endsWith(".pdf")) return "pdf"; + return "generated_file"; +} +function workspaceAssetDownloadUrl(assetId) { + return `/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download`; +} function normalizeWorkspaceRelativePath(relativePath) { const normalized = String(relativePath ?? "").normalize("NFKC").trim().replace(/\\/g, "/"); if (!normalized || normalized.startsWith("/") || normalized.includes("\0")) return null; @@ -16432,7 +17311,7 @@ function normalizeWorkspaceRelativePath(relativePath) { function shouldSyncWorkspaceFilename(filename) { const relativePath = normalizeWorkspaceRelativePath(filename); if (!relativePath) return false; - const basename = path17.posix.basename(relativePath); + const basename = path18.posix.basename(relativePath); if (!basename || basename.startsWith(".")) return false; if (SKIP_FILENAMES.has(basename)) return false; if (basename.endsWith(".thumbnail.svg")) return false; @@ -16449,7 +17328,7 @@ async function walkWorkspaceZoneDir(zoneDir, relativePrefix, files) { } for (const entry of entries) { if (entry.name.startsWith(".")) continue; - const absolutePath = path17.join(zoneDir, entry.name); + const absolutePath = path18.join(zoneDir, entry.name); if (entry.isDirectory()) { if (SKIP_ZONE_DIR_NAMES.has(entry.name)) continue; const nextPrefix = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name; @@ -16475,7 +17354,14 @@ async function listWorkspaceZoneFiles(workspaceRoot, categoryCode) { await walkWorkspaceZoneDir(zoneDir, "", files); return files; } -function createWorkspaceAssetSync({ pool: pool2, storageRoot, h5Root, maxFileBytes, idFactory }) { +function createWorkspaceAssetSync({ + pool: pool2, + storageRoot, + h5Root, + maxFileBytes, + idFactory, + conversationPackageRegistry = null +}) { void storageRoot; const loadExistingAssets = async (userId, categoryId) => { const [rows] = await pool2.query( @@ -16555,7 +17441,7 @@ function createWorkspaceAssetSync({ pool: pool2, storageRoot, h5Root, maxFileByt assetInternals.assetTypeForMime(detectedMimeType), detectedMimeType, file.filename, - path17.basename(file.filename, path17.extname(file.filename)) || file.filename, + path18.basename(file.filename, path18.extname(file.filename)) || file.filename, versionId, buffer.length, checksum, @@ -16589,7 +17475,16 @@ function createWorkspaceAssetSync({ pool: pool2, storageRoot, h5Root, maxFileByt [buffer.length, now, category.space_id, userId] ); await conn.commit(); - return { action: "imported", assetId, filename: file.filename, checksum }; + return { + action: "imported", + assetId, + filename: file.filename, + checksum, + mimeType: detectedMimeType, + sizeBytes: buffer.length, + storageKey: finalStorageKey, + categoryCode: category.category_code + }; } catch (error) { await conn.rollback(); throw error; @@ -16687,7 +17582,16 @@ function createWorkspaceAssetSync({ pool: pool2, storageRoot, h5Root, maxFileByt ); } await conn.commit(); - return { action: "updated", assetId: existing.id, filename: file.filename, checksum }; + return { + action: "updated", + assetId: existing.id, + filename: file.filename, + checksum, + mimeType: detectedMimeType, + sizeBytes: buffer.length, + storageKey: finalStorageKey, + categoryCode: category.category_code + }; } catch (error) { await conn.rollback(); throw error; @@ -16695,7 +17599,39 @@ function createWorkspaceAssetSync({ pool: pool2, storageRoot, h5Root, maxFileByt conn.release(); } }; - const syncCategory = async (userId, categoryCode) => { + const registerWorkspaceArtifactForConversation = async (userId, source, result, now = Date.now()) => { + const sessionId = String(source?.sessionId ?? source?.sourceSessionId ?? "").trim(); + if (!conversationPackageRegistry || !sessionId || !result?.assetId) return null; + try { + const packageRecord = await conversationPackageRegistry.ensurePackage({ + userId, + sessionId, + title: source?.title ?? null, + now + }); + const artifact = await conversationPackageRegistry.recordArtifact({ + id: `ca_workspace_${result.assetId}`, + packageId: packageRecord.id, + artifactKind: generatedArtifactKindForMime(result.mimeType, result.filename), + role: "assistant", + assetId: result.assetId, + messageId: source?.messageId ?? source?.sourceMessageId ?? null, + displayName: result.filename, + mimeType: result.mimeType, + sizeBytes: result.sizeBytes, + storageKey: result.storageKey, + canonicalUrl: workspaceAssetDownloadUrl(result.assetId), + sortOrder: now, + now + }); + await conversationPackageRegistry.writeManifestForSession({ userId, sessionId }); + return artifact; + } catch (error) { + console.warn("[MindSpace] workspace conversation artifact registration failed:", error?.message ?? error); + return null; + } + }; + const syncCategory = async (userId, categoryCode, source = {}) => { if (!h5Root) return { imported: 0, updated: 0, skipped: 0 }; const workspaceRoot = resolveUserWorkspaceRoot(h5Root, { id: userId }); const files = await listWorkspaceZoneFiles(workspaceRoot, categoryCode); @@ -16730,24 +17666,27 @@ function createWorkspaceAssetSync({ pool: pool2, storageRoot, h5Root, maxFileByt continue; } if (existing) { - await updateWorkspaceFile(userId, category, existing, file, buffer); + const result = await updateWorkspaceFile(userId, category, existing, file, buffer); + await registerWorkspaceArtifactForConversation(userId, source, result); existing.checksum = checksum; updated += 1; } else { - await importWorkspaceFile(userId, category, file, buffer); + const result = await importWorkspaceFile(userId, category, file, buffer); + await registerWorkspaceArtifactForConversation(userId, source, result); existingByName.set(file.filename, { checksum }); imported += 1; } } return { imported, updated, skipped }; }; - const syncUserWorkspace = async (userId, { categoryCode } = {}) => { + const syncUserWorkspace = async (userId, { categoryCode, sourceSessionId, sourceMessageId, title } = {}) => { const codes = categoryCode ? [categoryCode] : UPLOAD_ZONE_CODES; let imported = 0; let updated = 0; let skipped = 0; + const source = { sessionId: sourceSessionId, messageId: sourceMessageId, title }; for (const code of codes) { - const result = await syncCategory(userId, code); + const result = await syncCategory(userId, code, source); imported += result.imported; updated += result.updated; skipped += result.skipped; @@ -16775,7 +17714,7 @@ function startWorkspaceAssetSyncWatcher({ publishRoot, syncUserWorkspaceByDirKey }; const watchZoneDir = (dirKey, zoneDir, categoryCode) => { try { - fs13.watch( + fs14.watch( zoneDir, { recursive: true }, (_event, filename) => { @@ -16793,7 +17732,7 @@ function startWorkspaceAssetSyncWatcher({ publishRoot, syncUserWorkspaceByDirKey } }; const attachUser = async (dirKey) => { - const userDir = path17.join(publishRoot, dirKey); + const userDir = path18.join(publishRoot, dirKey); try { await fsPromises2.access(userDir); } catch { @@ -16803,15 +17742,15 @@ function startWorkspaceAssetSyncWatcher({ publishRoot, syncUserWorkspaceByDirKey watchZoneDir(dirKey, resolveZoneDir(userDir, categoryCode), categoryCode); } }; - for (const entry of fs13.readdirSync(publishRoot, { withFileTypes: true })) { + for (const entry of fs14.readdirSync(publishRoot, { withFileTypes: true })) { if (!entry.isDirectory() || entry.name === "wiki" || entry.name.startsWith(".")) continue; void attachUser(entry.name); } try { - fs13.watch(publishRoot, (_event, filename) => { + fs14.watch(publishRoot, (_event, filename) => { if (!filename) return; - const userDir = path17.join(publishRoot, filename); - if (fs13.existsSync(userDir) && fs13.statSync(userDir).isDirectory()) { + const userDir = path18.join(publishRoot, filename); + if (fs14.existsSync(userDir) && fs14.statSync(userDir).isDirectory()) { void attachUser(filename); } }); @@ -16933,8 +17872,8 @@ function assertMindSpaceRoute(flags, route) { // mindspace-pages.mjs import crypto12 from "node:crypto"; -import fs16 from "node:fs/promises"; -import path20 from "node:path"; +import fs17 from "node:fs/promises"; +import path21 from "node:path"; // mindspace-content-scan.mjs import crypto11 from "node:crypto"; @@ -17189,8 +18128,8 @@ function redactContent(content, options = {}) { } // mindspace-html-download-links.mjs -import fs14 from "node:fs"; -import path18 from "node:path"; +import fs15 from "node:fs"; +import path19 from "node:path"; var DOWNLOADABLE_FILE_PATTERN = /\.(?:docx?|pdf|xlsx?|pptx?|zip|csv|txt|md)$/i; var URL_ATTR_PATTERN = /(\b(?:href|src)\s*=\s*["'])([^"']+)(["'])/gi; function isRelativeDownloadReference(value) { @@ -17214,9 +18153,9 @@ function resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef) { if (!ref) return ref; if (/^(?:public|oa|private)\//.test(ref)) return ref; const htmlPath = String(htmlRelativePath ?? "public/index.html").replace(/^\/+/, ""); - const htmlDir = path18.posix.dirname(htmlPath); + const htmlDir = path19.posix.dirname(htmlPath); if (!htmlDir || htmlDir === ".") return `public/${ref}`; - return path18.posix.join(htmlDir, ref).replace(/\\/g, "/"); + return path19.posix.join(htmlDir, ref).replace(/\\/g, "/"); } function resolveSiblingDocxPath(htmlRelativePath) { const htmlPath = String(htmlRelativePath ?? "").replace(/^\/+/, ""); @@ -17233,20 +18172,20 @@ function inferWorkspaceHtmlRelativePath({ if (fromSnapshot) { const normalized = fromSnapshot.replace(/^\/+/, ""); if (normalized.toLowerCase().startsWith("public/")) return normalized; - if (normalized.toLowerCase().endsWith(".html")) return `public/${path18.posix.basename(normalized)}`; + if (normalized.toLowerCase().endsWith(".html")) return `public/${path19.posix.basename(normalized)}`; return normalized; } - const publicDir = publishDir ? path18.join(publishDir, "public") : null; - if (!publicDir || !fs14.existsSync(publicDir) || !htmlContent) { + const publicDir = publishDir ? path19.join(publishDir, "public") : null; + if (!publicDir || !fs15.existsSync(publicDir) || !htmlContent) { return "public/index.html"; } const normalizedContent = String(htmlContent); const title = String(pageTitle ?? "").trim(); - for (const name of fs14.readdirSync(publicDir)) { + for (const name of fs15.readdirSync(publicDir)) { if (!name.toLowerCase().endsWith(".html")) continue; - const absolutePath = path18.join(publicDir, name); + const absolutePath = path19.join(publicDir, name); try { - const diskContent = fs14.readFileSync(absolutePath, "utf8"); + const diskContent = fs15.readFileSync(absolutePath, "utf8"); if (diskContent === normalizedContent) return `public/${name}`; if (title) { const titleMatch = diskContent.match(/]*>([^<]+)<\/title>/i); @@ -17265,7 +18204,7 @@ function buildWorkspaceDownloadLinkIndex(assets = []) { const filename = String(asset.original_filename ?? asset.originalFilename ?? "").replace(/\\/g, "/"); if (!id || !filename) continue; byPath.set(filename, id); - byBasename.set(path18.posix.basename(filename), id); + byBasename.set(path19.posix.basename(filename), id); if (filename.startsWith("public/")) { byPath.set(filename.slice("public/".length), id); } else if (!filename.includes("/")) { @@ -17277,7 +18216,7 @@ function buildWorkspaceDownloadLinkIndex(assets = []) { function lookupAssetIdForWorkspacePath(index, workspaceRelativePath) { const normalized = String(workspaceRelativePath ?? "").replace(/\\/g, "/"); if (!normalized || !index) return null; - return index.byPath.get(normalized) ?? index.byPath.get(`public/${normalized}`) ?? index.byBasename.get(path18.posix.basename(normalized)) ?? null; + return index.byPath.get(normalized) ?? index.byPath.get(`public/${normalized}`) ?? index.byBasename.get(path19.posix.basename(normalized)) ?? null; } function buildAssetDownloadUrl(assetId) { return `/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download`; @@ -17288,9 +18227,9 @@ function buildPublicWorkspaceFileUrl(publicBaseUrl, publishKey, workspaceRelativ } function workspaceFileExists(publishDir, workspaceRelativePath) { if (!publishDir || !workspaceRelativePath) return false; - const absolutePath = path18.join(publishDir, ...String(workspaceRelativePath).split("/")); + const absolutePath = path19.join(publishDir, ...String(workspaceRelativePath).split("/")); try { - return fs14.existsSync(absolutePath) && fs14.statSync(absolutePath).isFile(); + return fs15.existsSync(absolutePath) && fs15.statSync(absolutePath).isFile(); } catch { return false; } @@ -17299,12 +18238,12 @@ function collectDownloadWorkspaceCandidates(htmlRelativePath, relativeRef, publi const workspacePath = resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef); const candidates = /* @__PURE__ */ new Set([ workspacePath, - path18.posix.join("public", path18.posix.basename(workspacePath)) + path19.posix.join("public", path19.posix.basename(workspacePath)) ]); const siblingDocx = resolveSiblingDocxPath(htmlRelativePath); if (siblingDocx) { candidates.add(siblingDocx); - candidates.add(path18.posix.join("public", path18.posix.basename(siblingDocx))); + candidates.add(path19.posix.join("public", path19.posix.basename(siblingDocx))); } return [...candidates]; } @@ -17370,8 +18309,9 @@ async function prepareHtmlDownloadLinks(pool2, userId, html, options = {}) { // mindspace-page-tag.mjs var MINDSPACE_PAGE_TAG_ATTR = "data-mindspace-page-tag"; var MINDSPACE_PAGE_TAG_PLATFORM_BRAND = "platform-brand"; -var PLATFORM_BRAND_INNER_RE = /(?:contact@tkmind\.(?:ai|cn)|📧[^<]*tkmind)/i; -var SIMPLE_BRAND_ELEMENT_RE = /<(p|div|span|small|footer|a)(\s[^>]*)?>([^<]*(?:contact@tkmind\.(?:ai|cn)|📧[^<]*tkmind)[^<]*)<\/\1>/gi; +var PLATFORM_BRAND_TEXT = "TKMind \xB7 \u667A\u8DA3"; +var PLATFORM_BRAND_INNER_RE = /(?:TKMind\s*·\s*智趣|contact@tkmind\.(?:ai|cn)|📧[^<]*tkmind)/i; +var SIMPLE_BRAND_ELEMENT_RE = /<(p|div|span|small|footer|a)(\s[^>]*)?>([^<]*(?:TKMind\s*·\s*智趣|contact@tkmind\.(?:ai|cn)|📧[^<]*tkmind)[^<]*)<\/\1>/gi; function normalizePlatformDomainText(text) { return String(text ?? "").replace(/tkmind\.ai/gi, "tkmind.cn"); } @@ -17382,6 +18322,7 @@ function normalizePlatformBrandHtml(html) { /https?:\/\/([a-z0-9.-]*\.)?tkmind\.ai/gi, (match) => match.replace(/tkmind\.ai/gi, "tkmind.cn") ); + next = next.replace(/(?:📧\s*)?contact@tkmind\.cn/gi, PLATFORM_BRAND_TEXT); return next; } function ensurePlatformBrandPageTags(html) { @@ -17399,8 +18340,8 @@ function prepareHtmlPageBrandMarkers(html) { } // mindspace-page-purge.mjs -import fs15 from "node:fs/promises"; -import path19 from "node:path"; +import fs16 from "node:fs/promises"; +import path20 from "node:path"; var PRIVATE_ASSET_URL_PATTERN = /(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi; var URL_ATTR_PATTERN2 = /(\b(?:href|src)\s*=\s*["'])([^"']+)(["'])/gi; function extractAssetIdsFromHtml(html) { @@ -17442,17 +18383,17 @@ async function purgeWorkspacePageArtifacts({ html = "" } = {}) { if (!publishDir) return { removed: [], skipped: [] }; - const resolvedRoot = path19.resolve(publishDir); + const resolvedRoot = path20.resolve(publishDir); const removed = []; const skipped = []; for (const relativeTarget of collectWorkspacePurgeTargets(htmlRelativePath, html)) { - const absolutePath = path19.resolve(publishDir, ...relativeTarget.split("/")); - if (absolutePath !== resolvedRoot && !absolutePath.startsWith(`${resolvedRoot}${path19.sep}`)) { + const absolutePath = path20.resolve(publishDir, ...relativeTarget.split("/")); + if (absolutePath !== resolvedRoot && !absolutePath.startsWith(`${resolvedRoot}${path20.sep}`)) { skipped.push(relativeTarget); continue; } try { - await fs15.unlink(absolutePath); + await fs16.unlink(absolutePath); removed.push(relativeTarget); } catch (error) { if (error?.code !== "ENOENT") skipped.push(relativeTarget); @@ -17570,7 +18511,7 @@ function normalizePageInput(input) { }; } function extensionForMime2(mimeType, filename = "") { - const ext = path20.extname(String(filename ?? "")).replace(/^\./, "").toLowerCase(); + const ext = path21.extname(String(filename ?? "")).replace(/^\./, "").toLowerCase(); if (ext && /^[a-z0-9]{1,8}$/.test(ext)) return ext === "jpeg" ? "jpg" : ext; if (mimeType === "image/jpeg") return "jpg"; if (mimeType === "image/png") return "png"; @@ -17697,9 +18638,10 @@ function renderPublicationHtml(page) { ); } function createPageService(pool2, options = {}) { - const storageRoot = path20.resolve(options.storageRoot ?? path20.join(process.cwd(), "data", "mindspace")); - const h5Root = options.h5Root ? path20.resolve(options.h5Root) : null; + const storageRoot = path21.resolve(options.storageRoot ?? path21.join(process.cwd(), "data", "mindspace")); + const h5Root = options.h5Root ? path21.resolve(options.h5Root) : null; const idFactory = options.idFactory ?? (() => crypto12.randomUUID()); + const conversationPackageRegistry = options.conversationPackageRegistry ?? null; const resolveWorkspacePublishDir = async (userId) => { if (!h5Root) return null; const [rows] = await pool2.query( @@ -17711,8 +18653,8 @@ function createPageService(pool2, options = {}) { return resolvePublishDir(h5Root, { id: userId }); }; const absoluteStoragePath2 = (storageKey) => { - const resolved = path20.resolve(storageRoot, storageKey); - if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path20.sep}`)) { + const resolved = path21.resolve(storageRoot, storageKey); + if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path21.sep}`)) { throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C"); } return resolved; @@ -17734,7 +18676,7 @@ function createPageService(pool2, options = {}) { for (const candidate of resolveStoragePathCandidates(storageKey)) { const absolutePath = absoluteStoragePath2(candidate); try { - await fs16.stat(absolutePath); + await fs17.stat(absolutePath); return absolutePath; } catch (error) { if (error?.code === "ENOENT") { @@ -17747,7 +18689,7 @@ function createPageService(pool2, options = {}) { throw lastError ?? Object.assign(new Error("\u5B58\u50A8\u6587\u4EF6\u4E0D\u5B58\u5728"), { code: "storage_not_found" }); }; const writeVersionContent = async (userId, assetId, versionId, content) => { - const storageKey = path20.posix.join( + const storageKey = path21.posix.join( "users", userId, "pages", @@ -17756,8 +18698,8 @@ function createPageService(pool2, options = {}) { `${versionId}.md` ); const target = absoluteStoragePath2(storageKey); - await fs16.mkdir(path20.dirname(target), { recursive: true }); - await fs16.writeFile(target, content, { flag: "wx" }); + await fs17.mkdir(path21.dirname(target), { recursive: true }); + await fs17.writeFile(target, content, { flag: "wx" }); return { storageKey, target }; }; const createVersion = async (userId, input, source = {}) => { @@ -17814,14 +18756,14 @@ function createPageService(pool2, options = {}) { LIMIT 1 FOR UPDATE`, [pageId, userId] ); - const page = pages[0]; - if (!page) throw pageError("\u9875\u9762\u4E0D\u5B58\u5728", "page_not_found"); - if (asNumber4(input.expectedVersion) !== asNumber4(page.version_no)) { + const page2 = pages[0]; + if (!page2) throw pageError("\u9875\u9762\u4E0D\u5B58\u5728", "page_not_found"); + if (asNumber4(input.expectedVersion) !== asNumber4(page2.version_no)) { throw pageError("\u9875\u9762\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "version_conflict", { - currentVersion: asNumber4(page.version_no) + currentVersion: asNumber4(page2.version_no) }); } - nextVersionNo = asNumber4(page.version_no) + 1; + nextVersionNo = asNumber4(page2.version_no) + 1; } else { pageId = idFactory(); } @@ -17981,10 +18923,23 @@ function createPageService(pool2, options = {}) { }); }); } - return getPage(userId, pageId); + const page = await getPage(userId, pageId); + await registerPageArtifactForConversation({ + registry: conversationPackageRegistry, + userId, + source, + page, + contentAssetId, + contentMimeType, + contentBytes: normalized.contentBytes, + storageKey: stored.storageKey, + versionNo: nextVersionNo, + now + }); + return page; } catch (error) { await conn.rollback(); - if (writtenPath) await fs16.rm(writtenPath, { force: true }).catch(() => { + if (writtenPath) await fs17.rm(writtenPath, { force: true }).catch(() => { }); throw error; } finally { @@ -18006,8 +18961,8 @@ function createPageService(pool2, options = {}) { ); return rows[0] ? pageResponse(rows[0]) : null; }; - const findPageBySourceMessage = async (userId, sessionId, messageId) => { - if (!sessionId || !messageId) return null; + const findPageBySourceMessage = async (userId, sessionId, messageId2) => { + if (!sessionId || !messageId2) return null; const [rows] = await pool2.query( `SELECT p.*, c.category_code, pv.version_no, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url FROM h5_page_records p @@ -18017,7 +18972,7 @@ function createPageService(pool2, options = {}) { WHERE p.user_id = ? AND p.source_session_id = ? AND p.source_message_id = ? AND p.status <> 'deleted' ORDER BY p.updated_at DESC LIMIT 1`, - [userId, sessionId, messageId] + [userId, sessionId, messageId2] ); return rows[0] ? pageResponse(rows[0]) : null; }; @@ -18090,7 +19045,7 @@ function createPageService(pool2, options = {}) { const row = rows[0]; if (!row) throw pageError("\u9875\u9762\u4E0D\u5B58\u5728", "page_not_found"); const storagePath = await resolveReadableStoragePath(row.storage_key); - const content = await fs16.readFile(storagePath, "utf8"); + const content = await fs17.readFile(storagePath, "utf8"); return pageResponse({ ...row, content }); } const listVersions = async (userId, pageId) => { @@ -18275,7 +19230,7 @@ ${updated.content ?? ""}`, for (const assetId of assetIds) { const asset = byId.get(assetId); if (!asset) continue; - const buffer = await fs16.readFile(absoluteStoragePath2(asset.storage_key)); + const buffer = await fs17.readFile(absoluteStoragePath2(asset.storage_key)); const mimeType = String(asset.mime_type || "application/octet-stream"); const ext = extensionForMime2(mimeType, asset.original_filename); const dataUri = `data:${mimeType};base64,${buffer.toString("base64")}`; @@ -18644,7 +19599,7 @@ ${updated.content ?? ""}`, throw pageError("\u8BE5\u9875\u9762\u4E0D\u652F\u6301\u7F29\u7565\u56FE", "thumbnail_not_supported"); } const storagePath = await resolveReadableStoragePath(row.storage_key); - const content = await fs16.readFile(storagePath, "utf8"); + const content = await fs17.readFile(storagePath, "utf8"); let snapshot = {}; try { snapshot = parseJsonColumn(row.source_snapshot_json); @@ -18810,6 +19765,53 @@ ${updated.content ?? ""}`, regenerateThumbnail }; } +async function registerPageArtifactForConversation({ + registry, + userId, + source, + page, + contentAssetId, + contentMimeType, + contentBytes, + storageKey, + versionNo, + now +}) { + const sessionId = String(source?.sessionId ?? "").trim(); + if (!registry || !sessionId || !page?.id) return null; + try { + const packageRecord = await registry.ensurePackage({ + userId, + sessionId, + title: page.title, + now + }); + const artifact = await registry.recordArtifact({ + id: `ca_${page.id}_${page.currentVersionId}`, + packageId: packageRecord.id, + artifactKind: "page", + role: "assistant", + assetId: contentAssetId, + pageId: page.id, + messageId: source.messageId ?? page.sourceMessageId ?? null, + displayName: page.title, + mimeType: contentMimeType, + sizeBytes: contentBytes, + storageKey, + canonicalUrl: `/api/mindspace/v1/pages/${encodeURIComponent(page.id)}/preview`, + sortOrder: Number.isFinite(Number(versionNo)) ? Number(versionNo) : 0, + now + }); + await registry.writeManifestForSession({ userId, sessionId }); + return artifact; + } catch (error) { + console.warn( + "[MindSpace] conversation package page artifact registration failed:", + error instanceof Error ? error.message : error + ); + return null; + } +} var pageInternals = { escapeHtml: escapeHtml2, normalizePageInput, @@ -18821,15 +19823,16 @@ var pageInternals = { previewContentSecurityPolicy, isHtmlPage, renderPublicationHtml, + registerPageArtifactForConversation, redactContent, buildPageDeleteSummary }; async function inlinePrivateAssetsInHtml(pool2, storageRoot, userId, html) { const PATTERN = /(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi; - const resolvedRoot = path20.resolve(storageRoot); + const resolvedRoot = path21.resolve(storageRoot); const absoluteStoragePath2 = (key) => { - const resolved = path20.resolve(resolvedRoot, key); - if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path20.sep}`)) { + const resolved = path21.resolve(resolvedRoot, key); + if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path21.sep}`)) { throw Object.assign(new Error("\u8DEF\u5F84\u8D8A\u754C"), { code: "invalid_storage_path" }); } return resolved; @@ -18848,7 +19851,7 @@ async function inlinePrivateAssetsInHtml(pool2, storageRoot, userId, html) { const dataUriMap = /* @__PURE__ */ new Map(); for (const asset of assets) { try { - const buffer = await fs16.readFile(absoluteStoragePath2(asset.storage_key)); + const buffer = await fs17.readFile(absoluteStoragePath2(asset.storage_key)); dataUriMap.set(asset.id, `data:${asset.mime_type};base64,${buffer.toString("base64")}`); } catch { } @@ -18897,6 +19900,2131 @@ function formatByteSize(bytes) { return `${(value / (1024 * 1024)).toFixed(value % (1024 * 1024) === 0 ? 0 : 1)} MB`; } +// mindspace-conversation-package-registry.mjs +function registryError(message, code) { + return Object.assign(new Error(message), { code }); +} +function requireMethod(target, methodName, code) { + if (!target || typeof target[methodName] !== "function") { + throw registryError(`MindSpace conversation package registry requires ${methodName}`, code); + } +} +function createConversationPackageRegistry({ store, service } = {}) { + requireMethod(store, "upsertPackage", "missing_conversation_package_store"); + requireMethod(store, "upsertArtifact", "missing_conversation_package_store"); + requireMethod(store, "getPackageBySession", "missing_conversation_package_store"); + requireMethod(store, "listArtifacts", "missing_conversation_package_store"); + requireMethod(service, "putConversationManifest", "missing_mindspace_service"); + requireMethod(service, "buildConversationManifest", "missing_mindspace_service"); + requireMethod(service, "putPackageObject", "missing_mindspace_service"); + requireMethod(service, "getPackageObject", "missing_mindspace_service"); + return { + async ensurePackage(input) { + return store.upsertPackage(input); + }, + async recordArtifact(input) { + return store.upsertArtifact(input); + }, + async writeManifestForSession({ userId, sessionId }) { + const packageRecord = await store.getPackageBySession(userId, sessionId); + if (!packageRecord) return null; + const artifacts = await store.listArtifacts(packageRecord.id); + return service.putConversationManifest({ packageRecord, artifacts }); + }, + async putObjectForSession({ userId, sessionId, title = null, relativePath, body }) { + const packageRecord = await store.upsertPackage({ userId, sessionId, title }); + const writeResult = await service.putPackageObject({ packageRecord, relativePath, body }); + return { packageRecord, writeResult }; + }, + async readManifestForSession({ userId, sessionId }) { + const packageRecord = await store.getPackageBySession(userId, sessionId); + if (!packageRecord) return null; + const artifacts = await store.listArtifacts(packageRecord.id); + return service.buildConversationManifest({ packageRecord, artifacts }); + }, + async readArtifactObject({ userId, sessionId, artifactId }) { + const packageRecord = await store.getPackageBySession(userId, sessionId); + if (!packageRecord) return null; + const artifacts = await store.listArtifacts(packageRecord.id); + const artifact = artifacts.find((item) => item.id === artifactId); + if (!artifact?.storageKey) return null; + const prefix = `${packageRecord.storagePrefix}/`; + if (!artifact.storageKey.startsWith(prefix)) { + throw registryError("conversation artifact storage key is outside package", "invalid_artifact_storage_key"); + } + const body = await service.getPackageObject({ + packageRecord, + relativePath: artifact.storageKey.slice(prefix.length) + }); + return { packageRecord, artifact, body }; + } + }; +} + +// mindspace-conversation-package-routes.mjs +function defaultSendData(res, _req, data) { + return res.json({ data }); +} +function defaultRouteError(res, _req, error) { + return res.status(500).json({ message: error?.message || "MindSpace \u670D\u52A1\u5F02\u5E38" }); +} +function resolveRegistry(input) { + return typeof input === "function" ? input() : input; +} +function encodeDownloadFilename(filename) { + const safe = String(filename ?? "").trim() || "mindspace-artifact"; + return `attachment; filename="${encodeURIComponent(safe)}"; filename*=UTF-8''${encodeURIComponent(safe)}`; +} +function createGetConversationPackageHandler({ + registry, + getRegistry, + userAuth: userAuth2, + getUserAuth, + ensureMindSpaceEnabled: ensureMindSpaceEnabled2 = () => true, + sendData: sendData2 = defaultSendData, + mindSpaceError: mindSpaceError2 = defaultRouteError, + beforeReadManifest +} = {}) { + return async function getConversationPackage(req, res) { + const activeRegistry = resolveRegistry(getRegistry ?? registry); + if (!activeRegistry || !ensureMindSpaceEnabled2(res, req)) { + if (res.headersSent) return void 0; + return res.status(503).json({ message: "MindSpace \u5BF9\u8BDD\u5305\u672A\u542F\u7528" }); + } + const sessionId = String(req.params?.sessionId ?? "").trim(); + if (!sessionId) return res.status(400).json({ message: "\u7F3A\u5C11 sessionId" }); + try { + const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth2); + if (!await activeUserAuth.ownsSession(req.currentUser.id, sessionId)) { + return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); + } + if (typeof beforeReadManifest === "function") { + await beforeReadManifest({ req, userId: req.currentUser.id, sessionId }); + } + const manifest = await activeRegistry.readManifestForSession({ + userId: req.currentUser.id, + sessionId + }); + if (!manifest) return res.status(404).json({ message: "\u5BF9\u8BDD\u5305\u4E0D\u5B58\u5728" }); + return sendData2(res, req, manifest); + } catch (error) { + return mindSpaceError2(res, req, error); + } + }; +} +function createDownloadConversationPackageManifestHandler({ + registry, + getRegistry, + userAuth: userAuth2, + getUserAuth, + ensureMindSpaceEnabled: ensureMindSpaceEnabled2 = () => true, + mindSpaceError: mindSpaceError2 = defaultRouteError, + beforeReadManifest +} = {}) { + return async function downloadConversationPackageManifest(req, res) { + const activeRegistry = resolveRegistry(getRegistry ?? registry); + if (!activeRegistry || !ensureMindSpaceEnabled2(res, req)) { + if (res.headersSent) return void 0; + return res.status(503).json({ message: "MindSpace \u5BF9\u8BDD\u5305\u672A\u542F\u7528" }); + } + const sessionId = String(req.params?.sessionId ?? "").trim(); + if (!sessionId) return res.status(400).json({ message: "\u7F3A\u5C11 sessionId" }); + try { + const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth2); + if (!await activeUserAuth.ownsSession(req.currentUser.id, sessionId)) { + return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); + } + if (typeof beforeReadManifest === "function") { + await beforeReadManifest({ req, userId: req.currentUser.id, sessionId }); + } + const manifest = await activeRegistry.readManifestForSession({ + userId: req.currentUser.id, + sessionId + }); + if (!manifest) return res.status(404).json({ message: "\u5BF9\u8BDD\u5305\u4E0D\u5B58\u5728" }); + const filename = `${manifest.packageId || sessionId}-manifest.json`; + res.set("Content-Type", "application/json; charset=utf-8"); + res.set("Content-Disposition", encodeDownloadFilename(filename)); + res.set("Cache-Control", "private, no-store"); + if (req.requestId) res.setHeader("X-Request-Id", req.requestId); + return res.send(`${JSON.stringify(manifest, null, 2)} +`); + } catch (error) { + return mindSpaceError2(res, req, error); + } + }; +} +function createDownloadConversationPackageArtifactHandler({ + registry, + getRegistry, + userAuth: userAuth2, + getUserAuth, + ensureMindSpaceEnabled: ensureMindSpaceEnabled2 = () => true, + mindSpaceError: mindSpaceError2 = defaultRouteError +} = {}) { + return async function downloadConversationPackageArtifact(req, res) { + const activeRegistry = resolveRegistry(getRegistry ?? registry); + if (!activeRegistry || !ensureMindSpaceEnabled2(res, req)) { + if (res.headersSent) return void 0; + return res.status(503).json({ message: "MindSpace \u5BF9\u8BDD\u5305\u672A\u542F\u7528" }); + } + const sessionId = String(req.params?.sessionId ?? "").trim(); + const artifactId = String(req.params?.artifactId ?? "").trim(); + if (!sessionId) return res.status(400).json({ message: "\u7F3A\u5C11 sessionId" }); + if (!artifactId) return res.status(400).json({ message: "\u7F3A\u5C11 artifactId" }); + try { + const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth2); + if (!await activeUserAuth.ownsSession(req.currentUser.id, sessionId)) { + return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); + } + const result = await activeRegistry.readArtifactObject({ + userId: req.currentUser.id, + sessionId, + artifactId + }); + if (!result) return res.status(404).json({ message: "\u6587\u4EF6\u4E0D\u5B58\u5728" }); + const filename = result.artifact.displayName || result.artifact.id; + res.set("Content-Type", result.artifact.mimeType || "application/octet-stream"); + res.set("Content-Disposition", encodeDownloadFilename(filename)); + res.set("Cache-Control", "private, no-store"); + if (req.requestId) res.setHeader("X-Request-Id", req.requestId); + return res.send(result.body); + } catch (error) { + return mindSpaceError2(res, req, error); + } + }; +} + +// mindspace-conversation-package-backfill.mjs +import fs19 from "node:fs/promises"; +import path23 from "node:path"; + +// mindspace-publications.mjs +import crypto13 from "node:crypto"; +import fs18 from "node:fs/promises"; +import path22 from "node:path"; + +// mindspace-html-localize.mjs +var GOOGLE_FONTS_CSS_IMPORT_RE = /@import\s+url\(\s*['"]?(https:\/\/fonts\.googleapis\.com\/[^'")\s]+)['"]?\s*\)\s*;?/gi; +var GOOGLE_FONTS_LINK_RE = /]*\bhref=['"](https:\/\/fonts\.googleapis\.com\/[^'"]+)['"][^>]*>/gi; +async function fetchStylesheet(url) { + const response = await fetch(url, { + headers: { "User-Agent": "TKMind-MindSpace/1.0" }, + signal: AbortSignal.timeout(15e3) + }); + if (!response.ok) { + throw new Error(`\u65E0\u6CD5\u4E0B\u8F7D\u6837\u5F0F\u8868: ${response.status}`); + } + return response.text(); +} +function uniqueUrls(matches) { + return [...new Set(matches.map((value) => value.replace(/&/g, "&")))]; +} +async function localizeGoogleFontsCss(html) { + const source = String(html); + const importUrls = uniqueUrls([...source.matchAll(GOOGLE_FONTS_CSS_IMPORT_RE)].map((m) => m[1])); + const linkUrls = uniqueUrls([...source.matchAll(GOOGLE_FONTS_LINK_RE)].map((m) => m[1])); + const stylesheetUrls = uniqueUrls([...importUrls, ...linkUrls]); + if (!stylesheetUrls.length) { + return { html: source, localizedCount: 0, inlinedBytes: 0 }; + } + const inlinedBlocks = []; + for (const url of stylesheetUrls) { + try { + inlinedBlocks.push(`/* localized from ${url} */ +${await fetchStylesheet(url)}`); + } catch { + inlinedBlocks.push(`/* failed to localize ${url} */`); + } + } + let next = source; + for (const url of importUrls) { + const pattern = new RegExp( + `@import\\s+url\\(\\s*['"]?${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]?\\s*\\)\\s*;?`, + "gi" + ); + next = next.replace(pattern, ""); + } + next = next.replace(GOOGLE_FONTS_LINK_RE, ""); + const styleBlock = ``; + if (/]*>/i.test(next)) { + next = next.replace(/]*)>/i, `${styleBlock}`); + } else if (/]*>/i.test(next)) { + next = next.replace(/]*)>/i, `${styleBlock}`); + } else { + next = `${styleBlock} +${next}`; + } + const inlinedBytes = Buffer.byteLength(inlinedBlocks.join("\n")); + return { html: next, localizedCount: stylesheetUrls.length, inlinedBytes }; +} + +// mindspace-publications.mjs +var SCANNER_VERSION = "mindspace-content-v1"; +var PRIVATE_ASSET_DOWNLOAD_URL_PATTERN = /(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi; +var URL_ATTR_PATTERN3 = /(\b(?:href|src)\s*=\s*["'])([^"']+)(["'])/gi; +var ACCESS_MODES = /* @__PURE__ */ new Set([ + "public", + "password", + "private_link", + "time_limited", + "login_required", + "owner_only" +]); +var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/; +function publicationError(message, code, details) { + return Object.assign(new Error(message), { code, details }); +} +function normalizeSlug(value) { + const slug = String(value ?? "").normalize("NFKC").trim().toLowerCase(); + if (!SLUG_PATTERN.test(slug)) { + throw publicationError("\u9875\u9762\u5730\u5740\u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26", "invalid_publish_input"); + } + return slug; +} +async function findAvailableSlug(pool2, userId, preferredSlug, excludePageId) { + const normalized = normalizeSlug(preferredSlug); + const candidates = [normalized]; + for (let index = 2; index <= 20; index += 1) { + candidates.push(`${normalized}-${index}`); + } + candidates.push(`${normalized}-${excludePageId.replace(/-/g, "").slice(0, 8)}`); + for (const candidate of candidates) { + if (!SLUG_PATTERN.test(candidate)) continue; + const [rows] = await pool2.query( + `SELECT id FROM h5_publish_records + WHERE user_id = ? AND url_slug = ? AND status = 'online' AND page_id <> ? + LIMIT 1`, + [userId, candidate, excludePageId] + ); + if (!rows[0]) return candidate; + } + return `${normalized}-${crypto13.randomBytes(3).toString("hex")}`; +} +function normalizeAccessMode(value) { + const mode = String(value ?? "public"); + if (!ACCESS_MODES.has(mode)) { + throw publicationError("\u4E0D\u652F\u6301\u7684\u8BBF\u95EE\u65B9\u5F0F", "invalid_publish_input"); + } + return mode; +} +function normalizePassword(value, required) { + const password = String(value ?? ""); + if (!required && !password) return null; + if (password.length < 8 || password.length > 128) { + throw publicationError("\u8BBF\u95EE\u5BC6\u7801\u957F\u5EA6\u5FC5\u987B\u4E3A 8 \u5230 128 \u4E2A\u5B57\u7B26", "invalid_publish_input"); + } + return password; +} +function normalizeExpiresAt(value, required) { + if (!required && (value == null || value === "")) return null; + const expiresAt = Number(value); + if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now()) { + throw publicationError("\u8FC7\u671F\u65F6\u95F4\u5FC5\u987B\u665A\u4E8E\u5F53\u524D\u65F6\u95F4", "invalid_publish_input"); + } + return expiresAt; +} +function hashPassword(password) { + const salt = crypto13.randomBytes(16); + const hash = crypto13.scryptSync(password, salt, 32); + return `scrypt$${salt.toString("hex")}$${hash.toString("hex")}`; +} +function verifyPassword2(password, encoded) { + const [algorithm, saltHex, hashHex] = String(encoded ?? "").split("$"); + if (algorithm !== "scrypt" || !saltHex || !hashHex) return false; + const expected = Buffer.from(hashHex, "hex"); + const actual = crypto13.scryptSync(String(password ?? ""), Buffer.from(saltHex, "hex"), expected.length); + return expected.length === actual.length && crypto13.timingSafeEqual(expected, actual); +} +function deviceType(userAgent) { + const value = String(userAgent ?? ""); + if (/bot|crawler|spider|slurp/i.test(value)) return "bot"; + if (/ipad|tablet|kindle|silk/i.test(value)) return "tablet"; + if (/mobile|iphone|android/i.test(value)) return "mobile"; + if (value) return "desktop"; + return "unknown"; +} +function referrerHost(referrer) { + if (!referrer) return null; + try { + return new URL(referrer).hostname.slice(0, 255) || null; + } catch { + return null; + } +} +function publicationResponse(row) { + if (!row) return null; + return { + id: row.id, + pageId: row.page_id, + pageVersionId: row.page_version_id, + urlSlug: row.url_slug, + publicUrl: row.public_url, + accessMode: row.access_mode, + expiresAt: row.expires_at == null ? null : Number(row.expires_at), + status: row.status, + viewCount: Number(row.view_count ?? 0), + publishedAt: Number(row.published_at), + offlineAt: row.offline_at == null ? null : Number(row.offline_at) + }; +} +function publicHomepageResponse(owner, pages) { + const totalViews = pages.reduce( + (sum, page) => sum + Number(page.viewCount ?? page.view_count ?? 0), + 0 + ); + return { + owner: { + id: owner.id, + slug: owner.slug, + username: owner.username, + displayName: owner.display_name || owner.username + }, + totalViews, + pageCount: pages.length, + pages: pages.map((page) => ({ + id: page.id, + pageId: page.page_id, + title: page.title, + summary: page.summary ?? "", + templateId: page.template_id, + publicUrl: page.public_url, + urlSlug: page.url_slug, + viewCount: Number(page.view_count ?? 0), + publishedAt: Number(page.published_at) + })) + }; +} +function buildPublicationThumbnailFallback(ownerSlug, urlSlug) { + return `/u/${encodeURIComponent(ownerSlug)}/pages/${encodeURIComponent(urlSlug)}.thumbnail.png`; +} +function companionKindForWorkspacePath(workspacePath) { + const normalized = String(workspacePath ?? "").toLowerCase(); + if (normalized.endsWith(".long.png")) return "long_image"; + if (normalized.endsWith(".docx") || normalized.endsWith(".doc")) return "docx"; + if (normalized.endsWith(".pdf")) return "pdf"; + return "generated_file"; +} +function companionMimeForWorkspacePath(workspacePath) { + const normalized = String(workspacePath ?? "").toLowerCase(); + if (normalized.endsWith(".long.png") || normalized.endsWith(".png")) return "image/png"; + if (normalized.endsWith(".docx")) { + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + } + if (normalized.endsWith(".doc")) return "application/msword"; + if (normalized.endsWith(".pdf")) return "application/pdf"; + if (normalized.endsWith(".csv")) return "text/csv"; + if (normalized.endsWith(".md")) return "text/markdown"; + if (normalized.endsWith(".txt")) return "text/plain"; + return "application/octet-stream"; +} +async function statWorkspaceFile(publishDir, workspacePath) { + if (!publishDir || !workspacePath) return null; + const normalized = String(workspacePath).replace(/^\/+/, "").replace(/\\/g, "/"); + const root = path22.resolve(publishDir); + const absolutePath = path22.resolve(root, ...normalized.split("/")); + if (absolutePath !== root && !absolutePath.startsWith(`${root}${path22.sep}`)) return null; + try { + const stat = await fs18.stat(absolutePath); + if (!stat.isFile()) return null; + return { workspacePath: normalized, sizeBytes: stat.size }; + } catch (error) { + if (error?.code !== "ENOENT") throw error; + return null; + } +} +async function collectPublicationCompanionArtifacts({ + publishDir, + html = "", + htmlRelativePath = null +}) { + if (!publishDir || !htmlRelativePath) return []; + const candidates = /* @__PURE__ */ new Set(); + const normalizedHtmlPath = String(htmlRelativePath).replace(/^\/+/, "").replace(/\\/g, "/"); + if (normalizedHtmlPath.toLowerCase().endsWith(".html")) { + candidates.add(normalizedHtmlPath.replace(/\.html$/i, ".docx")); + candidates.add(normalizedHtmlPath.replace(/\.html$/i, ".long.png")); + } + let match; + while ((match = URL_ATTR_PATTERN3.exec(String(html ?? ""))) !== null) { + const ref = match[2]; + if (isRelativeDownloadReference(ref)) { + candidates.add(resolveWorkspaceRelativeFilePath(normalizedHtmlPath, ref)); + } + } + const artifacts = []; + for (const candidate of candidates) { + const stat = await statWorkspaceFile(publishDir, candidate); + if (stat) artifacts.push(stat); + } + return artifacts; +} +var PUBLIC_IMAGE_STORAGE_KEY_PATTERN2 = /^users\/([^/]+)\/images\/(\d{4}-\d{2}-\d{2}\/[^?#"'<>@\s]+)$/i; +function storageKeyToPublicStandardImageUrl(storageKey, publicBaseUrl) { + const match = String(storageKey ?? "").match(PUBLIC_IMAGE_STORAGE_KEY_PATTERN2); + if (!match) return null; + return buildPublicUrl(publicBaseUrl, match[1], `public/images/${match[2]}`); +} +function replaceImgproxyStandardImageReferences(html, publicBaseUrl) { + const source = String(html ?? ""); + if (!source) return source; + return source.replace( + /(?:https?:\/\/[^/"'<>?\s,)]+)?\/[^"'<>?\s,)]*plain\/local:\/\/\/(users\/[^"'<>?\s,)]*?\/images\/\d{4}-\d{2}-\d{2}\/[^"'<>?\s,@)]+)(?:@[a-z0-9]+)?(?:\?[^"'<>),\s]*)?/gi, + (value, storageKey) => storageKeyToPublicStandardImageUrl(storageKey, publicBaseUrl) ?? value + ); +} +function workspacePublicAssetRelativeUrl(htmlRelativePath, assetRelativePath) { + const htmlPath = String(htmlRelativePath ?? "").replace(/^\/+/, "") || "public/index.html"; + const assetPath = String(assetRelativePath ?? "").replace(/^\/+/, ""); + const htmlDir = path22.posix.dirname(htmlPath); + const relativePath = path22.posix.relative(htmlDir === "." ? "" : htmlDir, assetPath).replace(/\\/g, "/"); + return relativePath || path22.posix.basename(assetPath); +} +function rewriteWorkspacePublicAssetReferences(html, htmlRelativePath) { + const source = String(html ?? ""); + if (!source) return source; + return source.replace( + /public\/((?:images|\.tmp-images)\/[^"'<>@\s)]+(?:\?[^"'<>)\s]*)?)/gi, + (value, assetRelativePath) => workspacePublicAssetRelativeUrl(htmlRelativePath, `public/${assetRelativePath}`) + ); +} +async function localizePrivateImageReferences({ + pool: pool2, + userId, + html, + absoluteStoragePath: absoluteStoragePath2, + imgproxySigner = null +}) { + const source = String(html ?? ""); + const matches = [...source.matchAll(PRIVATE_ASSET_DOWNLOAD_URL_PATTERN)]; + if (matches.length === 0) return source; + const assetIds = [...new Set(matches.map((match) => match[1]).filter(Boolean))]; + if (assetIds.length === 0) return source; + const [assets] = await pool2.query( + `SELECT a.id, a.mime_type, v.storage_key + FROM h5_assets a + JOIN h5_asset_versions v ON v.id = a.current_version_id + WHERE a.user_id = ? AND a.id IN (?) AND a.mime_type LIKE 'image/%' + AND a.status <> 'deleted'`, + [userId, assetIds] + ); + const byId = new Map(assets.map((asset) => [asset.id, asset])); + const replacements = /* @__PURE__ */ new Map(); + for (const assetId of assetIds) { + const asset = byId.get(assetId); + if (!asset) continue; + if (imgproxySigner) { + replacements.set(assetId, imgproxySigner.buildUrl(imgproxySigner.baseUrl, asset.storage_key, "display")); + } else { + const mimeType = String(asset.mime_type || "application/octet-stream"); + const buffer = await fs18.readFile(absoluteStoragePath2(asset.storage_key)); + replacements.set(assetId, `data:${mimeType};base64,${buffer.toString("base64")}`); + } + } + if (replacements.size === 0) return source; + return source.replace(PRIVATE_ASSET_DOWNLOAD_URL_PATTERN, (value, assetId) => { + return replacements.get(assetId) ?? value; + }); +} +async function prepareHtmlPublishContent({ + pool: pool2, + userId, + html, + ownerSlug, + urlSlug, + htmlRelativePath = null, + pageTitle = null, + absoluteStoragePath: absoluteStoragePath2, + imgproxySigner = null, + h5Root = null +}) { + let publishContent = (await localizeGoogleFontsCss(html)).html; + publishContent = replaceImgproxyStandardImageReferences( + publishContent, + resolvePublicBaseUrl() + ); + publishContent = await localizePrivateImageReferences({ + pool: pool2, + userId, + html: publishContent, + absoluteStoragePath: absoluteStoragePath2, + imgproxySigner + }); + const publishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null; + const resolvedHtmlRelativePath = htmlRelativePath ?? inferWorkspaceHtmlRelativePath({ + pageTitle, + htmlContent: publishContent, + publishDir + }); + publishContent = rewriteWorkspacePublicAssetReferences(publishContent, resolvedHtmlRelativePath); + const linkIndex = await loadWorkspaceDownloadLinkIndex(pool2, userId); + publishContent = rewriteRelativeDownloadLinks(publishContent, { + htmlRelativePath: resolvedHtmlRelativePath, + publishDir, + linkIndex, + publishKey: userId, + publicBaseUrl: resolvePublicBaseUrl(), + preferAssetDownload: false + }).html; + return replacePrivateResourceReferences( + publishContent, + buildPublicationThumbnailFallback(ownerSlug, urlSlug) + ); +} +function createPublicationService(pool2, options = {}) { + const storageRoot = path22.resolve(options.storageRoot ?? path22.join(process.cwd(), "data", "mindspace")); + const h5Root = options.h5Root ?? null; + const idFactory = options.idFactory ?? (() => crypto13.randomUUID()); + const publicPageLimitFallback = Number(options.publicPageLimit ?? 5); + const conversationPackageRegistry = options.conversationPackageRegistry ?? null; + let imgproxySigner = null; + if (process.env.IMGPROXY_BASE_URL && process.env.IMGPROXY_SIGNING_KEY && process.env.IMGPROXY_SIGNING_SALT) { + try { + imgproxySigner = createImgproxySigner( + process.env.IMGPROXY_SIGNING_KEY, + process.env.IMGPROXY_SIGNING_SALT + ); + imgproxySigner.baseUrl = process.env.IMGPROXY_BASE_URL; + console.log("[Publication] imgproxy signer initialized"); + } catch (err) { + console.warn("[Publication] imgproxy signer init failed:", err instanceof Error ? err.message : err); + } + } + const resolvePublicPageLimit = async () => { + try { + const config = await loadMindSpaceConfig(pool2); + return Number(config.publicPageLimit ?? publicPageLimitFallback); + } catch { + return publicPageLimitFallback; + } + }; + const absoluteStoragePath2 = (storageKey) => { + const resolved = path22.resolve(storageRoot, storageKey); + if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path22.sep}`)) { + throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C"); + } + return resolved; + }; + const resolveStoragePathCandidates = (storageKey) => { + const normalized = String(storageKey ?? "").replace(/\\/g, "/"); + const withoutMd = normalized.endsWith(".md") ? normalized.slice(0, -3) : normalized; + const match = withoutMd.match(/^users\/([^/]+)\/(assets|pages)\/([^/]+)\/(v\d+)$/); + if (!match) return [normalized]; + const [, userId, scope, entityId, versionTag] = match; + return [ + normalized, + `users/${userId}/${scope}/${entityId}/versions/${versionTag}.md`, + `users/${userId}/${scope}/${entityId}/versions/${versionTag}` + ].filter((candidate, index, list) => list.indexOf(candidate) === index); + }; + const resolveReadableStoragePath = async (storageKey) => { + let lastError = null; + for (const candidate of resolveStoragePathCandidates(storageKey)) { + const absolutePath = absoluteStoragePath2(candidate); + try { + await fs18.stat(absolutePath); + return absolutePath; + } catch (error) { + if (error?.code === "ENOENT") { + lastError = error; + continue; + } + throw error; + } + } + throw lastError ?? Object.assign(new Error("\u5B58\u50A8\u6587\u4EF6\u4E0D\u5B58\u5728"), { code: "storage_not_found" }); + }; + const loadVersion = async (userId, pageId, pageVersionId) => { + const [rows] = await pool2.query( + `SELECT p.id AS page_id, p.title, p.summary, p.page_type, p.template_id, p.current_version_id, + p.source_session_id, p.source_message_id, + p.user_id, p.space_id, pv.id AS page_version_id, pv.version_no, + pv.bundle_asset_id, av.storage_key, + JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path + FROM h5_page_records p + JOIN h5_page_versions pv ON pv.page_id = p.id + JOIN h5_asset_versions av ON av.asset_id = pv.content_asset_id AND av.version_no = 1 + WHERE p.id = ? AND p.user_id = ? AND pv.id = ? AND p.status <> 'deleted' + LIMIT 1`, + [pageId, userId, pageVersionId] + ); + const row = rows[0]; + if (!row) throw publicationError("\u9875\u9762\u7248\u672C\u4E0D\u5B58\u5728", "page_not_found"); + return { + ...row, + version_no: Number(row.version_no), + content: await fs18.readFile(await resolveReadableStoragePath(row.storage_key), "utf8") + }; + }; + const loadOwnerSlug = async (userId) => { + const [rows] = await pool2.query( + `SELECT COALESCE(slug, username) AS public_slug + FROM h5_users + WHERE id = ? + LIMIT 1`, + [userId] + ); + const ownerSlug = String(rows[0]?.public_slug ?? "").trim(); + if (!ownerSlug) throw publicationError("\u7528\u6237\u516C\u5F00\u5730\u5740\u4E0D\u5B58\u5728", "publication_owner_not_found"); + return ownerSlug; + }; + const preparePublishContent = async (page, ownerSlug, urlSlug) => { + let publishContent = page.content; + if (page.page_type !== "html") return publishContent; + const publishDir = h5Root ? resolvePublishDir(h5Root, { id: page.user_id }) : null; + return prepareHtmlPublishContent({ + pool: pool2, + userId: page.user_id, + html: publishContent, + ownerSlug, + urlSlug, + pageTitle: page.title, + htmlRelativePath: inferWorkspaceHtmlRelativePath({ + snapshotRelativePath: page.source_relative_path, + pageTitle: page.title, + htmlContent: publishContent, + publishDir + }), + absoluteStoragePath: absoluteStoragePath2, + h5Root, + imgproxySigner: imgproxySigner ? { buildUrl: (path35, preset) => imgproxySigner.buildUrl(imgproxySigner.baseUrl, path35, preset) } : null + }); + }; + const persistScan = async (conn, userId, pageVersionId, scan, now) => { + const scanId = idFactory(); + await conn.query( + `INSERT INTO h5_security_scans + (id, user_id, target_type, target_id, scanner_version, status, risk_level, + findings_count, summary_json, started_at, completed_at) + VALUES (?, ?, 'page_version', ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + scanId, + userId, + pageVersionId, + SCANNER_VERSION, + scan.status, + scan.riskLevel, + scan.findings.length, + JSON.stringify({ types: scan.findings.map((finding) => finding.type) }), + now, + now + ] + ); + for (const finding of scan.findings) { + await conn.query( + `INSERT INTO h5_security_findings + (id, scan_id, finding_type, risk_level, occurrence_count, sample_masked, blocking, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [ + idFactory(), + scanId, + finding.type, + finding.riskLevel, + finding.occurrenceCount, + finding.sampleMasked, + finding.blocking ? 1 : 0, + now + ] + ); + } + return scanId; + }; + const check = async (userId, pageId, input) => { + const slug = normalizeSlug(input.urlSlug); + const accessMode = normalizeAccessMode(input.accessMode); + normalizePassword(input.password, accessMode === "password"); + const expiresAt = normalizeExpiresAt( + input.expiresAt, + accessMode === "time_limited" + ); + const page = await loadVersion(userId, pageId, input.pageVersionId); + if (page.page_version_id !== page.current_version_id) { + throw publicationError("\u53EA\u80FD\u53D1\u5E03\u9875\u9762\u7684\u5F53\u524D\u7248\u672C", "invalid_state_transition"); + } + const [conflicts] = await pool2.query( + `SELECT pr.id, pr.page_id, p.title + FROM h5_publish_records pr + JOIN h5_page_records p ON p.id = pr.page_id + WHERE pr.user_id = ? AND pr.url_slug = ? AND pr.status = 'online' + AND pr.page_id <> ? LIMIT 1`, + [userId, slug, pageId] + ); + const conflict = conflicts[0] ?? null; + const ownerSlug = await loadOwnerSlug(userId); + const publishContent = await preparePublishContent(page, ownerSlug, slug); + const scan = scanContent(`${page.title} +${page.summary ?? ""} +${publishContent}`, { + format: page.page_type === "html" ? "html" : "text", + allowHtmlActiveContent: page.page_type === "html" + }); + const suggestedUrlSlug = conflict && accessMode !== "private_link" ? await findAvailableSlug(pool2, userId, slug, pageId) : null; + return { + pageVersionId: page.page_version_id, + urlSlug: slug, + accessMode, + expiresAt, + slugAvailable: accessMode === "private_link" || !conflict, + slugConflict: conflict ? { pageId: conflict.page_id, title: conflict.title } : null, + suggestedUrlSlug, + ...scan, + allowed: (accessMode === "private_link" || !conflict) && scan.allowed + }; + }; + const publish = async (userId, pageId, input) => { + const result = await check(userId, pageId, input); + if (!result.slugAvailable) throw publicationError("\u9875\u9762\u5730\u5740\u5DF2\u88AB\u4F7F\u7528", "slug_conflict"); + if (!result.allowed) { + throw publicationError("\u9875\u9762\u5305\u542B\u4E0D\u53EF\u516C\u5F00\u7684\u9AD8\u98CE\u9669\u4FE1\u606F", "security_risk_blocked", { + findings: result.findings + }); + } + const acknowledged = new Set(input.acknowledgedFindingIds ?? []); + const missingAcknowledgements = result.findings.filter( + (finding) => !finding.blocking && !acknowledged.has(finding.id) + ); + if (missingAcknowledgements.length) { + throw publicationError("\u8BF7\u5148\u786E\u8BA4\u53D1\u5E03\u68C0\u67E5\u4E2D\u7684\u98CE\u9669\u63D0\u793A", "security_ack_required", { + findings: missingAcknowledgements + }); + } + const page = await loadVersion(userId, pageId, result.pageVersionId); + const ownerSlug = await loadOwnerSlug(userId); + const publishContent = await preparePublishContent(page, ownerSlug, result.urlSlug); + const html = pageInternals.renderPublicationHtml({ ...page, content: publishContent }); + const htmlBytes = Buffer.byteLength(html); + const conn = await pool2.getConnection(); + let writtenPath; + try { + await conn.beginTransaction(); + const [spaces] = await conn.query( + `SELECT quota_bytes, used_bytes, reserved_bytes FROM h5_user_spaces + WHERE id = ? AND user_id = ? FOR UPDATE`, + [page.space_id, userId] + ); + const available = Number(spaces[0]?.quota_bytes ?? 0) - Number(spaces[0]?.used_bytes ?? 0) - Number(spaces[0]?.reserved_bytes ?? 0); + if (available < htmlBytes) { + throw publicationError("\u5269\u4F59\u7A7A\u95F4\u4E0D\u8DB3", "quota_exceeded", { + requiredBytes: htmlBytes, + availableBytes: Math.max(0, available) + }); + } + const [publicationUsage] = await conn.query( + `SELECT COUNT(DISTINCT page_id) AS public_page_used, + MAX(CASE WHEN page_id = ? THEN 1 ELSE 0 END) AS page_already_online + FROM h5_publish_records + WHERE user_id = ? AND status = 'online'`, + [pageId, userId] + ); + const publicPageLimit = await resolvePublicPageLimit(); + if (!Number(publicationUsage[0]?.page_already_online) && Number(publicationUsage[0]?.public_page_used) >= publicPageLimit) { + throw publicationError("\u516C\u5F00\u9875\u9762\u6570\u91CF\u5DF2\u8FBE\u5230\u5957\u9910\u4E0A\u9650", "public_page_limit_exceeded", { + limit: publicPageLimit + }); + } + const [categories] = await conn.query( + `SELECT id FROM h5_space_categories + WHERE user_id = ? AND space_id = ? AND category_code = 'public' LIMIT 1`, + [userId, page.space_id] + ); + if (!categories[0]) throw publicationError("\u516C\u5F00\u5206\u7C7B\u4E0D\u5B58\u5728", "category_not_found"); + const now = Date.now(); + const scanId = await persistScan(conn, userId, page.page_version_id, result, now); + const bundleAssetId = idFactory(); + const assetVersionId = idFactory(); + const publishId = idFactory(); + const privateToken = result.accessMode === "private_link" ? crypto13.randomBytes(24).toString("base64url") : null; + const tokenHash = privateToken ? crypto13.createHash("sha256").update(privateToken).digest("hex") : null; + const passwordHash = result.accessMode === "password" ? hashPassword(input.password) : null; + const storageKey = path22.posix.join( + "users", + userId, + "publications", + publishId, + "index.html" + ); + writtenPath = absoluteStoragePath2(storageKey); + await fs18.mkdir(path22.dirname(writtenPath), { recursive: true }); + await fs18.writeFile(writtenPath, html, { flag: "wx" }); + const checksum = crypto13.createHash("sha256").update(html).digest("hex"); + const publicBaseUrl = resolvePublicBaseUrl(); + const publicUrl2 = privateToken ? `/s/${privateToken}` : `${publicBaseUrl}/u/${encodeURIComponent(ownerSlug)}/pages/${result.urlSlug}`; + await conn.query( + `INSERT INTO h5_assets + (id, user_id, space_id, category_id, asset_type, mime_type, original_filename, + display_name, current_version_id, size_bytes, checksum, risk_level, visibility, + status, source_type, created_at, updated_at) + VALUES (?, ?, ?, ?, 'html', 'text/html', 'index.html', ?, ?, ?, ?, ?, + 'public_candidate', 'ready', 'generated', ?, ?)`, + [ + bundleAssetId, + userId, + page.space_id, + categories[0].id, + `${page.title} \xB7 \u53D1\u5E03\u5FEB\u7167`, + assetVersionId, + htmlBytes, + checksum, + result.riskLevel, + now, + now + ] + ); + await conn.query( + `INSERT INTO h5_asset_versions + (id, asset_id, version_no, storage_key, size_bytes, checksum, mime_type, + created_by, change_note, scan_status, created_at) + VALUES (?, ?, 1, ?, ?, ?, 'text/html', ?, '\u53D1\u5E03\u9875\u9762\u5FEB\u7167', ?, ?)`, + [ + assetVersionId, + bundleAssetId, + storageKey, + htmlBytes, + checksum, + userId, + result.status === "passed" ? "passed" : "warned", + now + ] + ); + const [previousRows] = await conn.query( + `SELECT id, page_version_id FROM h5_publish_records + WHERE user_id = ? AND page_id = ? + ORDER BY published_at DESC LIMIT 1 FOR UPDATE`, + [userId, pageId] + ); + await conn.query( + `UPDATE h5_publish_records SET status = 'offline', offline_at = ?, updated_at = ? + WHERE user_id = ? AND page_id = ? AND status = 'online'`, + [now, now, userId, pageId] + ); + await conn.query( + `UPDATE h5_page_versions + SET immutable = 1, bundle_asset_id = ?, security_scan_id = ? + WHERE id = ? AND page_id = ?`, + [bundleAssetId, scanId, page.page_version_id, pageId] + ); + await conn.query( + `INSERT INTO h5_publish_records + (id, user_id, page_id, page_version_id, publish_type, url_slug, public_url, + access_mode, password_hash, token_hash, token_prefix, expires_at, user_confirmed_at, published_at, + status, view_count, security_scan_id, created_at, updated_at) + VALUES (?, ?, ?, ?, 'page', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'online', 0, ?, ?, ?)`, + [ + publishId, + userId, + pageId, + page.page_version_id, + result.urlSlug, + publicUrl2, + result.accessMode, + passwordHash, + tokenHash, + privateToken?.slice(0, 8) ?? null, + result.expiresAt, + null, + now, + scanId, + now, + now + ] + ); + await conn.query( + `INSERT INTO h5_publication_events + (id, publish_id, event_type, actor_id, old_page_version_id, new_page_version_id, + access_mode, detail_json, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + idFactory(), + publishId, + previousRows.length ? "republished" : "published", + userId, + previousRows[0]?.page_version_id ?? null, + page.page_version_id, + result.accessMode, + JSON.stringify({ findings: result.findings.map((finding) => finding.id) }), + now + ] + ); + await conn.query( + `UPDATE h5_page_records + SET current_publish_id = ?, status = ?, visibility = ?, updated_at = ? + WHERE id = ? AND user_id = ?`, + [ + publishId, + result.accessMode === "public" ? "published" : "protected", + result.accessMode === "public" ? "public" : "private", + now, + pageId, + userId + ] + ); + await conn.query( + `UPDATE h5_user_spaces SET used_bytes = used_bytes + ?, updated_at = ? + WHERE id = ? AND user_id = ?`, + [htmlBytes, now, page.space_id, userId] + ); + const assetUrlPattern = /\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)/gi; + const assetIds = /* @__PURE__ */ new Set(); + let match; + while ((match = assetUrlPattern.exec(html)) !== null) { + assetIds.add(match[1]); + } + if (assetIds.size > 0) { + const refValues = Array.from(assetIds).map((assetId) => [publishId, assetId, now]); + await conn.query( + `INSERT IGNORE INTO h5_publication_asset_refs + (publication_id, asset_id, created_at) VALUES ` + refValues.map(() => "(?, ?, ?)").join(","), + refValues.flat() + ); + } + await conn.commit(); + const publication = { + ...publicationResponse({ + id: publishId, + page_id: pageId, + page_version_id: page.page_version_id, + url_slug: result.urlSlug, + public_url: publicUrl2, + access_mode: result.accessMode, + status: "online", + view_count: 0, + published_at: now, + offline_at: null, + expires_at: result.expiresAt + }), + findings: result.findings + }; + await registerPublicationArtifactForConversation({ + registry: conversationPackageRegistry, + userId, + page, + publication, + bundleAssetId, + storageKey, + htmlBytes, + html: page.content, + publishDir: h5Root ? resolvePublishDir(h5Root, { id: userId }) : null, + now + }); + return publication; + } catch (error) { + await conn.rollback(); + if (writtenPath) await fs18.rm(writtenPath, { force: true }).catch(() => { + }); + throw error; + } finally { + conn.release(); + } + }; + const getCurrent = async (userId, pageId) => { + const [rows] = await pool2.query( + `SELECT pr.* FROM h5_publish_records pr + JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id + WHERE pr.page_id = ? AND pr.user_id = ? AND pr.status = 'online' + ORDER BY pr.published_at DESC LIMIT 1`, + [pageId, userId] + ); + return publicationResponse(rows[0]); + }; + const updatePublicationStatus = async (userId, publicationId, { accessMode, expiresAt }) => { + const conn = await pool2.getConnection(); + try { + await conn.beginTransaction(); + const [rows] = await conn.query( + `SELECT * FROM h5_publish_records + WHERE id = ? AND user_id = ? AND status = 'online' LIMIT 1 FOR UPDATE`, + [publicationId, userId] + ); + const publication = rows[0]; + if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); + const normalizedMode = normalizeAccessMode(accessMode); + const normalizedExpiresAt = normalizeExpiresAt(expiresAt, normalizedMode === "time_limited"); + const now = Date.now(); + await conn.query( + `UPDATE h5_publish_records + SET access_mode = ?, expires_at = ?, user_confirmed_at = ?, updated_at = ? + WHERE id = ? AND user_id = ?`, + [normalizedMode, normalizedExpiresAt, now, now, publicationId, userId] + ); + await conn.commit(); + const updated = { + ...publication, + access_mode: normalizedMode, + expires_at: normalizedExpiresAt, + user_confirmed_at: now, + updated_at: now + }; + return publicationResponse(updated); + } catch (error) { + await conn.rollback(); + throw error; + } finally { + conn.release(); + } + }; + const offline = async (userId, publicationId) => { + const conn = await pool2.getConnection(); + try { + await conn.beginTransaction(); + const [rows] = await conn.query( + `SELECT * FROM h5_publish_records + WHERE id = ? AND user_id = ? AND status = 'online' LIMIT 1 FOR UPDATE`, + [publicationId, userId] + ); + const publication = rows[0]; + if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); + const now = Date.now(); + await conn.query( + `UPDATE h5_publish_records SET status = 'offline', offline_at = ?, updated_at = ? + WHERE id = ?`, + [now, now, publicationId] + ); + await conn.query( + `UPDATE h5_page_records + SET current_publish_id = NULL, status = 'offline', visibility = 'private', updated_at = ? + WHERE id = ? AND user_id = ? AND current_publish_id = ?`, + [now, publication.page_id, userId, publicationId] + ); + await conn.query( + `INSERT INTO h5_publication_events + (id, publish_id, event_type, actor_id, old_page_version_id, new_page_version_id, + access_mode, detail_json, created_at) + VALUES (?, ?, 'offlined', ?, ?, NULL, ?, '{}', ?)`, + [ + idFactory(), + publicationId, + userId, + publication.page_version_id, + publication.access_mode, + now + ] + ); + await conn.query( + `UPDATE plaza_posts SET status = 'hidden', updated_at = ? + WHERE publication_id = ? AND status != 'hidden'`, + [now, publicationId] + ); + await conn.commit(); + return { id: publicationId, status: "offline", offlineAt: now }; + } catch (error) { + await conn.rollback(); + throw error; + } finally { + conn.release(); + } + }; + const resolveRow = async (row, viewerId, password, requestMeta = {}) => { + if (!row) throw publicationError("\u516C\u5F00\u9875\u9762\u4E0D\u5B58\u5728", "publication_not_found"); + if (row.expires_at && Number(row.expires_at) <= Date.now()) { + await pool2.query( + `UPDATE h5_publish_records SET status = 'expired', updated_at = ? WHERE id = ?`, + [Date.now(), row.id] + ); + throw publicationError("\u516C\u5F00\u9875\u9762\u5DF2\u8FC7\u671F", "publication_not_found"); + } + if (row.access_mode === "owner_only" && row.owner_id !== viewerId) { + throw publicationError("\u516C\u5F00\u9875\u9762\u4E0D\u5B58\u5728", "publication_not_found"); + } + if (row.access_mode === "login_required" && !viewerId) { + throw publicationError("\u767B\u5F55\u540E\u624D\u80FD\u8BBF\u95EE\u6B64\u9875\u9762", "publication_login_required"); + } + if (row.access_mode === "password" && !verifyPassword2(password, row.password_hash)) { + throw publicationError("\u9700\u8981\u8BBF\u95EE\u5BC6\u7801", "publication_password_required"); + } + const now = Date.now(); + await pool2.query( + `UPDATE h5_publish_records SET view_count = view_count + 1, updated_at = ? WHERE id = ?`, + [now, row.id] + ); + await pool2.query( + `INSERT INTO h5_publication_views + (id, publish_id, viewer_user_id, referrer_host, device_type, viewed_at) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + idFactory(), + row.id, + viewerId ?? null, + referrerHost(requestMeta.referrer), + deviceType(requestMeta.userAgent), + now + ] + ); + const html = await fs18.readFile(await resolveReadableStoragePath(row.storage_key), "utf8"); + return { + html: await refreshPublishedHtmlDownloadLinks(row, html), + publication: publicationResponse({ ...row, view_count: Number(row.view_count) + 1 }), + ownerId: row.owner_id, + pageSource: { + pageId: row.page_id, + title: row.title, + sourceSessionId: row.source_session_id ?? null, + sourceMessageId: row.source_message_id ?? null + } + }; + }; + const refreshPublishedHtmlDownloadLinks = async (row, html) => { + if (!h5Root || !row?.owner_id) return html; + const source = String(html ?? ""); + if (!source.trim()) return html; + const publishDir = resolvePublishDir(h5Root, { id: row.owner_id }); + const linkIndex = await loadWorkspaceDownloadLinkIndex(pool2, row.owner_id); + return rewriteRelativeDownloadLinks(html, { + htmlRelativePath: inferWorkspaceHtmlRelativePath({ + pageTitle: row.title, + htmlContent: html, + publishDir + }), + publishDir, + linkIndex, + publishKey: row.owner_id, + publicBaseUrl: resolvePublicBaseUrl(), + preferAssetDownload: false + }).html; + }; + const resolvePublic = async (ownerSlug, urlSlug, viewerId, password, requestMeta) => { + const [rows] = await pool2.query( + `SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id, av.storage_key + FROM h5_publish_records pr + JOIN h5_users u ON u.id = pr.user_id + JOIN h5_page_records p ON p.id = pr.page_id + JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.immutable = 1 + JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1 + WHERE COALESCE(u.slug, u.username) = ? AND pr.url_slug = ? AND pr.status = 'online' + AND pr.access_mode <> 'private_link' + ORDER BY pr.published_at DESC LIMIT 1`, + [ownerSlug, urlSlug] + ); + return resolveRow(rows[0], viewerId, password, requestMeta); + }; + const resolvePrivateLink = async (token, viewerId, requestMeta) => { + const tokenHash = crypto13.createHash("sha256").update(String(token)).digest("hex"); + const [rows] = await pool2.query( + `SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id, av.storage_key + FROM h5_publish_records pr + JOIN h5_users u ON u.id = pr.user_id + JOIN h5_page_records p ON p.id = pr.page_id + JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.immutable = 1 + JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1 + WHERE pr.token_hash = ? AND pr.status = 'online' AND pr.access_mode = 'private_link' + LIMIT 1`, + [tokenHash] + ); + return resolveRow(rows[0], viewerId, null, requestMeta); + }; + const getStats = async (userId, publicationId) => { + const [publications] = await pool2.query( + `SELECT id, view_count, published_at, status + FROM h5_publish_records WHERE id = ? AND user_id = ? LIMIT 1`, + [publicationId, userId] + ); + const publication = publications[0]; + if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); + const since = Date.now() - 30 * 24 * 60 * 60 * 1e3; + const [daily] = await pool2.query( + `SELECT DATE_FORMAT(FROM_UNIXTIME(viewed_at / 1000), '%Y-%m-%d') AS day, + COUNT(*) AS views + FROM h5_publication_views + WHERE publish_id = ? AND viewed_at >= ? + GROUP BY day ORDER BY day`, + [publicationId, since] + ); + const [devices] = await pool2.query( + `SELECT device_type, COUNT(*) AS views + FROM h5_publication_views WHERE publish_id = ? + GROUP BY device_type ORDER BY views DESC`, + [publicationId] + ); + const [sources] = await pool2.query( + `SELECT COALESCE(referrer_host, 'direct') AS source, COUNT(*) AS views + FROM h5_publication_views WHERE publish_id = ? + GROUP BY source ORDER BY views DESC LIMIT 10`, + [publicationId] + ); + return { + publicationId, + status: publication.status, + totalViews: Number(publication.view_count), + publishedAt: Number(publication.published_at), + daily: daily.map((row) => ({ day: row.day, views: Number(row.views) })), + devices: devices.map((row) => ({ + deviceType: row.device_type, + views: Number(row.views) + })), + sources: sources.map((row) => ({ source: row.source, views: Number(row.views) })) + }; + }; + const getPublicHomepage = async (ownerSlug) => { + const [owners] = await pool2.query( + `SELECT id, username, COALESCE(slug, username) AS slug, display_name + FROM h5_users + WHERE COALESCE(slug, username) = ? + LIMIT 1`, + [ownerSlug] + ); + const owner = owners[0]; + if (!owner) throw publicationError("\u516C\u5F00\u4E3B\u9875\u4E0D\u5B58\u5728", "publication_not_found"); + const [rows] = await pool2.query( + `SELECT pr.id, pr.page_id, pr.url_slug, pr.public_url, pr.view_count, pr.published_at, + p.title, p.summary, p.template_id + FROM h5_publish_records pr + JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id + WHERE pr.user_id = ? AND pr.status = 'online' AND pr.access_mode = 'public' + ORDER BY pr.published_at DESC + LIMIT 48`, + [owner.id] + ); + return publicHomepageResponse(owner, rows); + }; + const cleanupExpiredUnconfirmedPublications = async (now = Date.now()) => { + const [result] = await pool2.query( + `UPDATE h5_publish_records + SET access_mode = 'private', expires_at = NULL, updated_at = ? + WHERE access_mode = 'public' + AND expires_at IS NOT NULL + AND expires_at <= ? + AND user_confirmed_at IS NULL + AND status = 'online'`, + [now, now] + ); + return { cleaned: result.affectedRows }; + }; + return { + check, + publish, + getCurrent, + getPublicHomepage, + getStats, + offline, + updatePublicationStatus, + resolvePublic, + resolvePrivateLink, + cleanupExpiredUnconfirmedPublications + }; +} +async function registerPublicationArtifactForConversation({ + registry, + userId, + page, + publication, + bundleAssetId, + storageKey, + htmlBytes, + html, + publishDir, + now +}) { + const sessionId = String(page?.source_session_id ?? "").trim(); + if (!registry || !sessionId || !publication?.id) return null; + try { + const packageRecord = await registry.ensurePackage({ + userId, + sessionId, + title: page.title, + now + }); + const artifact = await registry.recordArtifact({ + id: `ca_${publication.id}`, + packageId: packageRecord.id, + artifactKind: "public_html", + role: "assistant", + assetId: bundleAssetId, + pageId: page.page_id, + publicationId: publication.id, + messageId: page.source_message_id ?? null, + displayName: `${page.title}.html`, + mimeType: "text/html", + sizeBytes: htmlBytes, + storageKey, + canonicalUrl: publication.publicUrl, + sortOrder: Number.isFinite(Number(publication.publishedAt)) ? Number(publication.publishedAt) : 0, + now + }); + const htmlRelativePath = page.source_relative_path ?? null; + const companionArtifacts = await collectPublicationCompanionArtifacts({ + publishDir, + html, + htmlRelativePath + }); + await Promise.all( + companionArtifacts.map( + (companion, index) => registry.recordArtifact({ + id: `ca_${publication.id}_${crypto13.createHash("sha256").update(companion.workspacePath).digest("hex").slice(0, 12)}`, + packageId: packageRecord.id, + artifactKind: companionKindForWorkspacePath(companion.workspacePath), + role: "assistant", + pageId: page.page_id, + publicationId: publication.id, + messageId: page.source_message_id ?? null, + displayName: path22.posix.basename(companion.workspacePath), + mimeType: companionMimeForWorkspacePath(companion.workspacePath), + sizeBytes: companion.sizeBytes, + canonicalUrl: buildPublicUrl(resolvePublicBaseUrl(), userId, companion.workspacePath), + sortOrder: (Number.isFinite(Number(publication.publishedAt)) ? Number(publication.publishedAt) : 0) + index + 1, + now + }) + ) + ); + await registry.writeManifestForSession({ userId, sessionId }); + return artifact; + } catch (error) { + console.warn( + "[MindSpace] conversation package publication artifact registration failed:", + error instanceof Error ? error.message : error + ); + return null; + } +} +var publicationInternals = { + normalizeSlug, + normalizeAccessMode, + normalizePassword, + normalizeExpiresAt, + hashPassword, + verifyPassword: verifyPassword2, + deviceType, + referrerHost, + publicHomepageResponse, + buildPublicationThumbnailFallback, + replaceImgproxyStandardImageReferences, + workspacePublicAssetRelativeUrl, + rewriteWorkspacePublicAssetReferences, + localizePrivateImageReferences, + collectPublicationCompanionArtifacts, + prepareHtmlPublishContent, + registerPublicationArtifactForConversation, + scanContent + // re-exported from mindspace-content-scan.mjs +}; + +// mindspace-conversation-package-backfill.mjs +var BACKFILL_LIMIT = 200; +function asNumber5(value) { + const number = Number(value); + return Number.isFinite(number) ? number : null; +} +async function readUtf8IfExists(storageRoot, storageKey) { + if (!storageRoot || !storageKey) return ""; + try { + return await fs19.readFile(path23.resolve(storageRoot, storageKey), "utf8"); + } catch { + return ""; + } +} +async function backfillConversationPackageArtifacts({ + pool: pool2, + registry, + storageRoot, + h5Root, + user, + sessionId, + now = Date.now() +} = {}) { + const userId = String(user?.id ?? "").trim(); + const normalizedSessionId = String(sessionId ?? "").trim(); + if (!pool2 || !registry || !userId || !normalizedSessionId) { + return { pages: 0, publications: 0, uploads: 0 }; + } + let pages = 0; + const [pageRows] = await pool2.query( + `SELECT p.id, p.title, p.source_message_id, p.current_version_id, + pv.version_no, pv.content_asset_id, + av.mime_type, av.size_bytes, av.storage_key + FROM h5_page_records p + LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id AND pv.page_id = p.id + LEFT JOIN h5_asset_versions av ON av.asset_id = pv.content_asset_id AND av.version_no = 1 + WHERE p.user_id = ? AND p.source_session_id = ? AND p.status <> 'deleted' + ORDER BY p.updated_at DESC + LIMIT ?`, + [userId, normalizedSessionId, BACKFILL_LIMIT] + ); + for (const row of pageRows) { + const artifact = await pageInternals.registerPageArtifactForConversation({ + registry, + userId, + source: { sessionId: normalizedSessionId, messageId: row.source_message_id ?? null }, + page: { + id: row.id, + title: row.title, + currentVersionId: row.current_version_id, + sourceMessageId: row.source_message_id ?? null + }, + contentAssetId: row.content_asset_id ?? null, + contentMimeType: row.mime_type ?? null, + contentBytes: asNumber5(row.size_bytes), + storageKey: row.storage_key ?? null, + versionNo: row.version_no, + now + }); + if (artifact) pages += 1; + } + let publications = 0; + const [publicationRows] = await pool2.query( + `SELECT pr.id, pr.page_id, pr.public_url, pr.published_at, + p.title, p.source_message_id, + pv.bundle_asset_id, + JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path, + av.storage_key, av.size_bytes + FROM h5_publish_records pr + JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id + JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.page_id = p.id + LEFT JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1 + WHERE pr.user_id = ? AND p.source_session_id = ? AND pr.status IN ('online', 'offline') + ORDER BY pr.published_at DESC + LIMIT ?`, + [userId, normalizedSessionId, BACKFILL_LIMIT] + ); + const publishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null; + for (const row of publicationRows) { + const artifact = await publicationInternals.registerPublicationArtifactForConversation({ + registry, + userId, + page: { + page_id: row.page_id, + title: row.title, + source_session_id: normalizedSessionId, + source_message_id: row.source_message_id ?? null, + source_relative_path: row.source_relative_path ?? null + }, + publication: { + id: row.id, + publicUrl: row.public_url, + publishedAt: row.published_at + }, + bundleAssetId: row.bundle_asset_id ?? null, + storageKey: row.storage_key ?? null, + htmlBytes: asNumber5(row.size_bytes), + html: await readUtf8IfExists(storageRoot, row.storage_key), + publishDir, + now + }); + if (artifact) publications += 1; + } + let uploads = 0; + const [uploadRows] = await pool2.query( + `SELECT u.id, u.filename, u.source_message_id, u.completed_asset_id, + u.detected_mime_type, u.actual_size, + a.mime_type, a.original_filename, a.display_name, a.size_bytes, + v.storage_key + FROM h5_upload_sessions u + JOIN h5_assets a ON a.id = u.completed_asset_id AND a.user_id = u.user_id + LEFT JOIN h5_asset_versions v ON v.id = a.current_version_id AND v.asset_id = a.id + WHERE u.user_id = ? AND u.source_session_id = ? AND u.status = 'completed' + ORDER BY COALESCE(u.completed_at, u.created_at) DESC + LIMIT ?`, + [userId, normalizedSessionId, BACKFILL_LIMIT] + ); + for (const row of uploadRows) { + const artifact = await assetInternals.registerUploadArtifactForConversation({ + registry, + userId, + upload: { + filename: row.filename, + source_session_id: normalizedSessionId, + source_message_id: row.source_message_id ?? null, + detected_mime_type: row.detected_mime_type ?? row.mime_type ?? null, + actual_size: row.actual_size ?? row.size_bytes ?? null + }, + asset: { + id: row.completed_asset_id, + mimeType: row.mime_type ?? row.detected_mime_type ?? null, + filename: row.original_filename ?? row.filename, + displayName: row.display_name ?? row.filename, + sizeBytes: asNumber5(row.size_bytes ?? row.actual_size) + }, + assetId: row.completed_asset_id, + storageKey: row.storage_key ?? null, + canonicalUrl: `/api/mindspace/v1/assets/${encodeURIComponent(row.completed_asset_id)}/download`, + now + }); + if (artifact) uploads += 1; + } + return { pages, publications, uploads }; +} + +// mindspace-conversation-package.mjs +var PACKAGE_ID_PREFIX = "cp_"; +var ARTIFACT_ID_PREFIX = "ca_"; +var CONVERSATION_PACKAGE_SCHEMA_VERSION = 1; +var CONVERSATION_ARTIFACT_KINDS = Object.freeze([ + "input_image", + "input_file", + "generated_image", + "generated_file", + "page", + "public_html", + "long_image", + "thumbnail", + "docx", + "pdf" +]); +var MYSQL_SIGNED_INT_MAX = 2147483647; +var MYSQL_SIGNED_INT_MIN = -2147483648; +function requiredString(value, field) { + const normalized = String(value ?? "").trim(); + if (!normalized) { + throw Object.assign(new Error(`${field} is required`), { + code: "invalid_conversation_package", + field + }); + } + return normalized; +} +function optionalString(value) { + const normalized = String(value ?? "").trim(); + return normalized || null; +} +function optionalNumber(value) { + if (value == null || value === "") return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} +function safeSortOrder(value) { + const number = Number(value); + if (!Number.isFinite(number)) return 0; + let normalized = Math.trunc(number); + if (normalized > MYSQL_SIGNED_INT_MAX && Math.abs(normalized) >= 1e12) { + normalized = Math.trunc(normalized / 1e3); + } + if (normalized > MYSQL_SIGNED_INT_MAX) return MYSQL_SIGNED_INT_MAX; + if (normalized < MYSQL_SIGNED_INT_MIN) return MYSQL_SIGNED_INT_MIN; + return normalized; +} +function safeStorageSegment(value, field) { + const normalized = requiredString(value, field); + const encoded = encodeURIComponent(normalized); + if (encoded.includes("%2F") || encoded.includes("%5C")) { + throw Object.assign(new Error(`${field} must not contain path separators`), { + code: "invalid_storage_segment", + field + }); + } + return encoded; +} +function buildConversationStoragePrefix({ userId, sessionId }) { + return `users/${safeStorageSegment(userId, "userId")}/conversations/${safeStorageSegment( + sessionId, + "sessionId" + )}`; +} +function buildConversationPackageUri({ userId, sessionId }) { + return `mindspace://users/${safeStorageSegment(userId, "userId")}/conversations/${safeStorageSegment( + sessionId, + "sessionId" + )}`; +} +function createConversationPackageRecord(input) { + const userId = requiredString(input?.userId, "userId"); + const sessionId = requiredString(input?.sessionId, "sessionId"); + const now = Number.isFinite(Number(input?.now)) ? Number(input.now) : Date.now(); + const id = optionalString(input?.id) ?? `${PACKAGE_ID_PREFIX}${sessionId}`; + const storagePrefix = optionalString(input?.storagePrefix) ?? buildConversationStoragePrefix({ userId, sessionId }); + return { + id, + userId, + sessionId, + title: optionalString(input?.title), + status: optionalString(input?.status) ?? "active", + storagePrefix, + manifestAssetId: optionalString(input?.manifestAssetId), + createdAt: Number.isFinite(Number(input?.createdAt)) ? Number(input.createdAt) : now, + updatedAt: Number.isFinite(Number(input?.updatedAt)) ? Number(input.updatedAt) : now + }; +} +function assertConversationArtifactKind(kind) { + const normalized = requiredString(kind, "artifactKind"); + if (!CONVERSATION_ARTIFACT_KINDS.includes(normalized)) { + throw Object.assign(new Error(`unsupported conversation artifact kind: ${normalized}`), { + code: "unsupported_conversation_artifact_kind", + field: "artifactKind" + }); + } + return normalized; +} +function createConversationArtifact(input) { + const packageId = requiredString(input?.packageId, "packageId"); + const artifactKind = assertConversationArtifactKind(input?.artifactKind ?? input?.kind); + const now = Number.isFinite(Number(input?.now)) ? Number(input.now) : Date.now(); + const id = optionalString(input?.id) ?? `${ARTIFACT_ID_PREFIX}${packageId}_${artifactKind}_${now}`; + return { + id, + packageId, + artifactKind, + role: optionalString(input?.role), + assetId: optionalString(input?.assetId), + pageId: optionalString(input?.pageId), + publicationId: optionalString(input?.publicationId), + agentRunId: optionalString(input?.agentRunId), + messageId: optionalString(input?.messageId), + displayName: optionalString(input?.displayName), + mimeType: optionalString(input?.mimeType), + sizeBytes: optionalNumber(input?.sizeBytes), + storageKey: optionalString(input?.storageKey), + canonicalUrl: optionalString(input?.canonicalUrl), + sortOrder: safeSortOrder(input?.sortOrder), + createdAt: Number.isFinite(Number(input?.createdAt)) ? Number(input.createdAt) : now + }; +} +function artifactManifestEntry(artifact) { + return { + artifactId: artifact.id, + kind: artifact.artifactKind, + ...artifact.role ? { role: artifact.role } : {}, + ...artifact.assetId ? { assetId: artifact.assetId } : {}, + ...artifact.pageId ? { pageId: artifact.pageId } : {}, + ...artifact.publicationId ? { publicationId: artifact.publicationId } : {}, + ...artifact.agentRunId ? { agentRunId: artifact.agentRunId } : {}, + ...artifact.messageId ? { messageId: artifact.messageId } : {}, + ...artifact.displayName ? { displayName: artifact.displayName } : {}, + ...artifact.mimeType ? { mimeType: artifact.mimeType } : {}, + ...artifact.sizeBytes !== null ? { sizeBytes: artifact.sizeBytes } : {}, + ...artifact.storageKey ? { storageKey: artifact.storageKey } : {}, + ...artifact.canonicalUrl ? { canonicalUrl: artifact.canonicalUrl } : {}, + sortOrder: artifact.sortOrder, + createdAt: artifact.createdAt + }; +} +function groupedManifestEntries(artifacts, keyField, idField) { + const groups = /* @__PURE__ */ new Map(); + for (const artifact of artifacts) { + const key = artifact[keyField]; + if (!key) continue; + const existing = groups.get(key) ?? { + [idField]: key, + artifactIds: [], + createdAt: artifact.createdAt, + updatedAt: artifact.createdAt + }; + existing.artifactIds.push(artifact.id); + if (!existing.displayName && artifact.displayName) existing.displayName = artifact.displayName; + if (!existing.canonicalUrl && artifact.canonicalUrl) existing.canonicalUrl = artifact.canonicalUrl; + existing.createdAt = Math.min(existing.createdAt, artifact.createdAt); + existing.updatedAt = Math.max(existing.updatedAt, artifact.createdAt); + groups.set(key, existing); + } + return [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a[idField].localeCompare(b[idField])); +} +function provenanceManifestEntries(artifacts, keyField, idField) { + const groups = /* @__PURE__ */ new Map(); + for (const artifact of artifacts) { + const key = artifact[keyField]; + if (!key) continue; + const existing = groups.get(key) ?? { + [idField]: key, + artifactIds: [], + kinds: [], + createdAt: artifact.createdAt, + updatedAt: artifact.createdAt + }; + existing.artifactIds.push(artifact.id); + if (!existing.kinds.includes(artifact.artifactKind)) existing.kinds.push(artifact.artifactKind); + existing.createdAt = Math.min(existing.createdAt, artifact.createdAt); + existing.updatedAt = Math.max(existing.updatedAt, artifact.createdAt); + groups.set(key, existing); + } + return [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a[idField].localeCompare(b[idField])); +} +function buildConversationPackageManifest({ packageRecord, artifacts = [] }) { + const record = createConversationPackageRecord(packageRecord); + const normalizedArtifacts = artifacts.map((artifact) => createConversationArtifact({ ...artifact, packageId: record.id })).sort((a, b) => a.sortOrder - b.sortOrder || a.createdAt - b.createdAt || a.id.localeCompare(b.id)); + return { + schemaVersion: CONVERSATION_PACKAGE_SCHEMA_VERSION, + packageId: record.id, + userId: record.userId, + sessionId: record.sessionId, + uri: buildConversationPackageUri({ userId: record.userId, sessionId: record.sessionId }), + title: record.title, + status: record.status, + storagePrefix: record.storagePrefix, + manifestAssetId: record.manifestAssetId, + createdAt: record.createdAt, + updatedAt: record.updatedAt, + artifacts: normalizedArtifacts.map(artifactManifestEntry), + pages: groupedManifestEntries(normalizedArtifacts, "pageId", "pageId"), + publications: groupedManifestEntries(normalizedArtifacts, "publicationId", "publicationId"), + messages: provenanceManifestEntries(normalizedArtifacts, "messageId", "messageId"), + agentRuns: provenanceManifestEntries(normalizedArtifacts, "agentRunId", "agentRunId") + }; +} + +// mindspace-conversation-package-store.mjs +function storeError(message, code) { + return Object.assign(new Error(message), { code }); +} +function requirePool(pool2) { + if (!pool2 || typeof pool2.query !== "function") { + throw storeError("MindSpace conversation package store requires a queryable pool", "missing_query_pool"); + } + return pool2; +} +function packageFromRow(row) { + if (!row) return null; + return createConversationPackageRecord({ + id: row.id, + userId: row.user_id, + sessionId: row.session_id, + title: row.title, + status: row.status, + storagePrefix: row.storage_prefix, + manifestAssetId: row.manifest_asset_id, + createdAt: Number(row.created_at), + updatedAt: Number(row.updated_at) + }); +} +function artifactFromRow(row) { + if (!row) return null; + return createConversationArtifact({ + id: row.id, + packageId: row.package_id, + artifactKind: row.artifact_kind, + role: row.role, + assetId: row.asset_id, + pageId: row.page_id, + publicationId: row.publication_id, + agentRunId: row.agent_run_id, + messageId: row.message_id, + displayName: row.display_name, + mimeType: row.mime_type, + sizeBytes: row.size_bytes == null ? null : Number(row.size_bytes), + storageKey: row.storage_key, + canonicalUrl: row.canonical_url, + sortOrder: Number(row.sort_order ?? 0), + createdAt: Number(row.created_at) + }); +} +function createConversationPackageStore(pool2) { + const db = requirePool(pool2); + return { + async upsertPackage(input) { + const record = createConversationPackageRecord(input); + await db.query( + `INSERT INTO h5_conversation_packages + (id, user_id, session_id, title, status, storage_prefix, manifest_asset_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + title = VALUES(title), + status = VALUES(status), + storage_prefix = VALUES(storage_prefix), + manifest_asset_id = VALUES(manifest_asset_id), + updated_at = VALUES(updated_at)`, + [ + record.id, + record.userId, + record.sessionId, + record.title, + record.status, + record.storagePrefix, + record.manifestAssetId, + record.createdAt, + record.updatedAt + ] + ); + return record; + }, + async getPackageBySession(userId, sessionId) { + const [rows] = await db.query( + `SELECT id, user_id, session_id, title, status, storage_prefix, manifest_asset_id, created_at, updated_at + FROM h5_conversation_packages + WHERE user_id = ? AND session_id = ? + LIMIT 1`, + [userId, sessionId] + ); + return packageFromRow(rows?.[0]); + }, + async upsertArtifact(input) { + const artifact = createConversationArtifact(input); + await db.query( + `INSERT INTO h5_conversation_artifacts + (id, package_id, artifact_kind, role, asset_id, page_id, publication_id, agent_run_id, + message_id, display_name, mime_type, size_bytes, storage_key, canonical_url, sort_order, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + artifact_kind = VALUES(artifact_kind), + role = VALUES(role), + asset_id = VALUES(asset_id), + page_id = VALUES(page_id), + publication_id = VALUES(publication_id), + agent_run_id = VALUES(agent_run_id), + message_id = VALUES(message_id), + display_name = VALUES(display_name), + mime_type = VALUES(mime_type), + size_bytes = VALUES(size_bytes), + storage_key = VALUES(storage_key), + canonical_url = VALUES(canonical_url), + sort_order = VALUES(sort_order)`, + [ + artifact.id, + artifact.packageId, + artifact.artifactKind, + artifact.role, + artifact.assetId, + artifact.pageId, + artifact.publicationId, + artifact.agentRunId, + artifact.messageId, + artifact.displayName, + artifact.mimeType, + artifact.sizeBytes, + artifact.storageKey, + artifact.canonicalUrl, + artifact.sortOrder, + artifact.createdAt + ] + ); + return artifact; + }, + async listArtifacts(packageId) { + const [rows] = await db.query( + `SELECT id, package_id, artifact_kind, role, asset_id, page_id, publication_id, agent_run_id, + message_id, display_name, mime_type, size_bytes, storage_key, canonical_url, sort_order, created_at + FROM h5_conversation_artifacts + WHERE package_id = ? + ORDER BY sort_order ASC, created_at ASC, id ASC`, + [packageId] + ); + return (rows ?? []).map(artifactFromRow); + }, + async buildManifestForSession({ userId, sessionId }) { + const packageRecord = await this.getPackageBySession(userId, sessionId); + if (!packageRecord) return null; + const artifacts = await this.listArtifacts(packageRecord.id); + return buildConversationPackageManifest({ packageRecord, artifacts }); + } + }; +} + +// mindspace-storage-adapter.mjs +import fs20 from "node:fs"; +import fsp from "node:fs/promises"; +import path24 from "node:path"; +function storageError(message, code, details = {}) { + return Object.assign(new Error(message), { code, ...details }); +} +function normalizeMindSpaceStorageKey(key) { + const raw = String(key ?? "").trim(); + if (!raw) { + throw storageError("storage key is required", "invalid_storage_key"); + } + if (raw.includes("\0")) { + throw storageError("storage key must not contain null bytes", "invalid_storage_key"); + } + if (raw.includes("\\")) { + throw storageError("storage key must use forward slashes", "invalid_storage_key"); + } + if (path24.isAbsolute(raw) || /^[A-Za-z]:\//.test(raw)) { + throw storageError("storage key must be relative", "invalid_storage_key"); + } + if (raw.split("/").includes("..")) { + throw storageError("storage key must not traverse parent directories", "invalid_storage_key"); + } + const normalized = path24.posix.normalize(raw.replace(/^\/+/, "")); + if (normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) { + throw storageError("storage key must not traverse parent directories", "invalid_storage_key"); + } + return normalized; +} +function resolveStoragePath(storageRoot, key) { + const root = path24.resolve(String(storageRoot ?? "")); + if (!root) { + throw storageError("storage root is required", "invalid_storage_root"); + } + const normalizedKey = normalizeMindSpaceStorageKey(key); + const target = path24.resolve(root, ...normalizedKey.split("/")); + if (target !== root && !target.startsWith(`${root}${path24.sep}`)) { + throw storageError("storage key resolves outside storage root", "invalid_storage_key", { + key: normalizedKey + }); + } + return target; +} +async function walkFiles(root, dir, prefix, results) { + const entries = await fsp.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const absolutePath = path24.join(dir, entry.name); + const relativePath = path24.relative(root, absolutePath).split(path24.sep).join("/"); + if (entry.isDirectory()) { + await walkFiles(root, absolutePath, prefix, results); + } else if (!prefix || relativePath === prefix || relativePath.startsWith(`${prefix}/`)) { + const stat = await fsp.stat(absolutePath); + results.push({ + key: relativePath, + sizeBytes: stat.size, + updatedAt: stat.mtimeMs + }); + } + } +} +function createLocalMindSpaceStorageAdapter(storageRoot) { + const root = path24.resolve(storageRoot); + return { + kind: "local-fs", + root, + async putObject(key, body) { + const target = resolveStoragePath(root, key); + await fsp.mkdir(path24.dirname(target), { recursive: true }); + await fsp.writeFile(target, body); + const stat = await fsp.stat(target); + return { + key: normalizeMindSpaceStorageKey(key), + sizeBytes: stat.size, + updatedAt: stat.mtimeMs + }; + }, + async getObject(key) { + return fsp.readFile(resolveStoragePath(root, key)); + }, + async statObject(key) { + try { + const stat = await fsp.stat(resolveStoragePath(root, key)); + return { + key: normalizeMindSpaceStorageKey(key), + exists: true, + sizeBytes: stat.size, + updatedAt: stat.mtimeMs, + isDirectory: stat.isDirectory() + }; + } catch (error) { + if (error?.code !== "ENOENT") throw error; + return { + key: normalizeMindSpaceStorageKey(key), + exists: false, + sizeBytes: 0, + updatedAt: null, + isDirectory: false + }; + } + }, + async deleteObject(key) { + await fsp.rm(resolveStoragePath(root, key), { force: true }); + return { key: normalizeMindSpaceStorageKey(key), deleted: true }; + }, + async listObjects(prefix = "") { + const normalizedPrefix = prefix ? normalizeMindSpaceStorageKey(prefix).replace(/\/$/, "") : ""; + const results = []; + try { + await walkFiles(root, root, normalizedPrefix, results); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + } + return results.sort((a, b) => a.key.localeCompare(b.key)); + }, + async copyObject(sourceKey, targetKey) { + const source = resolveStoragePath(root, sourceKey); + const target = resolveStoragePath(root, targetKey); + await fsp.mkdir(path24.dirname(target), { recursive: true }); + await fsp.copyFile(source, target); + const stat = await fsp.stat(target); + return { + key: normalizeMindSpaceStorageKey(targetKey), + sourceKey: normalizeMindSpaceStorageKey(sourceKey), + sizeBytes: stat.size, + updatedAt: stat.mtimeMs + }; + }, + createReadStream(key) { + return fs20.createReadStream(resolveStoragePath(root, key)); + }, + createWriteStream(key) { + const target = resolveStoragePath(root, key); + fs20.mkdirSync(path24.dirname(target), { recursive: true }); + return fs20.createWriteStream(target); + }, + async getSignedUrl() { + throw storageError("local fs adapter does not support signed URLs", "signed_url_not_supported"); + } + }; +} + +// mindspace-service.mjs +function serviceError(message, code, details = {}) { + return Object.assign(new Error(message), { code, ...details }); +} +function requireStorageAdapter(storageAdapter) { + if (!storageAdapter || typeof storageAdapter.putObject !== "function") { + throw serviceError("MindSpace storage adapter is required", "missing_mindspace_storage_adapter"); + } + return storageAdapter; +} +function packageStorageKey(packageRecord, relativePath) { + const record = createConversationPackageRecord(packageRecord); + const relative = normalizeMindSpaceStorageKey(relativePath); + return `${record.storagePrefix}/${relative}`; +} +function createMindSpaceServiceFacade({ + storageAdapter, + publicBaseUrl, + conversationPackageBackfill, + conversationPackagePublicHtmlHydrator, + logger = console +} = {}) { + const storage = requireStorageAdapter(storageAdapter); + return { + createConversationPackage(input) { + return createConversationPackageRecord(input); + }, + createConversationArtifact(input) { + return createConversationArtifact(input); + }, + buildConversationManifest(input) { + return buildConversationPackageManifest(input); + }, + async putConversationManifest({ packageRecord, artifacts = [] }) { + const manifest = buildConversationPackageManifest({ packageRecord, artifacts }); + const body = `${JSON.stringify(manifest, null, 2)} +`; + const writeResult = await this.putPackageObject({ + packageRecord: manifest, + relativePath: "manifest.json", + body + }); + return { + manifest, + storageKey: writeResult.key, + writeResult + }; + }, + buildConversationStoragePrefix(input) { + return buildConversationStoragePrefix(input); + }, + createPublicPageUrl({ ownerKey, filename }) { + return createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey, filename }); + }, + createPublicObjectUrl({ ownerKey, relativePath }) { + return createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath }); + }, + resolvePackageStorageKey({ packageRecord, relativePath }) { + return packageStorageKey(packageRecord, relativePath); + }, + async putPackageObject({ packageRecord, relativePath, body }) { + return storage.putObject(packageStorageKey(packageRecord, relativePath), body); + }, + async getPackageObject({ packageRecord, relativePath }) { + return storage.getObject(packageStorageKey(packageRecord, relativePath)); + }, + async statPackageObject({ packageRecord, relativePath }) { + return storage.statObject(packageStorageKey(packageRecord, relativePath)); + }, + async listPackageObjects({ packageRecord, prefix = "" }) { + const record = createConversationPackageRecord(packageRecord); + const cleanPrefix = prefix ? normalizeMindSpacePublicRelativePath(prefix) : ""; + const storagePrefix = cleanPrefix ? `${record.storagePrefix}/${cleanPrefix}` : record.storagePrefix; + return storage.listObjects(storagePrefix); + }, + async prepareConversationPackageRead({ user, sessionId } = {}) { + if (typeof conversationPackageBackfill === "function") { + await conversationPackageBackfill({ user, sessionId }).catch((error) => { + logger?.warn?.("[MindSpace] conversation package DB backfill failed:", error?.message ?? error); + }); + } + if (typeof conversationPackagePublicHtmlHydrator === "function") { + return conversationPackagePublicHtmlHydrator({ user, sessionId }); + } + return []; + } + }; +} + // mindspace-page-patch.mjs function mergeMindSpacePagePatch(base, patch) { return { @@ -19316,10 +22444,10 @@ import { Agent as Agent4, fetch as undiciFetch4 } from "undici"; import { jsonrepair } from "jsonrepair"; // llm-providers.mjs -import crypto13 from "node:crypto"; -import fs17 from "node:fs"; +import crypto14 from "node:crypto"; +import fs21 from "node:fs"; import os from "node:os"; -import path21 from "node:path"; +import path25 from "node:path"; import { spawn, spawnSync } from "node:child_process"; import { Agent as Agent3, fetch as undiciFetch3 } from "undici"; var CUSTOM_PROVIDER_ID = "__custom__"; @@ -19407,12 +22535,12 @@ var insecureDispatcher3 = new Agent3({ }); function resolveEncryptionKey(explicitKey) { const raw = explicitKey ?? process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY ?? "local-dev-secret"; - return crypto13.createHash("sha256").update(raw).digest(); + return crypto14.createHash("sha256").update(raw).digest(); } function encryptSecret(plaintext, encryptionKey) { const key = resolveEncryptionKey(encryptionKey); - const iv = crypto13.randomBytes(12); - const cipher = crypto13.createCipheriv("aes-256-gcm", key, iv); + const iv = crypto14.randomBytes(12); + const cipher = crypto14.createCipheriv("aes-256-gcm", key, iv); const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); return { ciphertext: encrypted.toString("base64"), @@ -19422,7 +22550,7 @@ function encryptSecret(plaintext, encryptionKey) { } function decryptSecret({ ciphertext, iv, tag }, encryptionKey) { const key = resolveEncryptionKey(encryptionKey); - const decipher = crypto13.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64")); + const decipher = crypto14.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64")); decipher.setAuthTag(Buffer.from(tag, "base64")); const plain = Buffer.concat([ decipher.update(Buffer.from(ciphertext, "base64")), @@ -19598,7 +22726,7 @@ function profileFromRow(row, decryptRow) { models }; } -function normalizeExecutor(raw) { +function normalizeExecutor2(raw) { const executor = String(raw ?? "").trim().toLowerCase(); return executorById[executor] ? executor : null; } @@ -19606,7 +22734,7 @@ function normalizePurpose(raw) { return String(raw ?? "default").trim() || "default"; } function rowToExecutorBinding(row, keyPublic = null) { - const executor = normalizeExecutor(row.executor); + const executor = normalizeExecutor2(row.executor); const meta = executorById[executor] ?? null; return { id: row.id, @@ -19638,9 +22766,10 @@ function executorEnvForProfile(executor, profile, model, { includeSecret = false if (baseUrl) common.TKMIND_EXECUTOR_API_BASE = baseUrl; if (includeSecret) common.TKMIND_EXECUTOR_API_KEY = profile.apiKey; if (executor === "aider") { + const aiderModel = aiderModelNameForProvider(providerId, model); const env = { ...common, - AIDER_MODEL: model + AIDER_MODEL: aiderModel }; if (providerId === "anthropic") { env.AIDER_ANTHROPIC_API_KEY = apiKeyValue; @@ -19673,8 +22802,16 @@ function executorEnvForProfile(executor, profile, model, { includeSecret = false TKMIND_MODEL: model }; } +function aiderModelNameForProvider(providerId, model) { + const normalizedModel = String(model ?? "").trim(); + if (!normalizedModel) return normalizedModel; + if (String(providerId ?? "").trim() === "custom_deepseek") { + return normalizedModel.startsWith("deepseek/") ? normalizedModel : `deepseek/${normalizedModel}`; + } + return normalizedModel; +} function launchLogDir() { - return path21.join(os.tmpdir(), "memindadm-launches"); + return path25.join(os.tmpdir(), "memindadm-launches"); } function normalizeLaunchMode(mode) { const normalized = String(mode ?? "headless").trim().toLowerCase(); @@ -19685,9 +22822,9 @@ function resolveExecutorCommand(executor) { return candidates.map((item) => String(item ?? "").trim()).find(Boolean) ?? executor; } function commandExists(command) { - if (path21.isAbsolute(command)) { + if (path25.isAbsolute(command)) { try { - fs17.accessSync(command, fs17.constants.X_OK); + fs21.accessSync(command, fs21.constants.X_OK); return true; } catch { return false; @@ -19716,13 +22853,13 @@ function spawnDetachedExecutor(plan, { logDir = launchLogDir() } = {}) { message: `${plan.command} \u672A\u5B89\u88C5\u6216\u4E0D\u5728 PATH \u4E2D` }; } - fs17.mkdirSync(logDir, { recursive: true }); + fs21.mkdirSync(logDir, { recursive: true }); const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); - const logFile = path21.join( + const logFile = path25.join( logDir, - `${plan.executor ?? "executor"}-${stamp}-${crypto13.randomUUID().slice(0, 8)}.log` + `${plan.executor ?? "executor"}-${stamp}-${crypto14.randomUUID().slice(0, 8)}.log` ); - const fd = fs17.openSync(logFile, "a"); + const fd = fs21.openSync(logFile, "a"); try { const child = spawn(plan.command, plan.args ?? [], { cwd: plan.cwd ?? process.cwd(), @@ -19753,7 +22890,7 @@ function spawnDetachedExecutor(plan, { logDir = launchLogDir() } = {}) { }; } finally { try { - fs17.closeSync(fd); + fs21.closeSync(fd); } catch { } } @@ -19769,7 +22906,7 @@ function isProcessAlive(pid) { } } function launchStateKey(executor, purpose = "default") { - return `${normalizeExecutor(executor)}:${normalizePurpose(purpose)}`; + return `${normalizeExecutor2(executor)}:${normalizePurpose(purpose)}`; } function buildExecutorLaunchPlan(runtime, options = {}) { if (!runtime?.ok) { @@ -19793,6 +22930,7 @@ function buildExecutorLaunchPlan(runtime, options = {}) { if (runtime.executor === "aider") { const hasInstruction = Boolean(instruction); const command = resolveExecutorCommand("aider"); + const model = aiderModelNameForProvider(env.TKMIND_EXECUTOR_PROVIDER, runtime.model); return { ok: true, executor: "aider", @@ -19800,7 +22938,7 @@ function buildExecutorLaunchPlan(runtime, options = {}) { command, args: [ "--model", - runtime.model, + model, ...hasInstruction ? ["--message", instruction] : [], "--yes-always" ], @@ -20112,7 +23250,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret return rows[0] ?? null; } async function getExecutorBindingRow(executor, purpose = "default") { - const normalizedExecutor = normalizeExecutor(executor); + const normalizedExecutor = normalizeExecutor2(executor); if (!normalizedExecutor) return null; const [rows] = await pool2.query( "SELECT * FROM h5_llm_executor_bindings WHERE executor = ? AND purpose = ? LIMIT 1", @@ -20209,7 +23347,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret } } async function resolveExecutorRuntimeConfig(executor, { purpose = "default", includeSecret = false } = {}) { - const normalizedExecutor = normalizeExecutor(executor); + const normalizedExecutor = normalizeExecutor2(executor); if (!normalizedExecutor) return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; const binding = await getExecutorBindingRow(normalizedExecutor, purpose); if (!binding?.enabled) { @@ -20284,7 +23422,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret const key = launchStateKey(executor, purpose); launchStates.set(key, { ...state, - executor: normalizeExecutor(executor), + executor: normalizeExecutor2(executor), purpose: normalizePurpose(purpose) }); return launchStates.get(key); @@ -20463,7 +23601,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret }); }, async setExecutorBinding(executor, payload = {}) { - const normalizedExecutor = normalizeExecutor(executor); + const normalizedExecutor = normalizeExecutor2(executor); if (!normalizedExecutor) return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; const purpose = normalizePurpose(payload.purpose); const enabled = payload.enabled !== false; @@ -20495,7 +23633,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret `INSERT INTO h5_llm_executor_bindings (id, executor, purpose, provider_key_id, model, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [crypto13.randomUUID(), normalizedExecutor, purpose, keyId, model, enabled ? 1 : 0, now, now] + [crypto14.randomUUID(), normalizedExecutor, purpose, keyId, model, enabled ? 1 : 0, now, now] ); } const row = await getExecutorBindingRow(normalizedExecutor, purpose); @@ -20535,7 +23673,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret return stateToPublic(getLaunchState(executor, purpose)); }, async launchExecutor(executor, options = {}) { - const normalizedExecutor = normalizeExecutor(executor); + const normalizedExecutor = normalizeExecutor2(executor); if (!normalizedExecutor) { return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; } @@ -20609,7 +23747,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret }; }, async stopExecutor(executor, options = {}) { - const normalizedExecutor = normalizeExecutor(executor); + const normalizedExecutor = normalizeExecutor2(executor); if (!normalizedExecutor) { return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; } @@ -20629,7 +23767,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret }; }, async restartExecutor(executor, options = {}) { - const normalizedExecutor = normalizeExecutor(executor); + const normalizedExecutor = normalizeExecutor2(executor); if (!normalizedExecutor) { return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; } @@ -20703,7 +23841,7 @@ function createLlmProviderService(pool2, { apiTarget, apiTargets = [], apiSecret const shouldSelect = Number(countRows[0]?.total ?? 0) === 0; const encrypted = encryptSecret(apiKey, encryptionKey); const now = Date.now(); - const id = crypto13.randomUUID(); + const id = crypto14.randomUUID(); if (shouldSelect) await clearSelected(); await pool2.query( `INSERT INTO h5_llm_provider_keys @@ -21251,1027 +24389,6 @@ async function suggestCoverMetaWithAi(pool2, { title, summary, html, instruction return parsed; } -// mindspace-publications.mjs -import crypto14 from "node:crypto"; -import fs18 from "node:fs/promises"; -import path22 from "node:path"; - -// mindspace-html-localize.mjs -var GOOGLE_FONTS_CSS_IMPORT_RE = /@import\s+url\(\s*['"]?(https:\/\/fonts\.googleapis\.com\/[^'")\s]+)['"]?\s*\)\s*;?/gi; -var GOOGLE_FONTS_LINK_RE = /]*\bhref=['"](https:\/\/fonts\.googleapis\.com\/[^'"]+)['"][^>]*>/gi; -async function fetchStylesheet(url) { - const response = await fetch(url, { - headers: { "User-Agent": "TKMind-MindSpace/1.0" }, - signal: AbortSignal.timeout(15e3) - }); - if (!response.ok) { - throw new Error(`\u65E0\u6CD5\u4E0B\u8F7D\u6837\u5F0F\u8868: ${response.status}`); - } - return response.text(); -} -function uniqueUrls(matches) { - return [...new Set(matches.map((value) => value.replace(/&/g, "&")))]; -} -async function localizeGoogleFontsCss(html) { - const source = String(html); - const importUrls = uniqueUrls([...source.matchAll(GOOGLE_FONTS_CSS_IMPORT_RE)].map((m) => m[1])); - const linkUrls = uniqueUrls([...source.matchAll(GOOGLE_FONTS_LINK_RE)].map((m) => m[1])); - const stylesheetUrls = uniqueUrls([...importUrls, ...linkUrls]); - if (!stylesheetUrls.length) { - return { html: source, localizedCount: 0, inlinedBytes: 0 }; - } - const inlinedBlocks = []; - for (const url of stylesheetUrls) { - try { - inlinedBlocks.push(`/* localized from ${url} */ -${await fetchStylesheet(url)}`); - } catch { - inlinedBlocks.push(`/* failed to localize ${url} */`); - } - } - let next = source; - for (const url of importUrls) { - const pattern = new RegExp( - `@import\\s+url\\(\\s*['"]?${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]?\\s*\\)\\s*;?`, - "gi" - ); - next = next.replace(pattern, ""); - } - next = next.replace(GOOGLE_FONTS_LINK_RE, ""); - const styleBlock = ``; - if (/]*>/i.test(next)) { - next = next.replace(/]*)>/i, `${styleBlock}`); - } else if (/]*>/i.test(next)) { - next = next.replace(/]*)>/i, `${styleBlock}`); - } else { - next = `${styleBlock} -${next}`; - } - const inlinedBytes = Buffer.byteLength(inlinedBlocks.join("\n")); - return { html: next, localizedCount: stylesheetUrls.length, inlinedBytes }; -} - -// mindspace-publications.mjs -var SCANNER_VERSION = "mindspace-content-v1"; -var PRIVATE_ASSET_DOWNLOAD_URL_PATTERN = /(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi; -var ACCESS_MODES = /* @__PURE__ */ new Set([ - "public", - "password", - "private_link", - "time_limited", - "login_required", - "owner_only" -]); -var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/; -function publicationError(message, code, details) { - return Object.assign(new Error(message), { code, details }); -} -function normalizeSlug(value) { - const slug = String(value ?? "").normalize("NFKC").trim().toLowerCase(); - if (!SLUG_PATTERN.test(slug)) { - throw publicationError("\u9875\u9762\u5730\u5740\u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26", "invalid_publish_input"); - } - return slug; -} -async function findAvailableSlug(pool2, userId, preferredSlug, excludePageId) { - const normalized = normalizeSlug(preferredSlug); - const candidates = [normalized]; - for (let index = 2; index <= 20; index += 1) { - candidates.push(`${normalized}-${index}`); - } - candidates.push(`${normalized}-${excludePageId.replace(/-/g, "").slice(0, 8)}`); - for (const candidate of candidates) { - if (!SLUG_PATTERN.test(candidate)) continue; - const [rows] = await pool2.query( - `SELECT id FROM h5_publish_records - WHERE user_id = ? AND url_slug = ? AND status = 'online' AND page_id <> ? - LIMIT 1`, - [userId, candidate, excludePageId] - ); - if (!rows[0]) return candidate; - } - return `${normalized}-${crypto14.randomBytes(3).toString("hex")}`; -} -function normalizeAccessMode(value) { - const mode = String(value ?? "public"); - if (!ACCESS_MODES.has(mode)) { - throw publicationError("\u4E0D\u652F\u6301\u7684\u8BBF\u95EE\u65B9\u5F0F", "invalid_publish_input"); - } - return mode; -} -function normalizePassword(value, required) { - const password = String(value ?? ""); - if (!required && !password) return null; - if (password.length < 8 || password.length > 128) { - throw publicationError("\u8BBF\u95EE\u5BC6\u7801\u957F\u5EA6\u5FC5\u987B\u4E3A 8 \u5230 128 \u4E2A\u5B57\u7B26", "invalid_publish_input"); - } - return password; -} -function normalizeExpiresAt(value, required) { - if (!required && (value == null || value === "")) return null; - const expiresAt = Number(value); - if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now()) { - throw publicationError("\u8FC7\u671F\u65F6\u95F4\u5FC5\u987B\u665A\u4E8E\u5F53\u524D\u65F6\u95F4", "invalid_publish_input"); - } - return expiresAt; -} -function hashPassword(password) { - const salt = crypto14.randomBytes(16); - const hash = crypto14.scryptSync(password, salt, 32); - return `scrypt$${salt.toString("hex")}$${hash.toString("hex")}`; -} -function verifyPassword2(password, encoded) { - const [algorithm, saltHex, hashHex] = String(encoded ?? "").split("$"); - if (algorithm !== "scrypt" || !saltHex || !hashHex) return false; - const expected = Buffer.from(hashHex, "hex"); - const actual = crypto14.scryptSync(String(password ?? ""), Buffer.from(saltHex, "hex"), expected.length); - return expected.length === actual.length && crypto14.timingSafeEqual(expected, actual); -} -function deviceType(userAgent) { - const value = String(userAgent ?? ""); - if (/bot|crawler|spider|slurp/i.test(value)) return "bot"; - if (/ipad|tablet|kindle|silk/i.test(value)) return "tablet"; - if (/mobile|iphone|android/i.test(value)) return "mobile"; - if (value) return "desktop"; - return "unknown"; -} -function referrerHost(referrer) { - if (!referrer) return null; - try { - return new URL(referrer).hostname.slice(0, 255) || null; - } catch { - return null; - } -} -function publicationResponse(row) { - if (!row) return null; - return { - id: row.id, - pageId: row.page_id, - pageVersionId: row.page_version_id, - urlSlug: row.url_slug, - publicUrl: row.public_url, - accessMode: row.access_mode, - expiresAt: row.expires_at == null ? null : Number(row.expires_at), - status: row.status, - viewCount: Number(row.view_count ?? 0), - publishedAt: Number(row.published_at), - offlineAt: row.offline_at == null ? null : Number(row.offline_at) - }; -} -function publicHomepageResponse(owner, pages) { - const totalViews = pages.reduce( - (sum, page) => sum + Number(page.viewCount ?? page.view_count ?? 0), - 0 - ); - return { - owner: { - id: owner.id, - slug: owner.slug, - username: owner.username, - displayName: owner.display_name || owner.username - }, - totalViews, - pageCount: pages.length, - pages: pages.map((page) => ({ - id: page.id, - pageId: page.page_id, - title: page.title, - summary: page.summary ?? "", - templateId: page.template_id, - publicUrl: page.public_url, - urlSlug: page.url_slug, - viewCount: Number(page.view_count ?? 0), - publishedAt: Number(page.published_at) - })) - }; -} -function buildPublicationThumbnailFallback(ownerSlug, urlSlug) { - return `/u/${encodeURIComponent(ownerSlug)}/pages/${encodeURIComponent(urlSlug)}.thumbnail.png`; -} -var PUBLIC_IMAGE_STORAGE_KEY_PATTERN2 = /^users\/([^/]+)\/images\/(\d{4}-\d{2}-\d{2}\/[^?#"'<>@\s]+)$/i; -function storageKeyToPublicStandardImageUrl(storageKey, publicBaseUrl) { - const match = String(storageKey ?? "").match(PUBLIC_IMAGE_STORAGE_KEY_PATTERN2); - if (!match) return null; - return buildPublicUrl(publicBaseUrl, match[1], `public/images/${match[2]}`); -} -function replaceImgproxyStandardImageReferences(html, publicBaseUrl) { - const source = String(html ?? ""); - if (!source) return source; - return source.replace( - /(?:https?:\/\/[^/"'<>?\s,)]+)?\/[^"'<>?\s,)]*plain\/local:\/\/\/(users\/[^"'<>?\s,)]*?\/images\/\d{4}-\d{2}-\d{2}\/[^"'<>?\s,@)]+)(?:@[a-z0-9]+)?(?:\?[^"'<>),\s]*)?/gi, - (value, storageKey) => storageKeyToPublicStandardImageUrl(storageKey, publicBaseUrl) ?? value - ); -} -function workspacePublicAssetRelativeUrl(htmlRelativePath, assetRelativePath) { - const htmlPath = String(htmlRelativePath ?? "").replace(/^\/+/, "") || "public/index.html"; - const assetPath = String(assetRelativePath ?? "").replace(/^\/+/, ""); - const htmlDir = path22.posix.dirname(htmlPath); - const relativePath = path22.posix.relative(htmlDir === "." ? "" : htmlDir, assetPath).replace(/\\/g, "/"); - return relativePath || path22.posix.basename(assetPath); -} -function rewriteWorkspacePublicAssetReferences(html, htmlRelativePath) { - const source = String(html ?? ""); - if (!source) return source; - return source.replace( - /public\/((?:images|\.tmp-images)\/[^"'<>@\s)]+(?:\?[^"'<>)\s]*)?)/gi, - (value, assetRelativePath) => workspacePublicAssetRelativeUrl(htmlRelativePath, `public/${assetRelativePath}`) - ); -} -async function localizePrivateImageReferences({ - pool: pool2, - userId, - html, - absoluteStoragePath: absoluteStoragePath2, - imgproxySigner = null -}) { - const source = String(html ?? ""); - const matches = [...source.matchAll(PRIVATE_ASSET_DOWNLOAD_URL_PATTERN)]; - if (matches.length === 0) return source; - const assetIds = [...new Set(matches.map((match) => match[1]).filter(Boolean))]; - if (assetIds.length === 0) return source; - const [assets] = await pool2.query( - `SELECT a.id, a.mime_type, v.storage_key - FROM h5_assets a - JOIN h5_asset_versions v ON v.id = a.current_version_id - WHERE a.user_id = ? AND a.id IN (?) AND a.mime_type LIKE 'image/%' - AND a.status <> 'deleted'`, - [userId, assetIds] - ); - const byId = new Map(assets.map((asset) => [asset.id, asset])); - const replacements = /* @__PURE__ */ new Map(); - for (const assetId of assetIds) { - const asset = byId.get(assetId); - if (!asset) continue; - if (imgproxySigner) { - replacements.set(assetId, imgproxySigner.buildUrl(imgproxySigner.baseUrl, asset.storage_key, "display")); - } else { - const mimeType = String(asset.mime_type || "application/octet-stream"); - const buffer = await fs18.readFile(absoluteStoragePath2(asset.storage_key)); - replacements.set(assetId, `data:${mimeType};base64,${buffer.toString("base64")}`); - } - } - if (replacements.size === 0) return source; - return source.replace(PRIVATE_ASSET_DOWNLOAD_URL_PATTERN, (value, assetId) => { - return replacements.get(assetId) ?? value; - }); -} -async function prepareHtmlPublishContent({ - pool: pool2, - userId, - html, - ownerSlug, - urlSlug, - htmlRelativePath = null, - pageTitle = null, - absoluteStoragePath: absoluteStoragePath2, - imgproxySigner = null, - h5Root = null -}) { - let publishContent = (await localizeGoogleFontsCss(html)).html; - publishContent = replaceImgproxyStandardImageReferences( - publishContent, - resolvePublicBaseUrl() - ); - publishContent = await localizePrivateImageReferences({ - pool: pool2, - userId, - html: publishContent, - absoluteStoragePath: absoluteStoragePath2, - imgproxySigner - }); - const publishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null; - const resolvedHtmlRelativePath = htmlRelativePath ?? inferWorkspaceHtmlRelativePath({ - pageTitle, - htmlContent: publishContent, - publishDir - }); - publishContent = rewriteWorkspacePublicAssetReferences(publishContent, resolvedHtmlRelativePath); - const linkIndex = await loadWorkspaceDownloadLinkIndex(pool2, userId); - publishContent = rewriteRelativeDownloadLinks(publishContent, { - htmlRelativePath: resolvedHtmlRelativePath, - publishDir, - linkIndex, - publishKey: userId, - publicBaseUrl: resolvePublicBaseUrl(), - preferAssetDownload: false - }).html; - return replacePrivateResourceReferences( - publishContent, - buildPublicationThumbnailFallback(ownerSlug, urlSlug) - ); -} -function createPublicationService(pool2, options = {}) { - const storageRoot = path22.resolve(options.storageRoot ?? path22.join(process.cwd(), "data", "mindspace")); - const h5Root = options.h5Root ?? null; - const idFactory = options.idFactory ?? (() => crypto14.randomUUID()); - const publicPageLimitFallback = Number(options.publicPageLimit ?? 5); - let imgproxySigner = null; - if (process.env.IMGPROXY_BASE_URL && process.env.IMGPROXY_SIGNING_KEY && process.env.IMGPROXY_SIGNING_SALT) { - try { - imgproxySigner = createImgproxySigner( - process.env.IMGPROXY_SIGNING_KEY, - process.env.IMGPROXY_SIGNING_SALT - ); - imgproxySigner.baseUrl = process.env.IMGPROXY_BASE_URL; - console.log("[Publication] imgproxy signer initialized"); - } catch (err) { - console.warn("[Publication] imgproxy signer init failed:", err instanceof Error ? err.message : err); - } - } - const resolvePublicPageLimit = async () => { - try { - const config = await loadMindSpaceConfig(pool2); - return Number(config.publicPageLimit ?? publicPageLimitFallback); - } catch { - return publicPageLimitFallback; - } - }; - const absoluteStoragePath2 = (storageKey) => { - const resolved = path22.resolve(storageRoot, storageKey); - if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path22.sep}`)) { - throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C"); - } - return resolved; - }; - const resolveStoragePathCandidates = (storageKey) => { - const normalized = String(storageKey ?? "").replace(/\\/g, "/"); - const withoutMd = normalized.endsWith(".md") ? normalized.slice(0, -3) : normalized; - const match = withoutMd.match(/^users\/([^/]+)\/(assets|pages)\/([^/]+)\/(v\d+)$/); - if (!match) return [normalized]; - const [, userId, scope, entityId, versionTag] = match; - return [ - normalized, - `users/${userId}/${scope}/${entityId}/versions/${versionTag}.md`, - `users/${userId}/${scope}/${entityId}/versions/${versionTag}` - ].filter((candidate, index, list) => list.indexOf(candidate) === index); - }; - const resolveReadableStoragePath = async (storageKey) => { - let lastError = null; - for (const candidate of resolveStoragePathCandidates(storageKey)) { - const absolutePath = absoluteStoragePath2(candidate); - try { - await fs18.stat(absolutePath); - return absolutePath; - } catch (error) { - if (error?.code === "ENOENT") { - lastError = error; - continue; - } - throw error; - } - } - throw lastError ?? Object.assign(new Error("\u5B58\u50A8\u6587\u4EF6\u4E0D\u5B58\u5728"), { code: "storage_not_found" }); - }; - const loadVersion = async (userId, pageId, pageVersionId) => { - const [rows] = await pool2.query( - `SELECT p.id AS page_id, p.title, p.summary, p.page_type, p.template_id, p.current_version_id, - p.user_id, p.space_id, pv.id AS page_version_id, pv.version_no, - pv.bundle_asset_id, av.storage_key, - JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path - FROM h5_page_records p - JOIN h5_page_versions pv ON pv.page_id = p.id - JOIN h5_asset_versions av ON av.asset_id = pv.content_asset_id AND av.version_no = 1 - WHERE p.id = ? AND p.user_id = ? AND pv.id = ? AND p.status <> 'deleted' - LIMIT 1`, - [pageId, userId, pageVersionId] - ); - const row = rows[0]; - if (!row) throw publicationError("\u9875\u9762\u7248\u672C\u4E0D\u5B58\u5728", "page_not_found"); - return { - ...row, - version_no: Number(row.version_no), - content: await fs18.readFile(await resolveReadableStoragePath(row.storage_key), "utf8") - }; - }; - const loadOwnerSlug = async (userId) => { - const [rows] = await pool2.query( - `SELECT COALESCE(slug, username) AS public_slug - FROM h5_users - WHERE id = ? - LIMIT 1`, - [userId] - ); - const ownerSlug = String(rows[0]?.public_slug ?? "").trim(); - if (!ownerSlug) throw publicationError("\u7528\u6237\u516C\u5F00\u5730\u5740\u4E0D\u5B58\u5728", "publication_owner_not_found"); - return ownerSlug; - }; - const preparePublishContent = async (page, ownerSlug, urlSlug) => { - let publishContent = page.content; - if (page.page_type !== "html") return publishContent; - const publishDir = h5Root ? resolvePublishDir(h5Root, { id: page.user_id }) : null; - return prepareHtmlPublishContent({ - pool: pool2, - userId: page.user_id, - html: publishContent, - ownerSlug, - urlSlug, - pageTitle: page.title, - htmlRelativePath: inferWorkspaceHtmlRelativePath({ - snapshotRelativePath: page.source_relative_path, - pageTitle: page.title, - htmlContent: publishContent, - publishDir - }), - absoluteStoragePath: absoluteStoragePath2, - h5Root, - imgproxySigner: imgproxySigner ? { buildUrl: (path31, preset) => imgproxySigner.buildUrl(imgproxySigner.baseUrl, path31, preset) } : null - }); - }; - const persistScan = async (conn, userId, pageVersionId, scan, now) => { - const scanId = idFactory(); - await conn.query( - `INSERT INTO h5_security_scans - (id, user_id, target_type, target_id, scanner_version, status, risk_level, - findings_count, summary_json, started_at, completed_at) - VALUES (?, ?, 'page_version', ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - scanId, - userId, - pageVersionId, - SCANNER_VERSION, - scan.status, - scan.riskLevel, - scan.findings.length, - JSON.stringify({ types: scan.findings.map((finding) => finding.type) }), - now, - now - ] - ); - for (const finding of scan.findings) { - await conn.query( - `INSERT INTO h5_security_findings - (id, scan_id, finding_type, risk_level, occurrence_count, sample_masked, blocking, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [ - idFactory(), - scanId, - finding.type, - finding.riskLevel, - finding.occurrenceCount, - finding.sampleMasked, - finding.blocking ? 1 : 0, - now - ] - ); - } - return scanId; - }; - const check = async (userId, pageId, input) => { - const slug = normalizeSlug(input.urlSlug); - const accessMode = normalizeAccessMode(input.accessMode); - normalizePassword(input.password, accessMode === "password"); - const expiresAt = normalizeExpiresAt( - input.expiresAt, - accessMode === "time_limited" - ); - const page = await loadVersion(userId, pageId, input.pageVersionId); - if (page.page_version_id !== page.current_version_id) { - throw publicationError("\u53EA\u80FD\u53D1\u5E03\u9875\u9762\u7684\u5F53\u524D\u7248\u672C", "invalid_state_transition"); - } - const [conflicts] = await pool2.query( - `SELECT pr.id, pr.page_id, p.title - FROM h5_publish_records pr - JOIN h5_page_records p ON p.id = pr.page_id - WHERE pr.user_id = ? AND pr.url_slug = ? AND pr.status = 'online' - AND pr.page_id <> ? LIMIT 1`, - [userId, slug, pageId] - ); - const conflict = conflicts[0] ?? null; - const ownerSlug = await loadOwnerSlug(userId); - const publishContent = await preparePublishContent(page, ownerSlug, slug); - const scan = scanContent(`${page.title} -${page.summary ?? ""} -${publishContent}`, { - format: page.page_type === "html" ? "html" : "text", - allowHtmlActiveContent: page.page_type === "html" - }); - const suggestedUrlSlug = conflict && accessMode !== "private_link" ? await findAvailableSlug(pool2, userId, slug, pageId) : null; - return { - pageVersionId: page.page_version_id, - urlSlug: slug, - accessMode, - expiresAt, - slugAvailable: accessMode === "private_link" || !conflict, - slugConflict: conflict ? { pageId: conflict.page_id, title: conflict.title } : null, - suggestedUrlSlug, - ...scan, - allowed: (accessMode === "private_link" || !conflict) && scan.allowed - }; - }; - const publish = async (userId, pageId, input) => { - const result = await check(userId, pageId, input); - if (!result.slugAvailable) throw publicationError("\u9875\u9762\u5730\u5740\u5DF2\u88AB\u4F7F\u7528", "slug_conflict"); - if (!result.allowed) { - throw publicationError("\u9875\u9762\u5305\u542B\u4E0D\u53EF\u516C\u5F00\u7684\u9AD8\u98CE\u9669\u4FE1\u606F", "security_risk_blocked", { - findings: result.findings - }); - } - const acknowledged = new Set(input.acknowledgedFindingIds ?? []); - const missingAcknowledgements = result.findings.filter( - (finding) => !finding.blocking && !acknowledged.has(finding.id) - ); - if (missingAcknowledgements.length) { - throw publicationError("\u8BF7\u5148\u786E\u8BA4\u53D1\u5E03\u68C0\u67E5\u4E2D\u7684\u98CE\u9669\u63D0\u793A", "security_ack_required", { - findings: missingAcknowledgements - }); - } - const page = await loadVersion(userId, pageId, result.pageVersionId); - const ownerSlug = await loadOwnerSlug(userId); - const publishContent = await preparePublishContent(page, ownerSlug, result.urlSlug); - const html = pageInternals.renderPublicationHtml({ ...page, content: publishContent }); - const htmlBytes = Buffer.byteLength(html); - const conn = await pool2.getConnection(); - let writtenPath; - try { - await conn.beginTransaction(); - const [spaces] = await conn.query( - `SELECT quota_bytes, used_bytes, reserved_bytes FROM h5_user_spaces - WHERE id = ? AND user_id = ? FOR UPDATE`, - [page.space_id, userId] - ); - const available = Number(spaces[0]?.quota_bytes ?? 0) - Number(spaces[0]?.used_bytes ?? 0) - Number(spaces[0]?.reserved_bytes ?? 0); - if (available < htmlBytes) { - throw publicationError("\u5269\u4F59\u7A7A\u95F4\u4E0D\u8DB3", "quota_exceeded", { - requiredBytes: htmlBytes, - availableBytes: Math.max(0, available) - }); - } - const [publicationUsage] = await conn.query( - `SELECT COUNT(DISTINCT page_id) AS public_page_used, - MAX(CASE WHEN page_id = ? THEN 1 ELSE 0 END) AS page_already_online - FROM h5_publish_records - WHERE user_id = ? AND status = 'online'`, - [pageId, userId] - ); - const publicPageLimit = await resolvePublicPageLimit(); - if (!Number(publicationUsage[0]?.page_already_online) && Number(publicationUsage[0]?.public_page_used) >= publicPageLimit) { - throw publicationError("\u516C\u5F00\u9875\u9762\u6570\u91CF\u5DF2\u8FBE\u5230\u5957\u9910\u4E0A\u9650", "public_page_limit_exceeded", { - limit: publicPageLimit - }); - } - const [categories] = await conn.query( - `SELECT id FROM h5_space_categories - WHERE user_id = ? AND space_id = ? AND category_code = 'public' LIMIT 1`, - [userId, page.space_id] - ); - if (!categories[0]) throw publicationError("\u516C\u5F00\u5206\u7C7B\u4E0D\u5B58\u5728", "category_not_found"); - const now = Date.now(); - const scanId = await persistScan(conn, userId, page.page_version_id, result, now); - const bundleAssetId = idFactory(); - const assetVersionId = idFactory(); - const publishId = idFactory(); - const privateToken = result.accessMode === "private_link" ? crypto14.randomBytes(24).toString("base64url") : null; - const tokenHash = privateToken ? crypto14.createHash("sha256").update(privateToken).digest("hex") : null; - const passwordHash = result.accessMode === "password" ? hashPassword(input.password) : null; - const storageKey = path22.posix.join( - "users", - userId, - "publications", - publishId, - "index.html" - ); - writtenPath = absoluteStoragePath2(storageKey); - await fs18.mkdir(path22.dirname(writtenPath), { recursive: true }); - await fs18.writeFile(writtenPath, html, { flag: "wx" }); - const checksum = crypto14.createHash("sha256").update(html).digest("hex"); - const publicBaseUrl = resolvePublicBaseUrl(); - const publicUrl2 = privateToken ? `/s/${privateToken}` : `${publicBaseUrl}/u/${encodeURIComponent(ownerSlug)}/pages/${result.urlSlug}`; - await conn.query( - `INSERT INTO h5_assets - (id, user_id, space_id, category_id, asset_type, mime_type, original_filename, - display_name, current_version_id, size_bytes, checksum, risk_level, visibility, - status, source_type, created_at, updated_at) - VALUES (?, ?, ?, ?, 'html', 'text/html', 'index.html', ?, ?, ?, ?, ?, - 'public_candidate', 'ready', 'generated', ?, ?)`, - [ - bundleAssetId, - userId, - page.space_id, - categories[0].id, - `${page.title} \xB7 \u53D1\u5E03\u5FEB\u7167`, - assetVersionId, - htmlBytes, - checksum, - result.riskLevel, - now, - now - ] - ); - await conn.query( - `INSERT INTO h5_asset_versions - (id, asset_id, version_no, storage_key, size_bytes, checksum, mime_type, - created_by, change_note, scan_status, created_at) - VALUES (?, ?, 1, ?, ?, ?, 'text/html', ?, '\u53D1\u5E03\u9875\u9762\u5FEB\u7167', ?, ?)`, - [ - assetVersionId, - bundleAssetId, - storageKey, - htmlBytes, - checksum, - userId, - result.status === "passed" ? "passed" : "warned", - now - ] - ); - const [previousRows] = await conn.query( - `SELECT id, page_version_id FROM h5_publish_records - WHERE user_id = ? AND page_id = ? - ORDER BY published_at DESC LIMIT 1 FOR UPDATE`, - [userId, pageId] - ); - await conn.query( - `UPDATE h5_publish_records SET status = 'offline', offline_at = ?, updated_at = ? - WHERE user_id = ? AND page_id = ? AND status = 'online'`, - [now, now, userId, pageId] - ); - await conn.query( - `UPDATE h5_page_versions - SET immutable = 1, bundle_asset_id = ?, security_scan_id = ? - WHERE id = ? AND page_id = ?`, - [bundleAssetId, scanId, page.page_version_id, pageId] - ); - await conn.query( - `INSERT INTO h5_publish_records - (id, user_id, page_id, page_version_id, publish_type, url_slug, public_url, - access_mode, password_hash, token_hash, token_prefix, expires_at, user_confirmed_at, published_at, - status, view_count, security_scan_id, created_at, updated_at) - VALUES (?, ?, ?, ?, 'page', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'online', 0, ?, ?, ?)`, - [ - publishId, - userId, - pageId, - page.page_version_id, - result.urlSlug, - publicUrl2, - result.accessMode, - passwordHash, - tokenHash, - privateToken?.slice(0, 8) ?? null, - result.expiresAt, - null, - now, - scanId, - now, - now - ] - ); - await conn.query( - `INSERT INTO h5_publication_events - (id, publish_id, event_type, actor_id, old_page_version_id, new_page_version_id, - access_mode, detail_json, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - idFactory(), - publishId, - previousRows.length ? "republished" : "published", - userId, - previousRows[0]?.page_version_id ?? null, - page.page_version_id, - result.accessMode, - JSON.stringify({ findings: result.findings.map((finding) => finding.id) }), - now - ] - ); - await conn.query( - `UPDATE h5_page_records - SET current_publish_id = ?, status = ?, visibility = ?, updated_at = ? - WHERE id = ? AND user_id = ?`, - [ - publishId, - result.accessMode === "public" ? "published" : "protected", - result.accessMode === "public" ? "public" : "private", - now, - pageId, - userId - ] - ); - await conn.query( - `UPDATE h5_user_spaces SET used_bytes = used_bytes + ?, updated_at = ? - WHERE id = ? AND user_id = ?`, - [htmlBytes, now, page.space_id, userId] - ); - const assetUrlPattern = /\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)/gi; - const assetIds = /* @__PURE__ */ new Set(); - let match; - while ((match = assetUrlPattern.exec(html)) !== null) { - assetIds.add(match[1]); - } - if (assetIds.size > 0) { - const refValues = Array.from(assetIds).map((assetId) => [publishId, assetId, now]); - await conn.query( - `INSERT IGNORE INTO h5_publication_asset_refs - (publication_id, asset_id, created_at) VALUES ` + refValues.map(() => "(?, ?, ?)").join(","), - refValues.flat() - ); - } - await conn.commit(); - return { - ...publicationResponse({ - id: publishId, - page_id: pageId, - page_version_id: page.page_version_id, - url_slug: result.urlSlug, - public_url: publicUrl2, - access_mode: result.accessMode, - status: "online", - view_count: 0, - published_at: now, - offline_at: null, - expires_at: result.expiresAt - }), - findings: result.findings - }; - } catch (error) { - await conn.rollback(); - if (writtenPath) await fs18.rm(writtenPath, { force: true }).catch(() => { - }); - throw error; - } finally { - conn.release(); - } - }; - const getCurrent = async (userId, pageId) => { - const [rows] = await pool2.query( - `SELECT pr.* FROM h5_publish_records pr - JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id - WHERE pr.page_id = ? AND pr.user_id = ? AND pr.status = 'online' - ORDER BY pr.published_at DESC LIMIT 1`, - [pageId, userId] - ); - return publicationResponse(rows[0]); - }; - const updatePublicationStatus = async (userId, publicationId, { accessMode, expiresAt }) => { - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [rows] = await conn.query( - `SELECT * FROM h5_publish_records - WHERE id = ? AND user_id = ? AND status = 'online' LIMIT 1 FOR UPDATE`, - [publicationId, userId] - ); - const publication = rows[0]; - if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); - const normalizedMode = normalizeAccessMode(accessMode); - const normalizedExpiresAt = normalizeExpiresAt(expiresAt, normalizedMode === "time_limited"); - const now = Date.now(); - await conn.query( - `UPDATE h5_publish_records - SET access_mode = ?, expires_at = ?, user_confirmed_at = ?, updated_at = ? - WHERE id = ? AND user_id = ?`, - [normalizedMode, normalizedExpiresAt, now, now, publicationId, userId] - ); - await conn.commit(); - const updated = { - ...publication, - access_mode: normalizedMode, - expires_at: normalizedExpiresAt, - user_confirmed_at: now, - updated_at: now - }; - return publicationResponse(updated); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - }; - const offline = async (userId, publicationId) => { - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [rows] = await conn.query( - `SELECT * FROM h5_publish_records - WHERE id = ? AND user_id = ? AND status = 'online' LIMIT 1 FOR UPDATE`, - [publicationId, userId] - ); - const publication = rows[0]; - if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); - const now = Date.now(); - await conn.query( - `UPDATE h5_publish_records SET status = 'offline', offline_at = ?, updated_at = ? - WHERE id = ?`, - [now, now, publicationId] - ); - await conn.query( - `UPDATE h5_page_records - SET current_publish_id = NULL, status = 'offline', visibility = 'private', updated_at = ? - WHERE id = ? AND user_id = ? AND current_publish_id = ?`, - [now, publication.page_id, userId, publicationId] - ); - await conn.query( - `INSERT INTO h5_publication_events - (id, publish_id, event_type, actor_id, old_page_version_id, new_page_version_id, - access_mode, detail_json, created_at) - VALUES (?, ?, 'offlined', ?, ?, NULL, ?, '{}', ?)`, - [ - idFactory(), - publicationId, - userId, - publication.page_version_id, - publication.access_mode, - now - ] - ); - await conn.query( - `UPDATE plaza_posts SET status = 'hidden', updated_at = ? - WHERE publication_id = ? AND status != 'hidden'`, - [now, publicationId] - ); - await conn.commit(); - return { id: publicationId, status: "offline", offlineAt: now }; - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - }; - const resolveRow = async (row, viewerId, password, requestMeta = {}) => { - if (!row) throw publicationError("\u516C\u5F00\u9875\u9762\u4E0D\u5B58\u5728", "publication_not_found"); - if (row.expires_at && Number(row.expires_at) <= Date.now()) { - await pool2.query( - `UPDATE h5_publish_records SET status = 'expired', updated_at = ? WHERE id = ?`, - [Date.now(), row.id] - ); - throw publicationError("\u516C\u5F00\u9875\u9762\u5DF2\u8FC7\u671F", "publication_not_found"); - } - if (row.access_mode === "owner_only" && row.owner_id !== viewerId) { - throw publicationError("\u516C\u5F00\u9875\u9762\u4E0D\u5B58\u5728", "publication_not_found"); - } - if (row.access_mode === "login_required" && !viewerId) { - throw publicationError("\u767B\u5F55\u540E\u624D\u80FD\u8BBF\u95EE\u6B64\u9875\u9762", "publication_login_required"); - } - if (row.access_mode === "password" && !verifyPassword2(password, row.password_hash)) { - throw publicationError("\u9700\u8981\u8BBF\u95EE\u5BC6\u7801", "publication_password_required"); - } - const now = Date.now(); - await pool2.query( - `UPDATE h5_publish_records SET view_count = view_count + 1, updated_at = ? WHERE id = ?`, - [now, row.id] - ); - await pool2.query( - `INSERT INTO h5_publication_views - (id, publish_id, viewer_user_id, referrer_host, device_type, viewed_at) - VALUES (?, ?, ?, ?, ?, ?)`, - [ - idFactory(), - row.id, - viewerId ?? null, - referrerHost(requestMeta.referrer), - deviceType(requestMeta.userAgent), - now - ] - ); - const html = await fs18.readFile(await resolveReadableStoragePath(row.storage_key), "utf8"); - return { - html: await refreshPublishedHtmlDownloadLinks(row, html), - publication: publicationResponse({ ...row, view_count: Number(row.view_count) + 1 }) - }; - }; - const refreshPublishedHtmlDownloadLinks = async (row, html) => { - if (!h5Root || !row?.owner_id) return html; - const source = String(html ?? ""); - if (!source.trim()) return html; - const publishDir = resolvePublishDir(h5Root, { id: row.owner_id }); - const linkIndex = await loadWorkspaceDownloadLinkIndex(pool2, row.owner_id); - return rewriteRelativeDownloadLinks(html, { - htmlRelativePath: inferWorkspaceHtmlRelativePath({ - pageTitle: row.title, - htmlContent: html, - publishDir - }), - publishDir, - linkIndex, - publishKey: row.owner_id, - publicBaseUrl: resolvePublicBaseUrl(), - preferAssetDownload: false - }).html; - }; - const resolvePublic = async (ownerSlug, urlSlug, viewerId, password, requestMeta) => { - const [rows] = await pool2.query( - `SELECT pr.*, u.id AS owner_id, p.title, av.storage_key - FROM h5_publish_records pr - JOIN h5_users u ON u.id = pr.user_id - JOIN h5_page_records p ON p.id = pr.page_id - JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.immutable = 1 - JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1 - WHERE COALESCE(u.slug, u.username) = ? AND pr.url_slug = ? AND pr.status = 'online' - AND pr.access_mode <> 'private_link' - ORDER BY pr.published_at DESC LIMIT 1`, - [ownerSlug, urlSlug] - ); - return resolveRow(rows[0], viewerId, password, requestMeta); - }; - const resolvePrivateLink = async (token, viewerId, requestMeta) => { - const tokenHash = crypto14.createHash("sha256").update(String(token)).digest("hex"); - const [rows] = await pool2.query( - `SELECT pr.*, u.id AS owner_id, p.title, av.storage_key - FROM h5_publish_records pr - JOIN h5_users u ON u.id = pr.user_id - JOIN h5_page_records p ON p.id = pr.page_id - JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.immutable = 1 - JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1 - WHERE pr.token_hash = ? AND pr.status = 'online' AND pr.access_mode = 'private_link' - LIMIT 1`, - [tokenHash] - ); - return resolveRow(rows[0], viewerId, null, requestMeta); - }; - const getStats = async (userId, publicationId) => { - const [publications] = await pool2.query( - `SELECT id, view_count, published_at, status - FROM h5_publish_records WHERE id = ? AND user_id = ? LIMIT 1`, - [publicationId, userId] - ); - const publication = publications[0]; - if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); - const since = Date.now() - 30 * 24 * 60 * 60 * 1e3; - const [daily] = await pool2.query( - `SELECT DATE_FORMAT(FROM_UNIXTIME(viewed_at / 1000), '%Y-%m-%d') AS day, - COUNT(*) AS views - FROM h5_publication_views - WHERE publish_id = ? AND viewed_at >= ? - GROUP BY day ORDER BY day`, - [publicationId, since] - ); - const [devices] = await pool2.query( - `SELECT device_type, COUNT(*) AS views - FROM h5_publication_views WHERE publish_id = ? - GROUP BY device_type ORDER BY views DESC`, - [publicationId] - ); - const [sources] = await pool2.query( - `SELECT COALESCE(referrer_host, 'direct') AS source, COUNT(*) AS views - FROM h5_publication_views WHERE publish_id = ? - GROUP BY source ORDER BY views DESC LIMIT 10`, - [publicationId] - ); - return { - publicationId, - status: publication.status, - totalViews: Number(publication.view_count), - publishedAt: Number(publication.published_at), - daily: daily.map((row) => ({ day: row.day, views: Number(row.views) })), - devices: devices.map((row) => ({ - deviceType: row.device_type, - views: Number(row.views) - })), - sources: sources.map((row) => ({ source: row.source, views: Number(row.views) })) - }; - }; - const getPublicHomepage = async (ownerSlug) => { - const [owners] = await pool2.query( - `SELECT id, username, COALESCE(slug, username) AS slug, display_name - FROM h5_users - WHERE COALESCE(slug, username) = ? - LIMIT 1`, - [ownerSlug] - ); - const owner = owners[0]; - if (!owner) throw publicationError("\u516C\u5F00\u4E3B\u9875\u4E0D\u5B58\u5728", "publication_not_found"); - const [rows] = await pool2.query( - `SELECT pr.id, pr.page_id, pr.url_slug, pr.public_url, pr.view_count, pr.published_at, - p.title, p.summary, p.template_id - FROM h5_publish_records pr - JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id - WHERE pr.user_id = ? AND pr.status = 'online' AND pr.access_mode = 'public' - ORDER BY pr.published_at DESC - LIMIT 48`, - [owner.id] - ); - return publicHomepageResponse(owner, rows); - }; - const cleanupExpiredUnconfirmedPublications = async (now = Date.now()) => { - const [result] = await pool2.query( - `UPDATE h5_publish_records - SET access_mode = 'private', expires_at = NULL, updated_at = ? - WHERE access_mode = 'public' - AND expires_at IS NOT NULL - AND expires_at <= ? - AND user_confirmed_at IS NULL - AND status = 'online'`, - [now, now] - ); - return { cleaned: result.affectedRows }; - }; - return { - check, - publish, - getCurrent, - getPublicHomepage, - getStats, - offline, - updatePublicationStatus, - resolvePublic, - resolvePrivateLink, - cleanupExpiredUnconfirmedPublications - }; -} - // plaza-posts.mjs import crypto15 from "node:crypto"; @@ -25290,8 +27407,8 @@ function injectPlazaEmbedBootstrap(html) { // mindspace-cleanup.mjs import crypto22 from "node:crypto"; -import fs19 from "node:fs/promises"; -import path23 from "node:path"; +import fs22 from "node:fs/promises"; +import path26 from "node:path"; var WORKSPACE_TEMP_SKIP = /* @__PURE__ */ new Set([ ".tkmindhints", ".goosehints", @@ -25304,24 +27421,24 @@ function candidateId(kind, key) { } async function fileSize(targetPath) { try { - const stat = await fs19.stat(targetPath); + const stat = await fs22.stat(targetPath); return stat.isFile() ? stat.size : 0; } catch { return 0; } } -async function walkFiles(rootDir, onFile) { +async function walkFiles2(rootDir, onFile) { let entries; try { - entries = await fs19.readdir(rootDir, { withFileTypes: true }); + entries = await fs22.readdir(rootDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { - const fullPath = path23.join(rootDir, entry.name); + const fullPath = path26.join(rootDir, entry.name); if (entry.isDirectory()) { if (WORKSPACE_TEMP_SKIP.has(entry.name)) continue; - await walkFiles(fullPath, onFile); + await walkFiles2(fullPath, onFile); continue; } if (!entry.isFile()) continue; @@ -25329,11 +27446,11 @@ async function walkFiles(rootDir, onFile) { } } function createCleanupService(pool2, options = {}) { - const storageRoot = path23.resolve(options.storageRoot ?? path23.join(process.cwd(), "data", "mindspace")); - const h5Root = path23.resolve(options.h5Root ?? process.cwd()); + const storageRoot = path26.resolve(options.storageRoot ?? path26.join(process.cwd(), "data", "mindspace")); + const h5Root = path26.resolve(options.h5Root ?? process.cwd()); const absoluteStoragePath2 = (storageKey) => { - const resolved = path23.resolve(storageRoot, storageKey); - if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path23.sep}`)) { + const resolved = path26.resolve(storageRoot, storageKey); + if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path26.sep}`)) { throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C"); } return resolved; @@ -25363,9 +27480,9 @@ function createCleanupService(pool2, options = {}) { refId: upload.id }); } - const tmpDir = path23.join(storageRoot, "tmp", userId); - await walkFiles(tmpDir, async (fullPath) => { - const key = path23.relative(storageRoot, fullPath).split(path23.sep).join("/"); + const tmpDir = path26.join(storageRoot, "tmp", userId); + await walkFiles2(tmpDir, async (fullPath) => { + const key = path26.relative(storageRoot, fullPath).split(path26.sep).join("/"); const active = uploads.some( (upload) => upload.temporary_storage_key === key && upload.status === "reserved" ); @@ -25375,7 +27492,7 @@ function createCleanupService(pool2, options = {}) { candidates.push({ id: candidateId("tmp", key), kind: "orphan_tmp", - label: path23.basename(fullPath), + label: path26.basename(fullPath), path: key, sizeBytes, createdAt: null, @@ -25383,9 +27500,9 @@ function createCleanupService(pool2, options = {}) { refId: key }); }); - const workspaceTempDir = path23.join(h5Root, "temp", username); - await walkFiles(workspaceTempDir, async (fullPath) => { - const rel = path23.relative(workspaceTempDir, fullPath).split(path23.sep).join("/"); + const workspaceTempDir = path26.join(h5Root, "temp", username); + await walkFiles2(workspaceTempDir, async (fullPath) => { + const rel = path26.relative(workspaceTempDir, fullPath).split(path26.sep).join("/"); const sizeBytes = await fileSize(fullPath); if (!sizeBytes) return; candidates.push({ @@ -25467,7 +27584,7 @@ function createCleanupService(pool2, options = {}) { if (upload.temporary_storage_key) { const target = absoluteStoragePath2(upload.temporary_storage_key); const sizeBytes = await fileSize(target); - await fs19.rm(target, { force: true }); + await fs22.rm(target, { force: true }); freedBytes += sizeBytes; } removedCount += 1; @@ -25484,18 +27601,18 @@ function createCleanupService(pool2, options = {}) { if (item.kind === "orphan_tmp") { const target = absoluteStoragePath2(item.refId); const sizeBytes = await fileSize(target); - await fs19.rm(target, { force: true }); + await fs22.rm(target, { force: true }); freedBytes += sizeBytes; removedCount += 1; continue; } if (item.kind === "workspace_temp") { - const target = path23.join(h5Root, "temp", username, item.refId); - const resolvedRoot = path23.resolve(path23.join(h5Root, "temp", username)); - const resolved = path23.resolve(target); - if (!resolved.startsWith(`${resolvedRoot}${path23.sep}`)) continue; + const target = path26.join(h5Root, "temp", username, item.refId); + const resolvedRoot = path26.resolve(path26.join(h5Root, "temp", username)); + const resolved = path26.resolve(target); + if (!resolved.startsWith(`${resolvedRoot}${path26.sep}`)) continue; const sizeBytes = await fileSize(resolved); - await fs19.rm(resolved, { force: true }); + await fs22.rm(resolved, { force: true }); freedBytes += sizeBytes; removedCount += 1; continue; @@ -25581,7 +27698,7 @@ function createCleanupService(pool2, options = {}) { // mindspace-agent-jobs.mjs import crypto23 from "node:crypto"; -import path24 from "node:path"; +import path27 from "node:path"; var JOB_TYPES = /* @__PURE__ */ new Set(["generate_page", "analyze_asset", "summarize"]); var OUTPUT_TYPES = /* @__PURE__ */ new Set(["page_draft", "html_page", "markdown"]); var ACTIVE_JOB_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]); @@ -25594,7 +27711,7 @@ var RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([ "job_timed_out", "invalid_agent_job_output" ]); -function asNumber5(value) { +function asNumber6(value) { return Number(value ?? 0); } function agentJobError(message, code, details) { @@ -25674,10 +27791,10 @@ function jobResponse(row, assets = []) { errorCode: row.error_code ?? null, errorMessage: row.error_message ?? null, retryable: RETRYABLE_ERROR_CODES.has(row.error_code), - queuedAt: asNumber5(row.queued_at), - startedAt: row.started_at == null ? null : asNumber5(row.started_at), - completedAt: row.completed_at == null ? null : asNumber5(row.completed_at), - expiresAt: row.expires_at == null ? null : asNumber5(row.expires_at), + queuedAt: asNumber6(row.queued_at), + startedAt: row.started_at == null ? null : asNumber6(row.started_at), + completedAt: row.completed_at == null ? null : asNumber6(row.completed_at), + expiresAt: row.expires_at == null ? null : asNumber6(row.expires_at), assets }; } @@ -25708,10 +27825,10 @@ function createAgentJobService(pool2, options = {}) { const tokenTtlMs = Number(options.tokenTtlMs ?? 30 * 60 * 1e3); const maxOutputBytes = Number(options.maxOutputBytes ?? 2 * 1024 * 1024); const pageService = options.pageService; - const storageRoot = path24.resolve(options.storageRoot ?? path24.join(process.cwd(), "data", "mindspace")); + const storageRoot = path27.resolve(options.storageRoot ?? path27.join(process.cwd(), "data", "mindspace")); const absoluteStoragePath2 = (storageKey) => { - const resolved = path24.resolve(storageRoot, storageKey); - if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path24.sep}`)) { + const resolved = path27.resolve(storageRoot, storageKey); + if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path27.sep}`)) { throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C"); } return resolved; @@ -26035,7 +28152,7 @@ function createAgentJobService(pool2, options = {}) { categoryId: job.output_category_id, categoryCode: job.output_category_code, allowedTypes: [job.output_type], - maxBytes: asNumber5(job.max_output_bytes) + maxBytes: asNumber6(job.max_output_bytes) }, capabilities: permissionScope.capabilities ?? { network: false, @@ -26091,7 +28208,7 @@ function createAgentJobService(pool2, options = {}) { if (job.status !== "queued") { throw agentJobError("\u4EFB\u52A1\u5F53\u524D\u4E0D\u53EF\u9886\u53D6", "invalid_state_transition"); } - if (job.expires_at != null && asNumber5(job.expires_at) < nowFactory()) { + if (job.expires_at != null && asNumber6(job.expires_at) < nowFactory()) { throw agentJobError("\u4EFB\u52A1\u5DF2\u8FC7\u671F", "agent_job_expired"); } return await finalizeClaim(conn, job); @@ -26116,7 +28233,7 @@ function createAgentJobService(pool2, options = {}) { if (job.status !== "running") { throw agentJobError("\u4EFB\u52A1\u672A\u5904\u4E8E\u6267\u884C\u4E2D\u72B6\u6001", "invalid_state_transition"); } - if (job.expires_at != null && asNumber5(job.expires_at) < nowFactory()) { + if (job.expires_at != null && asNumber6(job.expires_at) < nowFactory()) { throw agentJobError("\u4EFB\u52A1 token \u5DF2\u8FC7\u671F", "agent_job_expired"); } if (!verifyTokenHash(job.job_token_hash, token)) { @@ -26181,9 +28298,9 @@ function createAgentJobService(pool2, options = {}) { if (!content.trim()) { throw agentJobError("\u4EFB\u52A1\u8F93\u51FA\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A", "invalid_agent_job_output"); } - if (outputBytes > asNumber5(job.max_output_bytes)) { + if (outputBytes > asNumber6(job.max_output_bytes)) { throw agentJobError("\u4EFB\u52A1\u8F93\u51FA\u8D85\u8FC7\u5927\u5C0F\u9650\u5236", "page_content_too_large", { - maxBytes: asNumber5(job.max_output_bytes) + maxBytes: asNumber6(job.max_output_bytes) }); } const sourceAssetIds = dedupeStrings(input?.sourceAssetIds); @@ -26255,7 +28372,7 @@ function createAgentJobService(pool2, options = {}) { // mindspace-agent-runner.mjs import crypto24 from "node:crypto"; -import fs20 from "node:fs/promises"; +import fs23 from "node:fs/promises"; import { Readable as Readable2 } from "node:stream"; import { Agent as Agent5, fetch as undiciFetch5 } from "undici"; import { jsonrepair as jsonrepair2 } from "jsonrepair"; @@ -26466,7 +28583,7 @@ async function readAssetContext(asset, maxBytes = DEFAULT_TEXT_BYTES) { note: "\u8BE5\u6587\u4EF6\u4E0D\u662F\u7EAF\u6587\u672C\uFF0CRunner \u5F53\u524D\u4E0D\u4F1A\u76F4\u63A5\u5185\u5D4C\u4E8C\u8FDB\u5236\u5185\u5BB9\u3002" }; } - const buffer = await fs20.readFile(asset.path); + const buffer = await fs23.readFile(asset.path); return { assetId: asset.assetId, displayName: asset.displayName, @@ -26710,12 +28827,12 @@ ${lines}`; } // mindspace-chat-save.mjs -import fs21 from "node:fs/promises"; -import path25 from "node:path"; +import fs24 from "node:fs/promises"; +import path28 from "node:path"; var PRIVATE_ASSET_URL_PATTERN3 = /(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi; var PUBLIC_TEMP_IMAGE_DIR2 = ".tmp-images"; function extensionForMime3(mimeType, filename = "") { - const ext = path25.extname(String(filename ?? "")).replace(/^\./, "").toLowerCase(); + const ext = path28.extname(String(filename ?? "")).replace(/^\./, "").toLowerCase(); if (ext && /^[a-z0-9]{1,8}$/.test(ext)) return ext === "jpeg" ? "jpg" : ext; if (mimeType === "image/jpeg") return "jpg"; if (mimeType === "image/png") return "png"; @@ -26727,17 +28844,17 @@ function publicTempImageFilename2(assetId, mimeType, fallbackFilename = "") { return `${assetId}.${extensionForMime3(mimeType, fallbackFilename)}`; } function absoluteStoragePath(storageRoot, storageKey) { - const resolvedRoot = path25.resolve(storageRoot); - const resolved = path25.resolve(resolvedRoot, storageKey); - if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path25.sep}`)) { + const resolvedRoot = path28.resolve(storageRoot); + const resolved = path28.resolve(resolvedRoot, storageKey); + if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path28.sep}`)) { throw Object.assign(new Error("\u8DEF\u5F84\u8D8A\u754C"), { code: "invalid_storage_path" }); } return resolved; } function workspaceTempImageRelativeUrl(htmlRelativePath, filename) { - const htmlDir = path25.posix.dirname(String(htmlRelativePath ?? "").replace(/^\/+/, "")); - const tmpDir = path25.posix.join(PUBLIC_ZONE_DIR, PUBLIC_TEMP_IMAGE_DIR2); - const relDir = htmlDir === "." || !htmlDir ? tmpDir : path25.posix.relative(htmlDir, tmpDir).replace(/\\/g, "/"); + const htmlDir = path28.posix.dirname(String(htmlRelativePath ?? "").replace(/^\/+/, "")); + const tmpDir = path28.posix.join(PUBLIC_ZONE_DIR, PUBLIC_TEMP_IMAGE_DIR2); + const relDir = htmlDir === "." || !htmlDir ? tmpDir : path28.posix.relative(htmlDir, tmpDir).replace(/\\/g, "/"); return relDir ? `${relDir}/${filename}` : filename; } async function materializePrivateAssetsInWorkspaceHtml({ @@ -26763,15 +28880,15 @@ async function materializePrivateAssetsInWorkspaceHtml({ [userId, assetIds] ); const publishDir = resolvePublishDir(h5Root, { id: userId }); - const tmpDir = path25.join(publishDir, PUBLIC_ZONE_DIR, PUBLIC_TEMP_IMAGE_DIR2); + const tmpDir = path28.join(publishDir, PUBLIC_ZONE_DIR, PUBLIC_TEMP_IMAGE_DIR2); const urlMap = /* @__PURE__ */ new Map(); for (const asset of assets) { const filename = publicTempImageFilename2(asset.id, asset.mime_type, asset.original_filename); - const target = path25.join(tmpDir, filename); + const target = path28.join(tmpDir, filename); try { const sourcePath = absoluteStoragePath(storageRoot, asset.storage_key); - await fs21.mkdir(tmpDir, { recursive: true }); - await fs21.copyFile(sourcePath, target); + await fs24.mkdir(tmpDir, { recursive: true }); + await fs24.copyFile(sourcePath, target); urlMap.set(asset.id, workspaceTempImageRelativeUrl(htmlRelativePath, filename)); } catch { } @@ -26790,8 +28907,8 @@ async function materializePrivateAssetsInWorkspaceHtml({ return { html, count: 0, changed: false }; } if (writeBack) { - const htmlPath = path25.join(publishDir, htmlRelativePath); - await fs21.writeFile(htmlPath, result, "utf8"); + const htmlPath = path28.join(publishDir, htmlRelativePath); + await fs24.writeFile(htmlPath, result, "utf8"); } return { html: result, count, changed: true }; } @@ -26854,7 +28971,7 @@ function createAssetDataUriResolver(pool2, storageRoot, userId) { return null; } try { - const buffer = await fs21.readFile(absoluteStoragePath(storageRoot, asset.storage_key)); + const buffer = await fs24.readFile(absoluteStoragePath(storageRoot, asset.storage_key)); const mimeType = String(asset.mime_type || "image/jpeg"); const dataUri = `data:${mimeType};base64,${buffer.toString("base64")}`; cache.set(key, dataUri); @@ -26912,7 +29029,7 @@ function extractStaticPageLinks(content, { userId, username } = {}) { publicUrl: canonicalizeStaticPageUrl2(match[0], originalRelativePath, relativePath), owner, relativePath, - filename: path25.basename(relativePath) + filename: path28.basename(relativePath) }); } return links; @@ -26936,38 +29053,38 @@ function resolvePublishHtmlAbsolutePath(h5Root, userId, relativePath) { if (!key || !clean || !clean.toLowerCase().endsWith(".html")) { throw Object.assign(new Error("\u65E0\u6548\u7684\u9875\u9762\u8DEF\u5F84"), { code: "invalid_page_path" }); } - const publishRoot = path25.resolve(h5Root, PUBLISH_ROOT_DIR, key); - const absolute = path25.resolve(publishRoot, clean); - if (absolute !== publishRoot && !absolute.startsWith(`${publishRoot}${path25.sep}`)) { + const publishRoot = path28.resolve(h5Root, PUBLISH_ROOT_DIR, key); + const absolute = path28.resolve(publishRoot, clean); + if (absolute !== publishRoot && !absolute.startsWith(`${publishRoot}${path28.sep}`)) { throw Object.assign(new Error("\u9875\u9762\u8DEF\u5F84\u8D8A\u754C"), { code: "invalid_page_path" }); } return absolute; } async function readPublishHtml(h5Root, userId, relativePath) { const absolute = resolvePublishHtmlAbsolutePath(h5Root, userId, relativePath); - const content = await fs21.readFile(absolute, "utf8"); + const content = await fs24.readFile(absolute, "utf8"); if (!content.trim()) { throw Object.assign(new Error("\u9875\u9762\u5185\u5BB9\u4E3A\u7A7A"), { code: "empty_page_content" }); } - return { absolute, content, relativePath, filename: path25.basename(relativePath) }; + return { absolute, content, relativePath, filename: path28.basename(relativePath) }; } async function walkPublishHtmlByBasename(publishRoot, basename, maxDepth = 6, depth = 0) { if (depth > maxDepth || !basename.toLowerCase().endsWith(".html")) return null; let entries; try { - entries = await fs21.readdir(publishRoot, { withFileTypes: true }); + entries = await fs24.readdir(publishRoot, { withFileTypes: true }); } catch { return null; } for (const entry of entries) { if (entry.name.startsWith(".") || entry.name === "node_modules") continue; - const full = path25.join(publishRoot, entry.name); + const full = path28.join(publishRoot, entry.name); if (entry.isFile() && entry.name === basename) return full; } for (const entry of entries) { if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue; const found = await walkPublishHtmlByBasename( - path25.join(publishRoot, entry.name), + path28.join(publishRoot, entry.name), basename, maxDepth, depth + 1 @@ -26980,13 +29097,13 @@ async function collectPublishHtmlPaths(publishRoot, results, maxDepth = 6, depth if (depth > maxDepth || results.length >= 200) return; let entries; try { - entries = await fs21.readdir(publishRoot, { withFileTypes: true }); + entries = await fs24.readdir(publishRoot, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (entry.name.startsWith(".") || entry.name === "node_modules") continue; - const full = path25.join(publishRoot, entry.name); + const full = path28.join(publishRoot, entry.name); if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) { results.push(full); if (results.length >= 200) return; @@ -26994,7 +29111,7 @@ async function collectPublishHtmlPaths(publishRoot, results, maxDepth = 6, depth } for (const entry of entries) { if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue; - await collectPublishHtmlPaths(path25.join(publishRoot, entry.name), results, maxDepth, depth + 1); + await collectPublishHtmlPaths(path28.join(publishRoot, entry.name), results, maxDepth, depth + 1); if (results.length >= 200) return; } } @@ -27021,7 +29138,7 @@ function levenshteinDistance(left, right) { return prev[right.length]; } function htmlNameForSimilarity(relativePath) { - return path25.basename(String(relativePath ?? ""), ".html").toLowerCase(); + return path28.basename(String(relativePath ?? ""), ".html").toLowerCase(); } function isAcceptableSimilarHtmlMatch(requested, candidate, score, nextScore = Infinity) { if (!requested || !candidate || requested === candidate) return false; @@ -27037,7 +29154,7 @@ async function resolveClosestHtmlRelativePath(rootDir, relativePath) { const candidates = []; await collectPublishHtmlPaths(rootDir, candidates); const ranked = candidates.map((absolute) => { - const candidateRelative = path25.relative(rootDir, absolute).split(path25.sep).join("/"); + const candidateRelative = path28.relative(rootDir, absolute).split(path28.sep).join("/"); const candidateName = htmlNameForSimilarity(candidateRelative); return { relativePath: candidateRelative, @@ -27054,10 +29171,10 @@ async function resolveClosestHtmlRelativePath(rootDir, relativePath) { } async function findPublishHtml(h5Root, userId, relativePath) { const normalized = normalizeStaticHtmlRelativePath2(relativePath); - const basename = path25.basename(normalized); + const basename = path28.basename(normalized); const candidates = [ normalized, - path25.posix.join("public", basename), + path28.posix.join("public", basename), basename ].filter((value, index, list) => value && list.indexOf(value) === index); for (const candidate of candidates) { @@ -27066,14 +29183,14 @@ async function findPublishHtml(h5Root, userId, relativePath) { } catch { } } - const publishRoot = path25.resolve( + const publishRoot = path28.resolve( h5Root, PUBLISH_ROOT_DIR, String(userId ?? "").trim().toLowerCase() ); const absolute = await walkPublishHtmlByBasename(publishRoot, basename); if (absolute) { - const resolvedRelativePath = path25.relative(publishRoot, absolute).split(path25.sep).join("/"); + const resolvedRelativePath = path28.relative(publishRoot, absolute).split(path28.sep).join("/"); return readPublishHtml(h5Root, userId, resolvedRelativePath); } const similarRelativePath = await resolveClosestHtmlRelativePath(publishRoot, normalized); @@ -27084,7 +29201,7 @@ async function findPublishHtml(h5Root, userId, relativePath) { } function buildWorkspaceBaseHref(userId, htmlRelativePath) { const key = String(userId ?? "").trim(); - const dir = path25.posix.dirname(String(htmlRelativePath ?? "").replace(/^\/+/, "")); + const dir = path28.posix.dirname(String(htmlRelativePath ?? "").replace(/^\/+/, "")); const segments = dir === "." ? [] : dir.split("/").filter(Boolean); const encoded = segments.map((part) => encodeURIComponent(part)).join("/"); return `/${PUBLISH_ROOT_DIR}/${encodeURIComponent(key)}/${encoded ? `${encoded}/` : ""}`; @@ -27103,14 +29220,14 @@ function injectHtmlBaseHref(html, baseHref) { } function buildChatSavePreviewQuery({ sessionId, - messageId, + messageId: messageId2, selectedLinkIndex = 0, previewTitle, previewSummary } = {}) { const params = new URLSearchParams({ session_id: String(sessionId ?? ""), - message_id: String(messageId ?? ""), + message_id: String(messageId2 ?? ""), selected_link_index: String(selectedLinkIndex) }); const title = String(previewTitle ?? "").trim(); @@ -27279,9 +29396,10 @@ async function resolveChatSaveAnalysis({ } // mindspace-public-finish-sync.mjs -import fs22 from "node:fs"; -import path26 from "node:path"; +import fs25 from "node:fs"; +import path29 from "node:path"; var PUBLIC_HTML_PATH_PATTERN = /(?:^|[^a-z0-9_./-])(public\/[a-z0-9][a-z0-9._/-]{0,255}\.html)\b/i; +var PUBLIC_HTML_PATH_PATTERN_GLOBAL = /(?:^|[^a-z0-9_./-])(public\/[a-z0-9][a-z0-9._/-]{0,255}\.html)\b/gi; var PUBLIC_DOWNLOAD_ATTR_PATTERN = /(?:href|src)=["']([^"'#?\s]+)["']/gi; var DEFAULT_DOWNLOAD_SYNC_MIN_SIZE = 1024; function normalizePublicDownloadHref(href) { @@ -27295,13 +29413,13 @@ function normalizePublicDownloadHref(href) { return clean; } function extractPublicDownloadReferencesFromHtml(publishDir) { - const root = path26.resolve(String(publishDir ?? "")); - const publicDir = path26.join(root, "public"); - if (!fs22.existsSync(publicDir) || !fs22.statSync(publicDir).isDirectory()) return []; + const root = path29.resolve(String(publishDir ?? "")); + const publicDir = path29.join(root, "public"); + if (!fs25.existsSync(publicDir) || !fs25.statSync(publicDir).isDirectory()) return []; const refs = /* @__PURE__ */ new Map(); - for (const file of fs22.readdirSync(publicDir)) { + for (const file of fs25.readdirSync(publicDir)) { if (!file.toLowerCase().endsWith(".html")) continue; - const content = fs22.readFileSync(path26.join(publicDir, file), "utf8"); + const content = fs25.readFileSync(path29.join(publicDir, file), "utf8"); const htmlRelativePath = `public/${file}`; for (const match of content.matchAll(PUBLIC_DOWNLOAD_ATTR_PATTERN)) { const relativePath = normalizePublicDownloadHref(match[1]); @@ -27313,30 +29431,30 @@ function extractPublicDownloadReferencesFromHtml(publishDir) { return [...refs.values()]; } function listOaDownloadCandidates(publishDir, extension, { minSize = DEFAULT_DOWNLOAD_SYNC_MIN_SIZE } = {}) { - const oaDir = path26.join(path26.resolve(String(publishDir ?? "")), "oa"); - if (!fs22.existsSync(oaDir) || !fs22.statSync(oaDir).isDirectory()) return []; - return fs22.readdirSync(oaDir).filter((name) => name.toLowerCase().endsWith(extension)).map((name) => { - const absolutePath = path26.join(oaDir, name); - const stat = fs22.statSync(absolutePath); + const oaDir = path29.join(path29.resolve(String(publishDir ?? "")), "oa"); + if (!fs25.existsSync(oaDir) || !fs25.statSync(oaDir).isDirectory()) return []; + return fs25.readdirSync(oaDir).filter((name) => name.toLowerCase().endsWith(extension)).map((name) => { + const absolutePath = path29.join(oaDir, name); + const stat = fs25.statSync(absolutePath); return { name, absolutePath, size: stat.size, mtimeMs: stat.mtimeMs }; }).filter((item) => item.size >= minSize).sort((a, b) => b.mtimeMs - a.mtimeMs || b.size - a.size); } function resolvePublicDownloadSyncSource(root, relativePath, htmlRelativePath, { minSize = DEFAULT_DOWNLOAD_SYNC_MIN_SIZE } = {}) { - const extension = path26.extname(relativePath).toLowerCase(); - const basename = path26.basename(relativePath); - const htmlStemFilename = path26.posix.basename(htmlRelativePath).replace(/\.html$/i, extension); + const extension = path29.extname(relativePath).toLowerCase(); + const basename = path29.basename(relativePath); + const htmlStemFilename = path29.posix.basename(htmlRelativePath).replace(/\.html$/i, extension); const exactCandidates = [ - path26.posix.join("oa", basename), - path26.posix.join("oa", htmlStemFilename) + path29.posix.join("oa", basename), + path29.posix.join("oa", htmlStemFilename) ]; for (const candidate of exactCandidates) { - const absolutePath = path26.resolve(root, candidate); - if (absolutePath === path26.resolve(root, relativePath)) continue; + const absolutePath = path29.resolve(root, candidate); + if (absolutePath === path29.resolve(root, relativePath)) continue; try { - const stat = fs22.statSync(absolutePath); + const stat = fs25.statSync(absolutePath); if (stat.isFile() && stat.size >= minSize) { return { - name: path26.basename(candidate), + name: path29.basename(candidate), absolutePath, size: stat.size, strategy: "exact_oa_match" @@ -27355,7 +29473,7 @@ function syncPublicDocxDownloads({ publishDir, minCompleteSize = DEFAULT_DOWNLOAD_SYNC_MIN_SIZE } = {}) { - const root = path26.resolve(String(publishDir ?? "")); + const root = path29.resolve(String(publishDir ?? "")); if (!root) return { synced: [], skipped: [], missing: [], source: null, sourceByRelativePath: {} }; const synced = []; const skipped = []; @@ -27363,15 +29481,15 @@ function syncPublicDocxDownloads({ const sourceByRelativePath = {}; for (const reference of extractPublicDownloadReferencesFromHtml(root)) { const { relativePath, htmlRelativePath } = reference; - const destination = path26.resolve(root, relativePath); - if (destination !== root && !destination.startsWith(`${root}${path26.sep}`)) { + const destination = path29.resolve(root, relativePath); + if (destination !== root && !destination.startsWith(`${root}${path29.sep}`)) { skipped.push(relativePath); continue; } const source = resolvePublicDownloadSyncSource(root, relativePath, htmlRelativePath, { minSize: minCompleteSize }); - const destinationExists = fs22.existsSync(destination) && fs22.statSync(destination).isFile(); + const destinationExists = fs25.existsSync(destination) && fs25.statSync(destination).isFile(); if (destinationExists && !source) { skipped.push(relativePath); continue; @@ -27382,7 +29500,7 @@ function syncPublicDocxDownloads({ } if (destinationExists) { try { - if (fs22.statSync(destination).size >= source.size) { + if (fs25.statSync(destination).size >= source.size) { skipped.push(relativePath); continue; } @@ -27390,8 +29508,8 @@ function syncPublicDocxDownloads({ } } try { - fs22.mkdirSync(path26.dirname(destination), { recursive: true }); - fs22.copyFileSync(source.absolutePath, destination); + fs25.mkdirSync(path29.dirname(destination), { recursive: true }); + fs25.copyFileSync(source.absolutePath, destination); synced.push(relativePath); sourceByRelativePath[relativePath] = source.name; } catch { @@ -27412,6 +29530,11 @@ function messageText(message) { const displayText = typeof message?.metadata?.displayText === "string" ? message.metadata.displayText.trim() : ""; return [...textParts, displayText].filter(Boolean).join("\n"); } +function messageId(message) { + const id = message?.id ?? message?.message_id ?? message?.metadata?.messageId ?? message?.metadata?.message_id ?? null; + const normalized = String(id ?? "").trim(); + return normalized || null; +} function normalizePublicHtmlRelativePath(relativePath) { const parts = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== ".."); if (parts.length === 0) return ""; @@ -27419,6 +29542,52 @@ function normalizePublicHtmlRelativePath(relativePath) { if (parts.length === 1 && parts[0].toLowerCase().endsWith(".html")) return `public/${parts[0]}`; return parts.join("/"); } +function publicHtmlFileExists(publishDir, relativePath) { + if (!publishDir) return false; + const root = path29.resolve(String(publishDir ?? "")); + const normalized = normalizePublicHtmlRelativePath(relativePath); + if (!root || !normalized.toLowerCase().endsWith(".html")) return false; + const target = path29.resolve(root, normalized); + if (target !== root && !target.startsWith(`${root}${path29.sep}`)) return false; + return fs25.existsSync(target) && fs25.statSync(target).isFile(); +} +function collectOwnPublicHtmlArtifactRefs({ + messages, + currentUser, + publishDir, + materialized = [], + skipped = [] +} = {}) { + const refs = /* @__PURE__ */ new Map(); + const add = (relativePath, sourceMessageId = null) => { + const normalized = normalizePublicHtmlRelativePath(relativePath); + if (!normalized || !publicHtmlFileExists(publishDir, normalized)) return; + const existing = refs.get(normalized); + refs.set(normalized, { + relativePath: normalized, + messageId: existing?.messageId ?? sourceMessageId ?? null + }); + }; + for (const relativePath of [...materialized, ...skipped]) add(relativePath); + for (const artifact of extractPublicHtmlWriteArtifacts(messages, { publishDir })) { + add(artifact.relativePath, artifact.messageId ?? null); + } + for (const message of Array.isArray(messages) ? messages : []) { + const sourceMessageId = messageId(message); + const text = messageText(message); + if (!text) continue; + for (const link of extractStaticPageLinks(text, { + userId: currentUser?.id, + username: currentUser?.username ?? null + })) { + add(link.relativePath, sourceMessageId); + } + for (const match of text.matchAll(PUBLIC_HTML_PATH_PATTERN_GLOBAL)) { + add(match[1], sourceMessageId); + } + } + return [...refs.values()].sort((a, b) => a.relativePath.localeCompare(b.relativePath)); +} function isWriteLikeHtmlTool(name, args) { const action = String(args?.action ?? "").toLowerCase(); const normalizedName = String(name ?? "").trim(); @@ -27431,13 +29600,13 @@ function isEditLikeHtmlTool(name, args) { return (normalizedName === "edit_file" || normalizedName.endsWith("__edit_file") || normalizedName === "developer" && String(args?.action ?? "").toLowerCase() === "edit") && typeof args?.path === "string"; } function readPublicHtmlBaseline(publishDir, relativePath) { - const root = path26.resolve(String(publishDir ?? "")); + const root = path29.resolve(String(publishDir ?? "")); if (!root) return ""; - const destination = path26.resolve(root, relativePath); - if (destination !== root && !destination.startsWith(`${root}${path26.sep}`)) return ""; - if (!fs22.existsSync(destination) || !fs22.statSync(destination).isFile()) return ""; + const destination = path29.resolve(root, relativePath); + if (destination !== root && !destination.startsWith(`${root}${path29.sep}`)) return ""; + if (!fs25.existsSync(destination) || !fs25.statSync(destination).isFile()) return ""; try { - return fs22.readFileSync(destination, "utf8"); + return fs25.readFileSync(destination, "utf8"); } catch { return ""; } @@ -27451,6 +29620,7 @@ function applyPublicHtmlEdit(existing, args) { function extractPublicHtmlWriteArtifacts(messages = [], { publishDir = null } = {}) { const artifacts = /* @__PURE__ */ new Map(); for (const message of messages) { + const sourceMessageId = messageId(message); for (const item of message?.content ?? []) { if (item?.type !== "toolRequest") continue; const toolCall = item.toolCall?.value; @@ -27470,7 +29640,7 @@ function extractPublicHtmlWriteArtifacts(messages = [], { publishDir = null } = content = args.new_str; } if (content == null) continue; - artifacts.set(relativePath, { relativePath, content }); + artifacts.set(relativePath, { relativePath, content, messageId: sourceMessageId }); } } return [...artifacts.values()]; @@ -27481,9 +29651,9 @@ function isStubPublicHtmlContent(content) { return PUBLIC_HTML_STUB_MARKERS.some((marker) => value.includes(marker)); } function shouldReplaceExistingPublicHtml(destination, nextContent) { - if (!fs22.existsSync(destination) || !fs22.statSync(destination).isFile()) return true; + if (!fs25.existsSync(destination) || !fs25.statSync(destination).isFile()) return true; try { - const existing = fs22.readFileSync(destination, "utf8"); + const existing = fs25.readFileSync(destination, "utf8"); if (isStubPublicHtmlContent(existing)) return true; return typeof nextContent === "string" && nextContent.length > 0 && existing !== nextContent; } catch { @@ -27491,25 +29661,25 @@ function shouldReplaceExistingPublicHtml(destination, nextContent) { } } function materializeMissingPublicHtmlWrites({ messages, publishDir }) { - const root = path26.resolve(String(publishDir ?? "")); + const root = path29.resolve(String(publishDir ?? "")); if (!root) return { materialized: [], skipped: [] }; const materialized = []; const skipped = []; for (const artifact of extractPublicHtmlWriteArtifacts(messages, { publishDir: root })) { - const destination = path26.resolve(root, artifact.relativePath); - if (destination !== root && !destination.startsWith(`${root}${path26.sep}`)) { + const destination = path29.resolve(root, artifact.relativePath); + if (destination !== root && !destination.startsWith(`${root}${path29.sep}`)) { skipped.push(artifact.relativePath); continue; } - if (fs22.existsSync(destination) && fs22.statSync(destination).isFile()) { + if (fs25.existsSync(destination) && fs25.statSync(destination).isFile()) { if (!shouldReplaceExistingPublicHtml(destination, artifact.content)) { skipped.push(artifact.relativePath); continue; } } try { - fs22.mkdirSync(path26.dirname(destination), { recursive: true }); - fs22.writeFileSync(destination, artifact.content, "utf8"); + fs25.mkdirSync(path29.dirname(destination), { recursive: true }); + fs25.writeFileSync(destination, artifact.content, "utf8"); materialized.push(artifact.relativePath); } catch { skipped.push(artifact.relativePath); @@ -27575,24 +29745,46 @@ async function syncPublicHtmlAfterFinish({ messages, currentUser, publishDir, - syncWorkspaceAssets + syncWorkspaceAssets, + registerPublicHtmlArtifacts, + sessionId = null } = {}) { if (!hasRecentOwnPublicHtmlReference(messages, currentUser, { publishDir })) { return { materialized: [], skipped: [], synced: false }; } const { materialized, skipped } = materializeMissingPublicHtmlWrites({ messages, publishDir }); const docxSync = syncPublicDocxDownloads({ publishDir }); + const publicHtmlArtifactRefs = collectOwnPublicHtmlArtifactRefs({ + messages, + currentUser, + publishDir, + materialized, + skipped + }); + const publicHtmlRelativePaths = publicHtmlArtifactRefs.map((ref) => ref.relativePath); + const sourceMessageId = publicHtmlArtifactRefs.find((ref) => ref.messageId)?.messageId ?? null; let synced = false; if (typeof syncWorkspaceAssets === "function" && currentUser?.id) { - await syncWorkspaceAssets(currentUser.id, { categoryCode: "public" }); + await syncWorkspaceAssets(currentUser.id, { + categoryCode: "public", + sourceSessionId: sessionId, + sourceMessageId + }); synced = true; } - return { materialized, skipped, synced, docxSync }; + if (typeof registerPublicHtmlArtifacts === "function" && currentUser?.id && sessionId) { + await registerPublicHtmlArtifacts(currentUser.id, { + sessionId, + relativePaths: publicHtmlRelativePaths, + artifactRefs: publicHtmlArtifactRefs + }); + } + return { materialized, skipped, synced, docxSync, publicHtmlRelativePaths, publicHtmlArtifactRefs }; } // mindspace-page-sync.mjs -import fs23 from "node:fs/promises"; -import path27 from "node:path"; +import fs26 from "node:fs/promises"; +import path30 from "node:path"; var PUBLIC_HTML_SKIP_DIRS = /* @__PURE__ */ new Set([ ".tmp-images", "assets", @@ -27604,7 +29796,7 @@ function normalizePublicHtmlPath(relativePath) { return normalizeWorkspaceRelativePath2(relativePath); } function titleFromRelativePath(relativePath) { - const basename = path27.basename(String(relativePath ?? ""), ".html"); + const basename = path30.basename(String(relativePath ?? ""), ".html"); return basename.replace(/[-_]+/g, " ").trim() || "\u751F\u6210\u7684\u9875\u9762"; } function extractHtmlTitle(content) { @@ -27665,25 +29857,25 @@ async function loadIndexedWorkspacePages(pool2, userId) { return byPath; } async function listPublishPublicHtmlFiles(publishDir) { - const publicDir = path27.join(publishDir, "public"); + const publicDir = path30.join(publishDir, "public"); const files = []; async function walk(absDir, relWithinPublic) { let entries; try { - entries = await fs23.readdir(absDir, { withFileTypes: true }); + entries = await fs26.readdir(absDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (entry.name.startsWith(".") || PUBLIC_HTML_SKIP_DIRS.has(entry.name)) continue; - const absPath = path27.join(absDir, entry.name); + const absPath = path30.join(absDir, entry.name); const relPath = relWithinPublic ? `${relWithinPublic}/${entry.name}` : entry.name; if (entry.isDirectory()) { await walk(absPath, relPath); continue; } if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".html")) continue; - const stat = await fs23.stat(absPath); + const stat = await fs26.stat(absPath); files.push({ relativePath: normalizePublicHtmlPath(`public/${relPath}`), absolutePath: absPath, @@ -27773,7 +29965,7 @@ async function syncGeneratedPagesFromPublicAssets({ const htmlFiles = await listPublishPublicHtmlFiles(publishDir); for (const file of htmlFiles) { try { - const content = await fs23.readFile(file.absolutePath, "utf8"); + const content = await fs26.readFile(file.absolutePath, "utf8"); if (!content.trim()) { skipped += 1; continue; @@ -27824,7 +30016,7 @@ async function syncGeneratedPagesFromPublicAssets({ } try { const { path: assetPath } = await assetService.readAsset(userId, asset.id); - const content = await fs23.readFile(assetPath, "utf8"); + const content = await fs26.readFile(assetPath, "utf8"); const result = await upsertWorkspaceHtmlPage({ pool: pool2, pageService, @@ -28130,7 +30322,7 @@ function renderWechatSharePreviewHtml(preview, { note = "" } = {}) { } // mindspace-thumbnail-png.mjs -import fs24 from "node:fs"; +import fs27 from "node:fs"; import { Resvg } from "@resvg/resvg-js"; var RENDER_WIDTH = 540; function rasterizeThumbnailSvgToPng(svg) { @@ -28146,23 +30338,301 @@ function thumbnailPngPathForSvg(svgAbsPath) { return svgAbsPath.replace(/\.svg$/i, ".png"); } function ensureThumbnailPng(svgAbsPath) { - if (!fs24.existsSync(svgAbsPath)) return null; + if (!fs27.existsSync(svgAbsPath)) return null; const pngPath = thumbnailPngPathForSvg(svgAbsPath); try { - const svgStat = fs24.statSync(svgAbsPath); - if (fs24.existsSync(pngPath) && fs24.statSync(pngPath).mtimeMs >= svgStat.mtimeMs) { + const svgStat = fs27.statSync(svgAbsPath); + if (fs27.existsSync(pngPath) && fs27.statSync(pngPath).mtimeMs >= svgStat.mtimeMs) { return pngPath; } - const svg = fs24.readFileSync(svgAbsPath, "utf8"); - fs24.writeFileSync(pngPath, rasterizeThumbnailSvgToPng(svg)); + const svg = fs27.readFileSync(svgAbsPath, "utf8"); + fs27.writeFileSync(pngPath, rasterizeThumbnailSvgToPng(svg)); return pngPath; } catch { - return fs24.existsSync(pngPath) ? pngPath : null; + return fs27.existsSync(pngPath) ? pngPath : null; } } -// billing-subscription.mjs +// mindspace-long-image.mjs +import fs28 from "node:fs"; +import fsPromises3 from "node:fs/promises"; +import os2 from "node:os"; +import path31 from "node:path"; +import { pathToFileURL } from "node:url"; +var DEFAULT_VIEWPORT_WIDTH = 1280; +var DEFAULT_VIEWPORT_HEIGHT = 720; +var MAX_LONG_IMAGE_HEIGHT = 2e4; +function isLongImageDownloadRequest(query) { + const value = String(query?.download ?? query?.export ?? "").trim().toLowerCase(); + return value === "long-image" || value === "long_image" || value === "png"; +} +function longImagePathForHtml(htmlPath, outputPath = null) { + return outputPath || String(htmlPath).replace(/\.html$/i, ".long.png"); +} +function clampDimension(value, fallback, max) { + const number = Math.ceil(Number(value) || fallback); + return Math.max(320, Math.min(number, max)); +} +async function launchChromium(chromium) { + const base = { + headless: true, + args: ["--disable-dev-shm-usage", "--hide-scrollbars"] + }; + if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) { + return chromium.launch({ + ...base, + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH + }); + } + if (process.env.PLAYWRIGHT_CHROMIUM_CHANNEL) { + return chromium.launch({ ...base, channel: process.env.PLAYWRIGHT_CHROMIUM_CHANNEL }); + } + try { + return await chromium.launch(base); + } catch (error) { + if (process.platform === "darwin") { + return chromium.launch({ ...base, channel: "chrome" }); + } + throw error; + } +} +async function renderLongImage({ + htmlPath = null, + url = null, + outputPath = null, + viewportWidth = DEFAULT_VIEWPORT_WIDTH, + viewportHeight = DEFAULT_VIEWPORT_HEIGHT +} = {}) { + if (!htmlPath && !url) throw new Error("\u7F3A\u5C11 htmlPath \u6216 url"); + const targetUrl = url || pathToFileURL(path31.resolve(htmlPath)).toString(); + const destination = outputPath ? path31.resolve(outputPath) : longImagePathForHtml(path31.resolve(htmlPath)); + const { chromium } = await import("playwright"); + let browser = null; + try { + browser = await launchChromium(chromium); + const page = await browser.newPage({ + viewport: { + width: clampDimension(viewportWidth, DEFAULT_VIEWPORT_WIDTH, 2400), + height: clampDimension(viewportHeight, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT) + }, + deviceScaleFactor: 2 + }); + await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 3e4 }); + await page.evaluate(() => document.fonts?.ready).catch(() => null); + const size = await page.evaluate(() => ({ + width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, 320), + height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, 320) + })); + await page.setViewportSize({ + width: clampDimension(size.width, DEFAULT_VIEWPORT_WIDTH, 2400), + height: Math.min(clampDimension(size.height, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT), 2400) + }); + await fsPromises3.mkdir(path31.dirname(destination), { recursive: true }); + await page.screenshot({ + path: destination, + fullPage: true, + type: "png", + animations: "disabled", + caret: "hide" + }); + return { + outputPath: destination, + bytes: fs28.statSync(destination).size + }; + } finally { + await browser?.close().catch(() => { + }); + } +} +async function renderLongImageBuffer({ url }) { + const tempDir = await fsPromises3.mkdtemp(path31.join(os2.tmpdir(), "memind-long-image-")); + const outputPath = path31.join(tempDir, "page.long.png"); + try { + await renderLongImage({ url, outputPath }); + return await fsPromises3.readFile(outputPath); + } finally { + await fsPromises3.rm(tempDir, { recursive: true, force: true }).catch(() => { + }); + } +} + +// mindspace-docx-export.mjs +var W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; +var R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; +var CONTENT_TYPES = ` + + + + +`; +var ROOT_RELS = ` + + +`; +var DOCUMENT_RELS = ` +`; +function escapeXml3(value) { + return String(value ?? "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} +function decodeHtmlEntities(value) { + return String(value ?? "").replace(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'/gi, "'").replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16))); +} +function extractPlainTextFromHtml(html) { + return decodeHtmlEntities( + String(html ?? "").replace(//gi, " ").replace(//gi, " ").replace(/<\/(?:p|div|section|article|header|footer|h[1-6]|li|tr|blockquote)>/gi, "\n").replace(//gi, "\n").replace(/<[^>]+>/g, " ").replace(/[ \t\r\f\v]+/g, " ").replace(/\n[ \t]+/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim() + ); +} +function normalizeParagraphs(content) { + return String(content ?? "").split(/\n{1,}/).map((line) => line.replace(/^#{1,6}\s+/, "").replace(/[*_`~]/g, "").trim()).filter(Boolean).slice(0, 500); +} +function paragraphXml(text, { heading = false } = {}) { + const style = heading ? '' : ""; + const runStyle = heading ? '' : ""; + return `${style}${runStyle}${escapeXml3(text)}`; +} +function documentXml({ title, paragraphs }) { + const body = [ + title ? paragraphXml(title, { heading: true }) : "", + ...paragraphs.map((paragraph) => paragraphXml(paragraph)) + ].join(""); + return ` + + ${body} +`; +} +var CRC_TABLE = (() => { + const table = new Uint32Array(256); + for (let n = 0; n < 256; n += 1) { + let c = n; + for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1; + table[n] = c >>> 0; + } + return table; +})(); +function crc32(buffer) { + let crc = 4294967295; + for (const byte of buffer) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8; + return (crc ^ 4294967295) >>> 0; +} +function dosDateTime(date = /* @__PURE__ */ new Date()) { + const year = Math.max(1980, date.getFullYear()); + const dosTime = date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2); + const dosDate = year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate(); + return { dosTime, dosDate }; +} +function writeZipEntry(name, content, offset) { + const nameBuffer = Buffer.from(name); + const data = Buffer.isBuffer(content) ? content : Buffer.from(String(content), "utf8"); + const checksum = crc32(data); + const { dosTime, dosDate } = dosDateTime(); + const local = Buffer.alloc(30 + nameBuffer.length); + local.writeUInt32LE(67324752, 0); + local.writeUInt16LE(20, 4); + local.writeUInt16LE(0, 6); + local.writeUInt16LE(0, 8); + local.writeUInt16LE(dosTime, 10); + local.writeUInt16LE(dosDate, 12); + local.writeUInt32LE(checksum, 14); + local.writeUInt32LE(data.length, 18); + local.writeUInt32LE(data.length, 22); + local.writeUInt16LE(nameBuffer.length, 26); + nameBuffer.copy(local, 30); + const central = Buffer.alloc(46 + nameBuffer.length); + central.writeUInt32LE(33639248, 0); + central.writeUInt16LE(20, 4); + central.writeUInt16LE(20, 6); + central.writeUInt16LE(0, 8); + central.writeUInt16LE(0, 10); + central.writeUInt16LE(dosTime, 12); + central.writeUInt16LE(dosDate, 14); + central.writeUInt32LE(checksum, 16); + central.writeUInt32LE(data.length, 20); + central.writeUInt32LE(data.length, 24); + central.writeUInt16LE(nameBuffer.length, 28); + central.writeUInt32LE(offset, 42); + nameBuffer.copy(central, 46); + return { local, data, central }; +} +function zip(entries) { + let offset = 0; + const locals = []; + const centrals = []; + for (const [name, content] of entries) { + const entry = writeZipEntry(name, content, offset); + locals.push(entry.local, entry.data); + centrals.push(entry.central); + offset += entry.local.length + entry.data.length; + } + const centralSize = centrals.reduce((sum, item) => sum + item.length, 0); + const end = Buffer.alloc(22); + end.writeUInt32LE(101010256, 0); + end.writeUInt16LE(entries.length, 8); + end.writeUInt16LE(entries.length, 10); + end.writeUInt32LE(centralSize, 12); + end.writeUInt32LE(offset, 16); + return Buffer.concat([...locals, ...centrals, end]); +} +function generateDocxBuffer({ title = "\u6587\u6863", content = "", contentFormat = "markdown" } = {}) { + const plain = contentFormat === "html" ? extractPlainTextFromHtml(content) : String(content ?? ""); + const paragraphs = normalizeParagraphs(plain); + return zip([ + ["[Content_Types].xml", CONTENT_TYPES], + ["_rels/.rels", ROOT_RELS], + ["word/document.xml", documentXml({ title, paragraphs: paragraphs.length > 0 ? paragraphs : [""] })], + ["word/_rels/document.xml.rels", DOCUMENT_RELS] + ]); +} + +// mindspace-chat-docx-package.mjs import crypto25 from "node:crypto"; +var DOCX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; +async function registerChatDocxArtifactForConversation({ + registry, + user, + bundle, + requestBody = {}, + filename, + buffer, + now = Date.now() +}) { + if (!registry || !user?.id || !filename || !Buffer.isBuffer(buffer)) return null; + const sessionId = String(requestBody.session_id ?? requestBody.sessionId ?? "").trim(); + const messageId2 = String(requestBody.message_id ?? requestBody.messageId ?? "").trim(); + if (!sessionId || !messageId2) return null; + const selectedLinkIndex = Number(requestBody.selected_link_index ?? requestBody.selectedLinkIndex ?? 0); + const hash = crypto25.createHash("sha256").update(`${user.id}:${sessionId}:${messageId2}:${selectedLinkIndex}:${filename}`).digest("hex").slice(0, 16); + const artifactId = `ca_docx_${hash}`; + const relativePath = `artifacts/${artifactId}/${filename}`; + const { packageRecord, writeResult } = await registry.putObjectForSession({ + userId: user.id, + sessionId, + title: bundle?.source?.session?.name ?? null, + relativePath, + body: buffer + }); + const canonicalUrl = `/api/mindspace/v1/conversation-packages/${encodeURIComponent(sessionId)}/artifacts/${encodeURIComponent(artifactId)}/download`; + const artifact = await registry.recordArtifact({ + id: artifactId, + packageId: packageRecord.id, + artifactKind: "docx", + messageId: messageId2, + displayName: filename, + mimeType: DOCX_MIME_TYPE, + sizeBytes: buffer.length, + storageKey: writeResult.key, + canonicalUrl, + sortOrder: now, + now + }); + await registry.writeManifestForSession({ + userId: user.id, + sessionId + }); + return artifact; +} + +// billing-subscription.mjs +import crypto26 from "node:crypto"; var PLAN_CATALOG = { free: { name: "\u514D\u8D39\u7248", @@ -28267,7 +30737,7 @@ function createSubscriptionService(pool2, { getPlanAsync = null } = {}) { WHERE user_id = ? AND status = 'active'`, [now, userId] ); - const id = crypto25.randomUUID(); + const id = crypto26.randomUUID(); await conn.query( `INSERT INTO h5_subscriptions (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used, @@ -28351,7 +30821,7 @@ function createSubscriptionService(pool2, { getPlanAsync = null } = {}) { const conn = await pool2.getConnection(); try { await conn.beginTransaction(); - const newId = crypto25.randomUUID(); + const newId = crypto26.randomUUID(); await conn.query( `UPDATE h5_subscriptions SET status = 'expired', updated_at = ? WHERE id = ?`, [now, subId] @@ -28519,7 +30989,7 @@ function createSubscriptionService(pool2, { getPlanAsync = null } = {}) { WHERE user_id = ? AND status = 'active'`, [now, userId] ); - const id = crypto25.randomUUID(); + const id = crypto26.randomUUID(); const periodEnd = now + plan.periodDays * 24 * 60 * 60 * 1e3; await conn.query( `INSERT INTO h5_subscriptions @@ -28613,7 +31083,7 @@ function createSubscriptionService(pool2, { getPlanAsync = null } = {}) { `UPDATE h5_subscriptions SET status = 'expired', auto_renew = 0, updated_at = ? WHERE id = ?`, [now, sub.id] ); - const newId = crypto25.randomUUID(); + const newId = crypto26.randomUUID(); const newPeriodEnd = now + plan.periodDays * 24 * 60 * 60 * 1e3; await conn.query( `INSERT INTO h5_subscriptions @@ -28853,21 +31323,21 @@ function createPlanCatalogService(pool2) { } // wechat-pay.mjs -import crypto26 from "node:crypto"; -import fs25 from "node:fs"; +import crypto27 from "node:crypto"; +import fs29 from "node:fs"; import { fetch as fetch2 } from "undici"; var API_BASE = "https://api.mch.weixin.qq.com"; function readPrivateKey(config) { if (config.privateKeyPem) return config.privateKeyPem; - if (config.privateKeyPath && fs25.existsSync(config.privateKeyPath)) { - return fs25.readFileSync(config.privateKeyPath, "utf8"); + if (config.privateKeyPath && fs29.existsSync(config.privateKeyPath)) { + return fs29.readFileSync(config.privateKeyPath, "utf8"); } return null; } function readPlatformCert(config) { if (config.platformCertPem) return config.platformCertPem; - if (config.platformCertPath && fs25.existsSync(config.platformCertPath)) { - return fs25.readFileSync(config.platformCertPath, "utf8"); + if (config.platformCertPath && fs29.existsSync(config.platformCertPath)) { + return fs29.readFileSync(config.platformCertPath, "utf8"); } return null; } @@ -28932,12 +31402,12 @@ function loadWechatPayConfig() { }; } function randomNonce(length = 32) { - return crypto26.randomBytes(length).toString("hex").slice(0, length); + return crypto27.randomBytes(length).toString("hex").slice(0, length); } function signV2Params(params, apiKey) { const stringA = Object.keys(params).filter((key) => key !== "sign" && params[key] !== void 0 && params[key] !== "").sort().map((key) => `${key}=${params[key]}`).join("&"); const stringSignTemp = `${stringA}&key=${apiKey}`; - return crypto26.createHash("md5").update(stringSignTemp, "utf8").digest("hex").toUpperCase(); + return crypto27.createHash("md5").update(stringSignTemp, "utf8").digest("hex").toUpperCase(); } function buildXml(fields) { const parts = [""]; @@ -28969,7 +31439,7 @@ function normalizeV2Transaction(fields) { amount: fields.total_fee ? { total: Number(fields.total_fee) } : void 0 }; } -async function wechatV2Request(config, path31, fields) { +async function wechatV2Request(config, path35, fields) { const payload = { appid: config.appId, mch_id: config.mchId, @@ -28978,7 +31448,7 @@ async function wechatV2Request(config, path31, fields) { }; payload.sign = signV2Params(payload, config.apiKey); const body = buildXml(payload); - const res = await fetch2(`${API_BASE}${path31}`, { + const res = await fetch2(`${API_BASE}${path35}`, { method: "POST", headers: { "Content-Type": "text/xml" }, body @@ -28997,7 +31467,7 @@ async function wechatV2Request(config, path31, fields) { return data; } function signMessage(message, privateKeyPem) { - return crypto26.createSign("RSA-SHA256").update(message).sign(privateKeyPem, "base64"); + return crypto27.createSign("RSA-SHA256").update(message).sign(privateKeyPem, "base64"); } function buildAuthorization({ mchId, serialNo, privateKey }, method, pathWithQuery, body) { const timestamp = String(Math.floor(Date.now() / 1e3)); @@ -29020,10 +31490,10 @@ ${payload} ].join(","); return { authorization, timestamp, nonce, signature }; } -async function wechatV3Request(config, method, path31, bodyObj) { +async function wechatV3Request(config, method, path35, bodyObj) { const body = bodyObj ? JSON.stringify(bodyObj) : ""; - const { authorization } = buildAuthorization(config, method, path31, body); - const res = await fetch2(`${API_BASE}${path31}`, { + const { authorization } = buildAuthorization(config, method, path35, body); + const res = await fetch2(`${API_BASE}${path35}`, { method, headers: { Authorization: authorization, @@ -29228,7 +31698,7 @@ ${bodyText} if (!config.platformCert) { throw new Error("\u672A\u914D\u7F6E\u5FAE\u4FE1\u652F\u4ED8\u5E73\u53F0\u8BC1\u4E66\uFF0C\u65E0\u6CD5\u9A8C\u7B7E"); } - const verified = crypto26.createVerify("RSA-SHA256").update(message).verify(config.platformCert, signature, "base64"); + const verified = crypto27.createVerify("RSA-SHA256").update(message).verify(config.platformCert, signature, "base64"); if (!verified) { throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u56DE\u8C03\u9A8C\u7B7E\u5931\u8D25"); } @@ -29241,7 +31711,7 @@ ${bodyText} const cipherBuf = Buffer.from(resource.ciphertext, "base64"); const authTag = cipherBuf.subarray(cipherBuf.length - 16); const dataBuf = cipherBuf.subarray(0, cipherBuf.length - 16); - const decipher = crypto26.createDecipheriv( + const decipher = crypto27.createDecipheriv( "aes-256-gcm", Buffer.from(config.apiV3Key, "utf8"), Buffer.from(resource.nonce, "utf8") @@ -29292,7 +31762,7 @@ function createWechatPayClient(config) { var WECHAT_NOTIFY_SUCCESS_V2 = ""; // wechat-oauth.mjs -import crypto27 from "node:crypto"; +import crypto28 from "node:crypto"; import { fetch as fetch3 } from "undici"; var OAUTH_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize"; var OAUTH_QRCONNECT_URL = "https://open.weixin.qq.com/connect/qrconnect"; @@ -29434,7 +31904,7 @@ function createWechatOAuthService(pool2, config, { userAuth: userAuth2 } = {}) { authMode = "mp", now = Date.now() } = {}) => { - const state = crypto27.randomBytes(24).toString("base64url"); + const state = crypto28.randomBytes(24).toString("base64url"); if (pool2) { await pruneExpiredStates(now); await pool2.query( @@ -29695,9 +32165,9 @@ function createWechatOAuthService(pool2, config, { userAuth: userAuth2 } = {}) { } // wechat-mp.mjs -import crypto29 from "node:crypto"; -import fs27 from "node:fs"; -import path29 from "node:path"; +import crypto30 from "node:crypto"; +import fs31 from "node:fs"; +import path33 from "node:path"; import { fetch as undiciFetch7 } from "undici"; // schedule-intent.mjs @@ -29925,7 +32395,7 @@ function buildChatSkillPrompt(promptKey, skillName) { case "analyze": return "\u8BF7\u6DF1\u5165\u5206\u6790\u4EE5\u4E0B\u5185\u5BB9\uFF0C\u7ED9\u51FA\u7ED3\u6784\u5316\u89E3\u8BFB\uFF1A\u80CC\u666F\u3001\u5173\u952E\u53D1\u73B0\u3001\u98CE\u9669\u6216\u673A\u4F1A\u3001\u4EE5\u53CA\u5EFA\u8BAE\u4E0B\u4E00\u6B65\uFF1A"; case "generate-page": - return `\u8BF7\u4F7F\u7528 ${skillName ?? PUBLISH_SKILL_NAME2} \u6280\u80FD\uFF1A\u5728\u6211\u7684\u4E13\u5C5E MindSpace \u53D1\u5E03\u76EE\u5F55\u751F\u6210\u9759\u6001 HTML \u9875\u9762\uFF0C\u5E76\u7ED9\u51FA\u53EF\u516C\u7F51\u8BBF\u95EE\u7684\u5B8C\u6574\u94FE\u63A5\u3002\u5FC5\u987B\u5148 load_skill\uFF0C\u518D\u7528 write_file/edit_file \u5199\u5165 public/\u9875\u9762.html\uFF1B\u5B8C\u6210\u540E\u76F4\u63A5\u8FD4\u56DE Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 \`[\u9875\u9762\u6807\u9898](URL)\`\u3002\u7981\u6B62\u53EA\u7ED9\u672C\u5730\u8DEF\u5F84\uFF08\u5982 hello/index.html\uFF09\uFF0C\u7981\u6B62\u8BE2\u95EE\u662F\u5426\u8FD8\u8981\u53D1\u5E03\uFF0C\u9664\u975E\u5199\u6587\u4EF6\u5DE5\u5177\u5B9E\u9645\u5931\u8D25\u3002\u5982\u679C\u9875\u9762\u91CC\u8FD8\u9700\u8981\u63D0\u4F9B Word / docx \u4E0B\u8F7D\uFF0C\u5FC5\u987B\u5148 \`load_skill\` \u2192 \`docx-generate\`\uFF0C\u751F\u6210 \`public/*.docx\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u518D\u5199 HTML \u5E76\u7528\u540C\u76EE\u5F55\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\u3002`; + return `\u8BF7\u4F7F\u7528 ${skillName ?? PUBLISH_SKILL_NAME2} \u6280\u80FD\uFF1A\u5728\u6211\u7684\u4E13\u5C5E MindSpace \u53D1\u5E03\u76EE\u5F55\u751F\u6210\u9759\u6001 HTML \u9875\u9762\uFF0C\u5E76\u7ED9\u51FA\u53EF\u516C\u7F51\u8BBF\u95EE\u7684\u5B8C\u6574\u94FE\u63A5\u3002\u9ED8\u8BA4\u53EA\u751F\u6210 HTML\uFF1A\u5FC5\u987B\u5148 load_skill\uFF0C\u518D\u7528 write_file/edit_file \u5199\u5165 public/\u9875\u9762.html\uFF1B\u5B8C\u6210\u540E\u76F4\u63A5\u8FD4\u56DE Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 \`[\u9875\u9762\u6807\u9898](URL)\`\u3002\u7981\u6B62\u53EA\u7ED9\u672C\u5730\u8DEF\u5F84\uFF08\u5982 hello/index.html\uFF09\uFF0C\u7981\u6B62\u8BE2\u95EE\u662F\u5426\u8FD8\u8981\u53D1\u5E03\uFF0C\u9664\u975E\u5199\u6587\u4EF6\u5DE5\u5177\u5B9E\u9645\u5931\u8D25\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF0C\u624D\u5148 \`load_skill\` \u2192 \`docx-generate\`\uFF0C\u751F\u6210 \`public/*.docx\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u518D\u5199 HTML \u5E76\u7528\u540C\u76EE\u5F55\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\u65F6\uFF0C\u624D\u5148 \`load_skill\` \u2192 \`long-image-download\`\uFF0C\u8C03\u7528 \`generate_long_image\` \u751F\u6210 \`public/*.long.png\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u518D\u7ED9\u957F\u56FE\u9884\u89C8\u94FE\u63A5\u548C \`?download=long-image\` \u4E0B\u8F7D\u94FE\u63A5\u3002\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210\u4F34\u751F\u6587\u4EF6\u3002`; default: return ""; } @@ -29965,12 +32435,12 @@ function stripKnownChatSkillPrompt(text) { } // wechat-media.mjs -import crypto28 from "node:crypto"; -import fs26 from "node:fs"; -import path28 from "node:path"; +import crypto29 from "node:crypto"; +import fs30 from "node:fs"; +import path32 from "node:path"; import { fileURLToPath as fileURLToPath5 } from "node:url"; import { fetch as undiciFetch6 } from "undici"; -var __dirname4 = path28.dirname(fileURLToPath5(import.meta.url)); +var __dirname4 = path32.dirname(fileURLToPath5(import.meta.url)); var DEFAULT_WECHAT_MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/media/get"; var DEFAULT_MAX_IMAGE_BYTES = 10 * 1024 * 1024; var ALLOWED_IMAGE_MIME_TYPES = /* @__PURE__ */ new Map([ @@ -29987,7 +32457,7 @@ function resolveImageExtension(contentType = "", fallbackUrl = "") { extension: ALLOWED_IMAGE_MIME_TYPES.get(normalized) }; } - const ext = path28.extname(String(fallbackUrl ?? "").split("?")[0]).replace(/^\./, "").toLowerCase(); + const ext = path32.extname(String(fallbackUrl ?? "").split("?")[0]).replace(/^\./, "").toLowerCase(); if (ext === "jpg" || ext === "jpeg") return { mimeType: "image/jpeg", extension: "jpg" }; if (ext === "png") return { mimeType: "image/png", extension: "png" }; if (ext === "webp") return { mimeType: "image/webp", extension: "webp" }; @@ -30046,8 +32516,8 @@ async function persistWechatImage({ h5Root = __dirname4 } = {}) { if (!userId) throw new Error("\u7F3A\u5C11 userId"); - const publishDir = path28.join(h5Root, PUBLISH_ROOT_DIR, String(userId), PUBLIC_ZONE_DIR, "wechat-mp"); - fs26.mkdirSync(publishDir, { recursive: true }); + const publishDir = path32.join(h5Root, PUBLISH_ROOT_DIR, String(userId), PUBLIC_ZONE_DIR, "wechat-mp"); + fs30.mkdirSync(publishDir, { recursive: true }); let source = "wechat_media"; let buffer; let contentType = ""; @@ -30079,11 +32549,11 @@ async function persistWechatImage({ throw new Error(`\u6682\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B\uFF1A${contentType || "unknown"}`); } const timestamp = Date.now(); - const hash = crypto28.createHash("sha1").update(buffer).digest("hex").slice(0, 12); + const hash = crypto29.createHash("sha1").update(buffer).digest("hex").slice(0, 12); const fileBase = [appId || "wx", openid || "openid", msgId || timestamp, mediaId || hash].filter(Boolean).join("-").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 120); const filename = `${fileBase}.${resolved.extension}`; - const absolutePath = path28.join(publishDir, filename); - fs26.writeFileSync(absolutePath, buffer); + const absolutePath = path32.join(publishDir, filename); + fs30.writeFileSync(absolutePath, buffer); return { absolutePath, bytes: buffer.length, @@ -30345,7 +32815,7 @@ function deriveWechatEndpointFromUrl(sourceUrl, pathname) { } function createUserMessage2(text, metadata = {}) { return { - id: crypto29.randomUUID(), + id: crypto30.randomUUID(), role: "user", created: Math.floor(Date.now() / 1e3), content: [{ type: "text", text }], @@ -30560,7 +33030,7 @@ async function hasAnyValidPublishedHtmlLink(text, linkExists, { confirmedArtifac } const filename = String(match[2] ?? "").split("/").pop()?.trim(); if (filename && confirmedArtifacts.some((artifact) => { - const artifactName = path29.posix.basename(String(artifact.relativePath ?? "").replace(/\\/g, "/")); + const artifactName = path33.posix.basename(String(artifact.relativePath ?? "").replace(/\\/g, "/")); return artifactName === filename && artifactFileExists(artifact); })) { return true; @@ -30582,23 +33052,23 @@ function isMissingRequiredPublishSkill(reply, intent) { return !usedStaticPagePublishSkill(reply?.messages ?? []); } function buildPublicHtmlUrl(workingDir, relativePath, publicBaseUrl) { - const owner = path29.basename(path29.resolve(workingDir)); - const normalized = relativePath.split(path29.sep).map(encodeURIComponent).join("/"); - return `${String(publicBaseUrl ?? "").replace(/\/+$/, "")}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(owner)}/${normalized}`; + const owner = path33.basename(path33.resolve(workingDir)); + const normalized = relativePath.split(path33.sep).map(encodeURIComponent).join("/"); + return buildPublicUrl(publicBaseUrl, owner, normalized); } function ensurePublicHtmlArtifact(htmlPath, workingDir) { - const workspaceRoot = path29.resolve(workingDir); - const source = path29.isAbsolute(String(htmlPath ?? "")) ? path29.resolve(String(htmlPath)) : path29.resolve(workspaceRoot, String(htmlPath ?? "")); - if (source !== workspaceRoot && !source.startsWith(`${workspaceRoot}${path29.sep}`)) return null; - if (!fs27.existsSync(source) || !fs27.statSync(source).isFile()) return null; - const publicRoot = path29.join(workspaceRoot, "public"); + const workspaceRoot = path33.resolve(workingDir); + const source = path33.isAbsolute(String(htmlPath ?? "")) ? path33.resolve(String(htmlPath)) : path33.resolve(workspaceRoot, String(htmlPath ?? "")); + if (source !== workspaceRoot && !source.startsWith(`${workspaceRoot}${path33.sep}`)) return null; + if (!fs31.existsSync(source) || !fs31.statSync(source).isFile()) return null; + const publicRoot = path33.join(workspaceRoot, "public"); let publishedPath = source; - if (source !== publicRoot && !source.startsWith(`${publicRoot}${path29.sep}`)) { - fs27.mkdirSync(publicRoot, { recursive: true }); - publishedPath = path29.join(publicRoot, path29.basename(source)); - if (publishedPath !== source) fs27.copyFileSync(source, publishedPath); + if (source !== publicRoot && !source.startsWith(`${publicRoot}${path33.sep}`)) { + fs31.mkdirSync(publicRoot, { recursive: true }); + publishedPath = path33.join(publicRoot, path33.basename(source)); + if (publishedPath !== source) fs31.copyFileSync(source, publishedPath); } - const relativePath = path29.relative(workspaceRoot, publishedPath); + const relativePath = path33.relative(workspaceRoot, publishedPath); if (!relativePath || relativePath.startsWith("..")) return null; return { localPath: publishedPath, @@ -30647,14 +33117,14 @@ function collectPublicHtmlLinkFilenames(text) { const filenames = /* @__PURE__ */ new Set(); for (const match of String(text ?? "").matchAll(PUBLIC_HTML_LINK_PATTERN2)) { const relativePath = String(match[2] ?? "").trim(); - const filename = path29.posix.basename(relativePath); + const filename = path33.posix.basename(relativePath); if (filename) filenames.add(filename); } return filenames; } function collectRecentPublishedHtmlArtifacts(intent, { workingDir, publicBaseUrl, replyText = "", sinceMs = 0, limit = 20 } = {}) { - const publicRoot = path29.join(path29.resolve(workingDir), "public"); - if (!fs27.existsSync(publicRoot) || !fs27.statSync(publicRoot).isDirectory()) return []; + const publicRoot = path33.join(path33.resolve(workingDir), "public"); + if (!fs31.existsSync(publicRoot) || !fs31.statSync(publicRoot).isDirectory()) return []; const expectedTarget = extractRequestedHtmlTarget(intent?.agentText ?? intent?.content ?? ""); const expectedRelativePath = expectedTarget ? expectedTarget.replace(/^\/+/, "").replace(/\\/g, "/") : ""; const linkedFilenames = collectPublicHtmlLinkFilenames(replyText); @@ -30664,12 +33134,12 @@ function collectRecentPublishedHtmlArtifacts(intent, { workingDir, publicBaseUrl const current = stack.pop(); let entries = []; try { - entries = fs27.readdirSync(current, { withFileTypes: true }); + entries = fs31.readdirSync(current, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { - const absolutePath = path29.join(current, entry.name); + const absolutePath = path33.join(current, entry.name); if (entry.isDirectory()) { stack.push(absolutePath); continue; @@ -30677,15 +33147,15 @@ function collectRecentPublishedHtmlArtifacts(intent, { workingDir, publicBaseUrl if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".html")) continue; let stat; try { - stat = fs27.statSync(absolutePath); + stat = fs31.statSync(absolutePath); } catch { continue; } if (sinceMs > 0 && Number(stat.mtimeMs ?? 0) + 1 < sinceMs) continue; - const relativePath = path29.relative(path29.resolve(workingDir), absolutePath); + const relativePath = path33.relative(path33.resolve(workingDir), absolutePath); if (!relativePath || relativePath.startsWith("..")) continue; const normalizedRelativePath = relativePath.replace(/\\/g, "/"); - const filename = path29.posix.basename(normalizedRelativePath); + const filename = path33.posix.basename(normalizedRelativePath); if (linkedFilenames.size > 0 && filename && !linkedFilenames.has(filename)) continue; artifacts.push({ localPath: absolutePath, @@ -30824,14 +33294,14 @@ function defaultPublicHtmlLinkExists(urlText) { const owner = parts[1]; const rest = parts.slice(3); if (!owner || rest.some((part) => !part || part === "." || part === "..")) return false; - const root = path29.resolve(process.cwd(), PUBLISH_ROOT_DIR, owner, "public"); - const target = path29.resolve(root, ...rest); - if (target !== root && !target.startsWith(`${root}${path29.sep}`)) return false; - return fs27.existsSync(target) && fs27.statSync(target).isFile(); + const root = path33.resolve(process.cwd(), PUBLISH_ROOT_DIR, owner, "public"); + const target = path33.resolve(root, ...rest); + if (target !== root && !target.startsWith(`${root}${path33.sep}`)) return false; + return fs31.existsSync(target) && fs31.statSync(target).isFile(); } function createPublicHtmlLinkExists(workingDir) { - const workspaceRoot = path29.resolve(String(workingDir ?? "")); - const workspaceKey = path29.basename(workspaceRoot); + const workspaceRoot = path33.resolve(String(workingDir ?? "")); + const workspaceKey = path33.basename(workspaceRoot); return (urlText) => { let url; try { @@ -30851,10 +33321,10 @@ function createPublicHtmlLinkExists(workingDir) { const rest = parts.slice(3); if (!owner || rest.some((part) => !part || part === "." || part === "..")) return false; if (owner === workspaceKey) { - const root = path29.join(workspaceRoot, "public"); - const target = path29.resolve(root, ...rest); - if (target !== root && !target.startsWith(`${root}${path29.sep}`)) return false; - return fs27.existsSync(target) && fs27.statSync(target).isFile(); + const root = path33.join(workspaceRoot, "public"); + const target = path33.resolve(root, ...rest); + if (target !== root && !target.startsWith(`${root}${path33.sep}`)) return false; + return fs31.existsSync(target) && fs31.statSync(target).isFile(); } return defaultPublicHtmlLinkExists(urlText); }; @@ -30899,7 +33369,7 @@ function artifactFileExists(artifact) { const localPath = String(artifact?.localPath ?? "").trim(); if (!localPath) return false; try { - return fs27.existsSync(localPath) && fs27.statSync(localPath).isFile(); + return fs31.existsSync(localPath) && fs31.statSync(localPath).isFile(); } catch { return false; } @@ -30929,7 +33399,7 @@ function buildVerifiedHtmlArtifacts({ const linkedFilenames = collectPublicHtmlLinkFilenames(replyText); if (linkedFilenames.size === 0) return existing; const matched = existing.filter((artifact) => { - const filename = path29.posix.basename(String(artifact.relativePath ?? "").replace(/\\/g, "/")); + const filename = path33.posix.basename(String(artifact.relativePath ?? "").replace(/\\/g, "/")); return filename && linkedFilenames.has(filename); }); return matched; @@ -31291,7 +33761,7 @@ function loadWechatMpConfig(env = process.env) { }; } function sha1Hex(parts) { - return crypto29.createHash("sha1").update([...parts].sort().join("")).digest("hex"); + return crypto30.createHash("sha1").update([...parts].sort().join("")).digest("hex"); } function verifyWechatMpSignature({ token, timestamp, nonce, signature }) { return sha1Hex([token, timestamp, nonce]) === String(signature ?? ""); @@ -31309,7 +33779,7 @@ function decodeWechatMpAesKey(encodingAesKey) { function decryptWechatMpPayload({ encodingAesKey, appId, encrypted }) { const key = decodeWechatMpAesKey(encodingAesKey); const iv = key.subarray(0, 16); - const decipher = crypto29.createDecipheriv("aes-256-cbc", key, iv); + const decipher = crypto30.createDecipheriv("aes-256-cbc", key, iv); decipher.setAutoPadding(false); const cipherBuf = Buffer.from(String(encrypted ?? ""), "base64"); let decoded = Buffer.concat([decipher.update(cipherBuf), decipher.final()]); @@ -31428,8 +33898,8 @@ function createWechatMpService({ const fetchForSession = (sessionId, pathname, init) => sessionApiFetch ? sessionApiFetch(sessionId, pathname, init) : apiFetch2(pathname, init); const buildBindUrl = () => { if (/^https?:\/\//i.test(config.bindPath)) return config.bindPath; - const path31 = config.bindPath.startsWith("/") ? config.bindPath : `/${config.bindPath}`; - return `${config.publicBaseUrl}${path31}`; + const path35 = config.bindPath.startsWith("/") ? config.bindPath : `/${config.bindPath}`; + return `${config.publicBaseUrl}${path35}`; }; const verifyRequest = (query = {}) => verifyWechatMpSignature({ token: config.token, @@ -31494,9 +33964,9 @@ function createWechatMpService({ throw new Error("\u7B7E\u540D URL \u5FC5\u987B\u662F\u5B8C\u6574 http(s) \u5730\u5740"); } const ticket = await getJsapiTicket(); - const nonceStr = crypto29.randomBytes(12).toString("hex"); + const nonceStr = crypto30.randomBytes(12).toString("hex"); const timestamp = Math.floor(Date.now() / 1e3); - const signature = crypto29.createHash("sha1").update( + const signature = crypto30.createHash("sha1").update( [ `jsapi_ticket=${ticket}`, `noncestr=${nonceStr}`, @@ -31796,7 +34266,7 @@ function createWechatMpService({ }, config.progressDelayMs); progressTimer.unref?.(); } - const requestId = crypto29.randomUUID(); + const requestId = crypto30.randomUUID(); const guardScheduleReply = async (replyText) => { if (!scheduleService2 || !looksLikeScheduleConfirmation(replyText)) return replyText; const sourceMessageId = String(intent.msgId ?? "").trim(); @@ -31883,7 +34353,7 @@ function createWechatMpService({ }); await ensureSessionProvider(sessionId); await rememberWechatUserContext(sessionId, user); - const retryId = crypto29.randomUUID(); + const retryId = crypto30.randomUUID(); const retryStartedAt = Date.now(); const reply = await executeSessionReply( (pathname, init) => fetchForSession(sessionId, pathname, init), @@ -32011,7 +34481,7 @@ function createWechatMpService({ return { ok: false, status: 403, body: "invalid signature" }; } const inbound = parseWechatMessage(String(bodyText ?? "")); - const rawXmlHash = crypto29.createHash("sha1").update(inbound.rawXml || "").digest("hex"); + const rawXmlHash = crypto30.createHash("sha1").update(inbound.rawXml || "").digest("hex"); const intent = normalizeWechatInboundIntent(inbound); intent.appId = config.appId; if (!inbound.fromUserName || !inbound.toUserName || !inbound.msgType) { @@ -32423,7 +34893,7 @@ function validateWechatShareSignatureUrl(pageUrl, { publicBaseUrl = "", requestH } // schedule-service.mjs -import crypto30 from "node:crypto"; +import crypto31 from "node:crypto"; // schedule-time.mjs var DEFAULT_TIMEZONE2 = "Asia/Shanghai"; @@ -32719,7 +35189,7 @@ function createScheduleService(pool2, options = {}) { const cleanTitle = String(title ?? "").trim(); if (!cleanTitle) throw new Error("\u7F3A\u5C11\u5F85\u529E\u6807\u9898"); const safeKind = kind === "event" ? "event" : "task"; - const id = crypto30.randomUUID(); + const id = crypto31.randomUUID(); const ts = clock.now(); await pool2.query( `INSERT INTO h5_schedule_items @@ -32940,7 +35410,7 @@ function createScheduleService(pool2, options = {}) { [itemId, userId] ); if (!itemRows[0]) throw new Error("\u4E8B\u9879\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE"); - const id = crypto30.randomUUID(); + const id = crypto31.randomUUID(); const ts = clock.now(); await pool2.query( `INSERT INTO h5_schedule_reminders @@ -33035,7 +35505,7 @@ function createScheduleService(pool2, options = {}) { timezone: tz, now: clock.now() }); - const id = crypto30.randomUUID(); + const id = crypto31.randomUUID(); const ts = clock.now(); await pool2.query( `INSERT INTO h5_schedule_digest_subscriptions @@ -33104,7 +35574,7 @@ function createScheduleService(pool2, options = {}) { throw new Error("\u4F59\u989D\u9608\u503C\u65E0\u6548"); } const now = clock.now(); - const id = crypto30.randomUUID(); + const id = crypto31.randomUUID(); await pool2.query( `INSERT INTO h5_balance_alert_subscriptions (id, user_id, threshold_cents, channel, status, next_run_at, last_run_at, @@ -33337,7 +35807,7 @@ function createScheduleService(pool2, options = {}) { error_code, error_message, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ - crypto30.randomUUID(), + crypto31.randomUUID(), reminderId, subscriptionId, userId, @@ -33378,7 +35848,7 @@ function createScheduleService(pool2, options = {}) { if (!title) throw new Error("\u7F3A\u5C11\u901A\u77E5\u6807\u9898"); if (!body) throw new Error("\u7F3A\u5C11\u901A\u77E5\u5185\u5BB9"); const now = clock.now(); - const id = crypto30.randomUUID(); + const id = crypto31.randomUUID(); await pool2.query( `INSERT INTO h5_user_notifications (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at) @@ -33495,7 +35965,7 @@ function createScheduleService(pool2, options = {}) { } // user-feedback.mjs -import crypto31 from "node:crypto"; +import crypto32 from "node:crypto"; var FEEDBACK_TYPES = /* @__PURE__ */ new Set(["bug", "feature", "other"]); var FEEDBACK_MAX_IMAGES = 5; var FEEDBACK_MAX_TITLE = 120; @@ -33617,7 +36087,7 @@ function createFeedbackService(pool2) { const images = normalizeImages(input.images); const context = normalizeContext(input.context); const now = Date.now(); - const id = crypto31.randomUUID(); + const id = crypto32.randomUUID(); await pool2.query( `INSERT INTO h5_user_feedback (id, user_id, type, title, description, contact, status, images_json, context_json, created_at, updated_at) @@ -34185,7 +36655,7 @@ function createSessionSnapshotService(pool2, options = {}) { } // conversation-memory.mjs -import crypto32 from "node:crypto"; +import crypto33 from "node:crypto"; import { fetch as undiciFetch8 } from "undici"; import { jsonrepair as jsonrepair3 } from "jsonrepair"; var MAX_MESSAGE_TEXT = 12e3; @@ -34204,7 +36674,7 @@ function resolveEncryptionKey3(explicitKey) { return explicitKey ?? process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY ?? "local-dev-secret"; } function stableId(...parts) { - return crypto32.createHash("sha256").update(parts.map((part) => String(part ?? "")).join("\0")).digest("hex"); + return crypto33.createHash("sha256").update(parts.map((part) => String(part ?? "")).join("\0")).digest("hex"); } function parseJsonObject3(text) { const source = String(text ?? "").trim(); @@ -34515,7 +36985,7 @@ function createConversationMemoryService(pool2, options = {}) { } // experience-service.mjs -import crypto33 from "node:crypto"; +import crypto34 from "node:crypto"; function createExperienceService(pool2, options = {}) { const now = options.now ?? (() => Date.now()); const halfLifeMs = Math.max( @@ -34577,7 +37047,7 @@ ${row.body}`.toLowerCase(); const body = String(input?.body ?? "").trim(); if (!title) throw experienceError("\u7ECF\u9A8C\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A", "invalid_experience_input"); if (!body) throw experienceError("\u7ECF\u9A8C\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A", "invalid_experience_input"); - const id = crypto33.randomUUID(); + const id = crypto34.randomUUID(); const ts = now(); const scope = String(input?.scope ?? "global").trim() || "global"; const kind = String(input?.kind ?? "lesson").trim() || "lesson"; @@ -34715,10 +37185,10 @@ function attachAsrRoutes(api2, deps) { } // server.mjs -var __dirname5 = path30.dirname(fileURLToPath6(import.meta.url)); +var __dirname5 = path34.dirname(fileURLToPath6(import.meta.url)); function loadEnvFile(filePath) { - if (!fs28.existsSync(filePath)) return; - for (const line of fs28.readFileSync(filePath, "utf8").split("\n")) { + if (!fs32.existsSync(filePath)) return; + for (const line of fs32.readFileSync(filePath, "utf8").split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eq = trimmed.indexOf("="); @@ -34728,8 +37198,8 @@ function loadEnvFile(filePath) { if (!process.env[key]) process.env[key] = value; } } -loadEnvFile(path30.join(__dirname5, "../../.env.local")); -loadEnvFile(path30.join(__dirname5, ".env")); +loadEnvFile(path34.join(__dirname5, "../../.env.local")); +loadEnvFile(path34.join(__dirname5, ".env")); function parseApiTargets() { const csvTargets = (process.env.TKMIND_API_TARGETS ?? "").split(",").map((value) => value.trim()).filter(Boolean); const legacyTargets = [ @@ -34747,7 +37217,7 @@ var INTERNAL_AGENT_SECRET = process.env.MINDSPACE_INTERNAL_AGENT_SECRET ?? API_S var ACCESS_PASSWORD = process.env.H5_ACCESS_PASSWORD; var WECHAT_MP_CONFIG = loadWechatMpConfig(); var WORKSPACE_MAINTENANCE_ENABLED = process.env.MEMIND_WORKSPACE_MAINTENANCE !== "0"; -var USERS_ROOT = process.env.H5_USERS_ROOT ?? path30.join(__dirname5, "users"); +var USERS_ROOT = process.env.H5_USERS_ROOT ?? path34.join(__dirname5, "users"); var app = express2(); app.set("trust proxy", 1); var isSecureRequest = (req) => req.secure || req.get("x-forwarded-proto")?.split(",")[0]?.trim() === "https"; @@ -34787,6 +37257,7 @@ function csrfOriginCheck(req, res, next) { } app.use("/api", csrfOriginCheck); app.use("/auth", csrfOriginCheck); +app.use("/mindspace", csrfOriginCheck); var jsonBody = express2.json({ limit: "1mb" }); var jsonUnlessMultipart = (req, res, next) => { if ((req.headers["content-type"] ?? "").includes("multipart/form-data")) return next(); @@ -34796,7 +37267,7 @@ var rawUploadBody = express2.raw({ type: "application/octet-stream", limit: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES) }); -var wikiAuth = createWikiAuth(path30.join(__dirname5, PUBLISH_ROOT_DIR, "wiki-db")); +var wikiAuth = createWikiAuth(path34.join(__dirname5, PUBLISH_ROOT_DIR, "wiki-db")); var legacyAuth = null; if (ACCESS_PASSWORD && !isDatabaseConfigured()) { legacyAuth = createAuthManager({ password: ACCESS_PASSWORD }); @@ -34806,11 +37277,14 @@ if (ACCESS_PASSWORD && !isDatabaseConfigured()) { var userAuth = null; var tkmindProxy = null; var agentRunGateway = null; +var toolGateway = null; var sessionSnapshotService = null; var conversationMemoryService = null; var mindSpace = null; var mindSpaceAssets = null; var mindSpaceAudit = null; +var mindSpaceServiceFacade = null; +var mindSpaceConversationPackageRegistry = null; var mindSpacePages = null; var mindSpacePageLiveEdit = null; var mindSpaceAssetAgent = null; @@ -34858,14 +37332,34 @@ async function bootstrapUserAuth() { monthlyViewLimit: Number(process.env.MINDSPACE_FREE_MONTHLY_VIEW_LIMIT ?? 1e3), scheduleService }); + const mindSpaceStorageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"); + mindSpaceServiceFacade = createMindSpaceServiceFacade({ + storageAdapter: createLocalMindSpaceStorageAdapter(mindSpaceStorageRoot), + publicBaseUrl: resolvePublicBaseUrl(), + conversationPackageBackfill: ({ user, sessionId }) => backfillConversationPackageArtifacts({ + pool: pool2, + registry: mindSpaceConversationPackageRegistry, + storageRoot: mindSpaceStorageRoot, + h5Root: __dirname5, + user, + sessionId + }), + conversationPackagePublicHtmlHydrator: hydratePublicHtmlArtifactsForConversationPackage + }); + mindSpaceConversationPackageRegistry = createConversationPackageRegistry({ + store: createConversationPackageStore(pool2), + service: mindSpaceServiceFacade + }); mindSpaceAssets = createAssetService(pool2, { h5Root: __dirname5, - storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace"), - maxFileBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES) + storageRoot: mindSpaceStorageRoot, + maxFileBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES), + conversationPackageRegistry: mindSpaceConversationPackageRegistry }); mindSpacePages = createPageService(pool2, { h5Root: __dirname5, - storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace") + storageRoot: mindSpaceStorageRoot, + conversationPackageRegistry: mindSpaceConversationPackageRegistry }); const resolveUserIdForAgentSession = async (sessionId) => { const [rows] = await pool2.query( @@ -34884,8 +37378,9 @@ async function bootstrapUserAuth() { }); mindSpacePublications = createPublicationService(pool2, { h5Root: __dirname5, - storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace"), - publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5) + storageRoot: mindSpaceStorageRoot, + publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5), + conversationPackageRegistry: mindSpaceConversationPackageRegistry }); setInterval(async () => { try { @@ -34938,7 +37433,7 @@ async function bootstrapUserAuth() { writebackPublications }); mindSpaceCleanup = createCleanupService(pool2, { - storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace"), + storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"), h5Root: __dirname5 }); await ensurePlanCatalogSchema(pool2); @@ -34967,7 +37462,7 @@ async function bootstrapUserAuth() { } mindSpaceAgentJobs = createAgentJobService(pool2, { pageService: mindSpacePages, - storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace"), + storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"), maxOutputBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES) }); let experienceService = null; @@ -35052,9 +37547,9 @@ async function bootstrapUserAuth() { ); } if (WORKSPACE_MAINTENANCE_ENABLED) { - startWorkspaceThumbnailWatcher(path30.join(__dirname5, PUBLISH_ROOT_DIR)); + startWorkspaceThumbnailWatcher(path34.join(__dirname5, PUBLISH_ROOT_DIR)); startWorkspaceAssetSyncWatcher({ - publishRoot: path30.join(__dirname5, PUBLISH_ROOT_DIR), + publishRoot: path34.join(__dirname5, PUBLISH_ROOT_DIR), syncUserWorkspaceByDirKey: async (dirKey, options) => { let userId = dirKey; if (!PUBLISH_KEY_UUID.test(dirKey)) { @@ -35108,14 +37603,16 @@ async function bootstrapUserAuth() { conversationMemoryService, localFetchAsset: mindSpaceAssets ? async (userId, assetId) => { const { asset, path: assetPath } = await mindSpaceAssets.readAsset(userId, assetId); - const buffer = await fs28.promises.readFile(assetPath); + const buffer = await fs32.promises.readFile(assetPath); return { buffer, mimeType: asset.mimeType }; } : null }); + toolGateway = createToolGateway({ llmProviderService }); agentRunGateway = createAgentRunGateway({ pool: pool2, userAuth, tkmindProxy, + toolGateway, autoDispatch: ["1", "true", "yes", "on"].includes( String(process.env.MEMIND_AGENT_RUN_AUTODISPATCH ?? "1").trim().toLowerCase() ), @@ -35212,25 +37709,41 @@ async function attachUserSession(req, _res, next) { if (!userAuth) return next(); const token = userToken(req); req.userToken = token; - req.userSession = token ? await userAuth.verify(token) : null; + try { + req.userSession = token ? await userAuth.verify(token) : null; + req.userSessionError = null; + } catch (err) { + req.userSession = null; + req.userSessionError = err; + console.error("[Auth] session verify failed:", err instanceof Error ? err.message : err); + } next(); } app.use(attachUserSession); app.get("/auth/status", async (req, res) => { await userAuthReady; if (userAuth) { - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.json({ authenticated: false, mode: "user" }); - const row = await userAuth.getUserById(me.id); - const capabilityState = await userAuth.resolveUserCapabilities(row); - return res.json({ - authenticated: true, - user: me, - mode: "user", - capabilities: capabilityState.capabilities, - grantedSkills: capabilityState.grantedSkills ?? [], - unrestricted: capabilityState.unrestricted - }); + try { + const me = await userAuth.getMe(userToken(req)); + if (!me) return res.json({ authenticated: false, mode: "user" }); + const row = await userAuth.getUserById(me.id); + const capabilityState = await userAuth.resolveUserCapabilities(row); + return res.json({ + authenticated: true, + user: me, + mode: "user", + capabilities: capabilityState.capabilities, + grantedSkills: capabilityState.grantedSkills ?? [], + unrestricted: capabilityState.unrestricted + }); + } catch (err) { + console.error("[Auth] status failed:", err instanceof Error ? err.message : err); + return res.status(503).json({ + authenticated: false, + mode: "unavailable", + message: "\u7528\u6237\u8BA4\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" + }); + } } if (isDatabaseConfigured()) { return res.status(503).json({ @@ -36163,18 +38676,27 @@ api.use(async (req, res, next) => { } const plazaPublic = isPlazaPublicRead(req.path, req.method); if (userAuth && tkmindProxy) { - if (req.userSession) { - const me2 = await userAuth.getMe(req.userToken); - if (me2) req.currentUser = me2; + if (req.userSessionError) { + if (plazaPublic) return next(); + return res.status(503).json({ message: "\u7528\u6237\u8BA4\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" }); } - if (plazaPublic) return next(); - if (!req.userSession) { - return res.status(401).json({ message: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }); + try { + if (req.userSession) { + const me2 = await userAuth.getMe(req.userToken); + if (me2) req.currentUser = me2; + } + if (plazaPublic) return next(); + if (!req.userSession) { + return res.status(401).json({ message: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }); + } + const me = await userAuth.getMe(req.userToken); + if (!me) return res.status(401).json({ message: "\u767B\u5F55\u5DF2\u8FC7\u671F" }); + req.currentUser = me; + return next(); + } catch (err) { + console.error("[Auth] API auth failed:", err instanceof Error ? err.message : err); + return res.status(503).json({ message: "\u7528\u6237\u8BA4\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" }); } - const me = await userAuth.getMe(req.userToken); - if (!me) return res.status(401).json({ message: "\u767B\u5F55\u5DF2\u8FC7\u671F" }); - req.currentUser = me; - return next(); } if (legacyAuth?.verify(legacySessionToken(req))) return next(); return res.status(401).json({ message: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }); @@ -36199,6 +38721,22 @@ api.get("/status", async (_req, res, next) => { } return next(); }); +function runtimeEnvFlag(value, fallback = false) { + const raw = String(value ?? "").trim().toLowerCase(); + if (!raw) return fallback; + return ["1", "true", "yes", "on"].includes(raw); +} +function runtimeCsvList(value) { + return String(value ?? "").split(",").map((item) => item.trim()).filter(Boolean); +} +function runtimeCodeRunPolicyStatus() { + return { + enabled: runtimeEnvFlag(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), + userAllowlist: runtimeCsvList(process.env.MEMIND_AGENT_CODE_RUNS_USER_IDS), + taskTypeAllowlist: runtimeCsvList(process.env.MEMIND_AGENT_CODE_RUN_TASK_TYPES), + requireValidation: runtimeEnvFlag(process.env.MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION) + }; +} api.get("/runtime/status", async (_req, res) => { await userAuthReady; if (!tkmindProxy?.getRuntimeStatus) { @@ -36212,6 +38750,7 @@ api.get("/runtime/status", async (_req, res) => { if (toolQueue) { status.toolRuntime = { ...status.toolRuntime ?? {}, + codeRunPolicy: runtimeCodeRunPolicyStatus(), queue: toolQueue }; } @@ -36407,18 +38946,18 @@ api.post("/mindspace/v1/space/cleanup", async (req, res) => { return mindSpaceError(res, req, error); } }); -function isPlazaPublicRead(path31, method) { - if (!path31.startsWith("/plaza/v1/")) return false; - if (method === "POST" && path31 === "/plaza/v1/events") return true; +function isPlazaPublicRead(path35, method) { + if (!path35.startsWith("/plaza/v1/")) return false; + if (method === "POST" && path35 === "/plaza/v1/events") return true; if (method !== "GET") return false; - return path31 === "/plaza/v1/feed" || path31 === "/plaza/v1/categories" || path31 === "/plaza/v1/seo/sitemap" || /^\/plaza\/v1\/posts\/[^/]+$/.test(path31) || /^\/plaza\/v1\/posts\/[^/]+\/comments$/.test(path31) || /^\/plaza\/v1\/users\/[^/]+$/.test(path31) || /^\/plaza\/v1\/users\/[^/]+\/posts$/.test(path31); + return path35 === "/plaza/v1/feed" || path35 === "/plaza/v1/categories" || path35 === "/plaza/v1/seo/sitemap" || /^\/plaza\/v1\/posts\/[^/]+$/.test(path35) || /^\/plaza\/v1\/posts\/[^/]+\/comments$/.test(path35) || /^\/plaza\/v1\/users\/[^/]+$/.test(path35) || /^\/plaza\/v1\/users\/[^/]+\/posts$/.test(path35); } var PLAZA_SID_COOKIE = "plaza_sid"; function resolvePlazaSessionId(req, res) { const cookies = parseCookies(req.get("cookie")); let sessionId = cookies[PLAZA_SID_COOKIE]; if (!sessionId) { - sessionId = crypto35.randomUUID(); + sessionId = crypto36.randomUUID(); res.append( "Set-Cookie", `${PLAZA_SID_COOKIE}=${sessionId}; Path=/; Max-Age=31536000; SameSite=Lax; HttpOnly` @@ -36729,7 +39268,7 @@ api.get("/internal/agent/jobs/:jobId/assets/:assetId", async (req, res) => { res.set("Content-Disposition", `inline; filename="${encodeURIComponent(asset.displayName)}"`); res.set("Cache-Control", "private, no-store"); res.setHeader("X-Request-Id", req.requestId); - return res.send(await fs28.promises.readFile(asset.path)); + return res.send(await fs32.promises.readFile(asset.path)); } catch (error) { return mindSpaceError(res, req, error); } @@ -36770,6 +39309,39 @@ api.post("/internal/agent/jobs/:jobId/complete", async (req, res) => { return mindSpaceError(res, req, error); } }); +api.get("/mindspace/v1/conversation-packages/:sessionId", createGetConversationPackageHandler({ + getRegistry: () => mindSpaceConversationPackageRegistry, + getUserAuth: () => userAuth, + ensureMindSpaceEnabled, + sendData, + mindSpaceError, + beforeReadManifest: ({ req, sessionId }) => mindSpaceServiceFacade?.prepareConversationPackageRead({ + user: req.currentUser, + sessionId + }) +})); +api.get( + "/mindspace/v1/conversation-packages/:sessionId/manifest.json", + createDownloadConversationPackageManifestHandler({ + getRegistry: () => mindSpaceConversationPackageRegistry, + getUserAuth: () => userAuth, + ensureMindSpaceEnabled, + mindSpaceError, + beforeReadManifest: ({ req, sessionId }) => mindSpaceServiceFacade?.prepareConversationPackageRead({ + user: req.currentUser, + sessionId + }) + }) +); +api.get( + "/mindspace/v1/conversation-packages/:sessionId/artifacts/:artifactId/download", + createDownloadConversationPackageArtifactHandler({ + getRegistry: () => mindSpaceConversationPackageRegistry, + getUserAuth: () => userAuth, + ensureMindSpaceEnabled, + mindSpaceError + }) +); api.post("/mindspace/v1/uploads", async (req, res) => { if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req, { upload: true })) return; try { @@ -36777,7 +39349,9 @@ api.post("/mindspace/v1/uploads", async (req, res) => { categoryId: req.body?.category_id, filename: req.body?.filename, sizeBytes: req.body?.size_bytes, - declaredMimeType: req.body?.declared_mime_type + declaredMimeType: req.body?.declared_mime_type, + sourceSessionId: req.body?.session_id, + sourceMessageId: req.body?.message_id }); return sendData(res, req, upload, 201); } catch (error) { @@ -36809,6 +39383,18 @@ api.post("/mindspace/v1/uploads/:uploadId/complete", async (req, res) => { return mindSpaceError(res, req, error); } }); +api.post("/mindspace/v1/conversation-packages/:sessionId/claim-uploads", async (req, res) => { + if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req, { upload: true })) return; + try { + const result = await mindSpaceAssets.claimUploadArtifactsForConversation(req.currentUser.id, { + sessionId: req.params.sessionId, + messageId: req.body?.message_id + }); + return sendData(res, req, result); + } catch (error) { + return mindSpaceError(res, req, error); + } +}); api.delete("/mindspace/v1/uploads/:uploadId", async (req, res) => { if (!mindSpaceAssets) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); try { @@ -37010,7 +39596,7 @@ api.post("/mindspace/v1/pages/from-asset", async (req, res) => { }); } } - const content = await fs28.promises.readFile(assetPath, "utf8"); + const content = await fs32.promises.readFile(assetPath, "utf8"); const contentFormat = asset.mimeType === "text/html" ? "html" : "markdown"; const htmlRelativePath = contentFormat === "html" ? normalizeWorkspaceRelativePath2( String(asset.originalFilename ?? asset.original_filename ?? "").includes("/") ? asset.originalFilename ?? asset.original_filename : `public/${asset.originalFilename ?? asset.original_filename ?? ""}` @@ -37059,8 +39645,8 @@ api.delete("/mindspace/v1/assets/:assetId", async (req, res) => { function messageText2(message) { return (message?.content ?? []).filter((item) => item?.type === "text" && typeof item.text === "string").map((item) => item.text).join("").trim(); } -async function resolveOwnedAssistantMessage(userId, sessionId, messageId) { - if (!sessionId || !messageId) { +async function resolveOwnedAssistantMessage(userId, sessionId, messageId2) { + if (!sessionId || !messageId2) { throw Object.assign(new Error("\u7F3A\u5C11\u6765\u6E90\u4F1A\u8BDD\u6216\u6D88\u606F"), { code: "invalid_page_input" }); @@ -37075,7 +39661,7 @@ async function resolveOwnedAssistantMessage(userId, sessionId, messageId) { throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u6765\u6E90\u4F1A\u8BDD"), { code: "source_message_not_found" }); } const session = await upstream.json(); - const message = (session.conversation ?? []).find((item) => item.id === messageId); + const message = (session.conversation ?? []).find((item) => item.id === messageId2); if (!message) { throw Object.assign(new Error("\u6765\u6E90\u6D88\u606F\u4E0D\u5B58\u5728"), { code: "source_message_not_found" }); } @@ -37087,9 +39673,9 @@ async function resolveOwnedAssistantMessage(userId, sessionId, messageId) { } return { session, message, content }; } -async function resolveExistingSavedPage(userId, { sessionId, messageId, relativePath } = {}) { +async function resolveExistingSavedPage(userId, { sessionId, messageId: messageId2, relativePath } = {}) { if (!mindSpacePages || !userId) return null; - const byMessage = await mindSpacePages.findPageBySourceMessage(userId, sessionId, messageId).catch(() => null); + const byMessage = await mindSpacePages.findPageBySourceMessage(userId, sessionId, messageId2).catch(() => null); if (byMessage) return byMessage; const normalizedPath = normalizeWorkspaceRelativePath2(relativePath); if (!normalizedPath) return null; @@ -37121,11 +39707,11 @@ async function syncUserGeneratedPages(userId) { } async function resolveChatSaveBundle(user, h5Root, input = {}) { const sessionId = input.sessionId ?? input.session_id; - const messageId = input.messageId ?? input.message_id; + const messageId2 = input.messageId ?? input.message_id; const selectedLinkIndex = Number(input.selectedLinkIndex ?? input.selected_link_index ?? 0); const previewTitle = String(input.previewTitle ?? input.preview_title ?? "").trim(); const previewSummary = String(input.previewSummary ?? input.preview_summary ?? "").trim(); - const source = await resolveOwnedAssistantMessage(user.id, sessionId, messageId); + const source = await resolveOwnedAssistantMessage(user.id, sessionId, messageId2); let { analysis, resolvedHtml } = await resolveChatSaveAnalysis({ content: source.content, userId: user.id, @@ -37134,7 +39720,7 @@ async function resolveChatSaveBundle(user, h5Root, input = {}) { selectedLinkIndex }); if (resolvedHtml?.content && resolvedHtml.relativePath && authPool) { - const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace"); + const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"); let htmlContent = resolvedHtml.content; const repaired = await repairMissingHtmlAssetReferences({ pool: authPool, @@ -37159,8 +39745,8 @@ async function resolveChatSaveBundle(user, h5Root, input = {}) { } if (htmlContent !== resolvedHtml.content) { const publishDir = resolvePublishDir(h5Root, { id: user.id }); - await fsPromises3.writeFile( - path30.join(publishDir, resolvedHtml.relativePath), + await fsPromises4.writeFile( + path34.join(publishDir, resolvedHtml.relativePath), htmlContent, "utf8" ); @@ -37174,10 +39760,10 @@ async function resolveChatSaveBundle(user, h5Root, input = {}) { selectedLinkIndex, previewTitle, previewSummary, - previewFrameUrl: resolvedHtml != null ? buildChatSavePreviewFrameUrl({ sessionId, messageId, selectedLinkIndex }) : null, + previewFrameUrl: resolvedHtml != null ? buildChatSavePreviewFrameUrl({ sessionId, messageId: messageId2, selectedLinkIndex }) : null, thumbnailUrl: resolvedHtml != null ? buildChatSaveThumbnailUrl({ sessionId, - messageId, + messageId: messageId2, selectedLinkIndex, previewTitle, previewSummary @@ -37186,6 +39772,142 @@ async function resolveChatSaveBundle(user, h5Root, input = {}) { localThumbnailUrl: resolvedHtml?.relativePath != null ? buildWorkspaceThumbnailUrl(user.id, resolvedHtml.relativePath) : null }; } +async function registerPublishedLongImageArtifactForConversation({ + result, + image, + canonicalUrl +}) { + const ownerId = String(result?.ownerId ?? "").trim(); + const pageSource = result?.pageSource ?? {}; + const sessionId = String(pageSource.sourceSessionId ?? "").trim(); + const messageId2 = String(pageSource.sourceMessageId ?? "").trim(); + const publicationId = String(result?.publication?.id ?? "").trim(); + if (!mindSpaceConversationPackageRegistry || !ownerId || !sessionId || !publicationId || !Buffer.isBuffer(image)) { + return; + } + try { + const hash = crypto36.createHash("sha256").update(`${ownerId}:${sessionId}:${publicationId}:long-image`).digest("hex").slice(0, 16); + const artifactId = `ca_long_image_${hash}`; + const filename = `${publicationId || "published-page"}.long.png`; + const relativePath = `artifacts/${artifactId}/${filename}`; + const now = Date.now(); + const { packageRecord, writeResult } = await mindSpaceConversationPackageRegistry.putObjectForSession({ + userId: ownerId, + sessionId, + title: pageSource.title ?? null, + relativePath, + body: image + }); + await mindSpaceConversationPackageRegistry.recordArtifact({ + id: artifactId, + packageId: packageRecord.id, + artifactKind: "long_image", + role: "assistant", + pageId: pageSource.pageId ?? result?.publication?.pageId ?? null, + publicationId, + messageId: messageId2 || null, + displayName: filename, + mimeType: "image/png", + sizeBytes: image.length, + storageKey: writeResult.key, + canonicalUrl, + sortOrder: now, + now + }); + await mindSpaceConversationPackageRegistry.writeManifestForSession({ + userId: ownerId, + sessionId + }); + } catch (error) { + console.warn("[MindSpace] failed to record published long image artifact:", error?.message ?? error); + } +} +async function registerPublicHtmlArtifactsForConversation({ + user, + publishDir, + sessionId, + title = null, + relativePaths = [], + artifactRefs = [] +}) { + if (!mindSpaceConversationPackageRegistry || !user?.id || !publishDir || !sessionId || (!Array.isArray(relativePaths) || relativePaths.length === 0) && (!Array.isArray(artifactRefs) || artifactRefs.length === 0)) { + return []; + } + try { + const publishRoot = path34.resolve(publishDir); + const packageTitle = String(title ?? "").trim() || null; + const existingManifest = await mindSpaceConversationPackageRegistry.readManifestForSession({ userId: user.id, sessionId }).catch(() => null); + const packageRecord = existingManifest?.packageId && existingManifest.title ? { id: existingManifest.packageId } : await mindSpaceConversationPackageRegistry.ensurePackage({ + userId: user.id, + sessionId, + title: packageTitle ?? existingManifest?.title ?? null + }); + const recorded = []; + const now = Date.now(); + const refs = Array.isArray(artifactRefs) && artifactRefs.length > 0 ? artifactRefs : relativePaths.map((relativePath) => ({ relativePath })); + for (const ref of refs) { + const relativePath = typeof ref === "string" ? ref : ref?.relativePath; + const normalized = normalizePublicHtmlRelativePath(relativePath); + if (!normalized || !normalized.toLowerCase().endsWith(".html")) continue; + const absolutePath = path34.resolve(publishRoot, normalized); + if (absolutePath !== publishRoot && !absolutePath.startsWith(`${publishRoot}${path34.sep}`)) continue; + let stat = null; + try { + stat = await fsPromises4.stat(absolutePath); + } catch { + continue; + } + if (!stat.isFile()) continue; + const hash = crypto36.createHash("sha256").update(`${user.id}:${sessionId}:${normalized}`).digest("hex").slice(0, 16); + const artifactId = `ca_public_html_${hash}`; + await mindSpaceConversationPackageRegistry.recordArtifact({ + id: artifactId, + packageId: packageRecord.id, + artifactKind: "public_html", + role: "assistant", + messageId: typeof ref?.messageId === "string" && ref.messageId.trim() ? ref.messageId.trim() : null, + displayName: path34.posix.basename(normalized), + mimeType: "text/html", + sizeBytes: stat.size, + canonicalUrl: buildPublicUrl(resolvePublicBaseUrl(), user.id, normalized), + sortOrder: Number.isFinite(stat.mtimeMs) ? Math.round(stat.mtimeMs) : now, + now + }); + recorded.push({ artifactId, relativePath: normalized }); + } + if (recorded.length > 0) { + await mindSpaceConversationPackageRegistry.writeManifestForSession({ + userId: user.id, + sessionId + }); + } + return recorded; + } catch (error) { + console.warn("[MindSpace] failed to record public html artifacts:", error?.message ?? error); + return []; + } +} +async function hydratePublicHtmlArtifactsForConversationPackage({ + user, + sessionId +}) { + if (!sessionSnapshotService?.isEnabled?.() || !user?.id || !sessionId) return []; + const publishDir = resolvePublishDir(__dirname5, { id: user.id }); + const snapshot = await sessionSnapshotService.get(sessionId).catch(() => null); + const messages = Array.isArray(snapshot?.messages) ? snapshot.messages : []; + const artifactRefs = collectOwnPublicHtmlArtifactRefs({ + messages, + currentUser: user, + publishDir + }); + return registerPublicHtmlArtifactsForConversation({ + user, + publishDir, + sessionId, + title: snapshot?.session?.name, + artifactRefs + }); +} api.get("/mindspace/v1/pages/chat-save-preview", async (req, res) => { if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); try { @@ -37221,7 +39943,7 @@ api.get("/mindspace/v1/pages/chat-save-thumbnail", async (req, res) => { { title, subtitle, force: true } ).catch(() => { }); - const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace"); + const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"); const resolveAssetDataUri = createAssetDataUriResolver( authPool, storageRoot, @@ -37230,7 +39952,7 @@ api.get("/mindspace/v1/pages/chat-save-thumbnail", async (req, res) => { const svg = await generateHtmlThumbnail(publishDir, thumbRel, bundle.resolvedHtml.content, { title, subtitle, - contentBaseDir: path30.dirname(bundle.resolvedHtml.absolute), + contentBaseDir: path34.dirname(bundle.resolvedHtml.absolute), resolveAssetDataUri, force: true }); @@ -37246,7 +39968,7 @@ api.post("/mindspace/v1/pages/analyze-chat-save", async (req, res) => { if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); try { const sessionId = req.body?.session_id; - const messageId = req.body?.message_id; + const messageId2 = req.body?.message_id; const bundle = await resolveChatSaveBundle(req.currentUser, __dirname5, req.body); const { source, @@ -37275,7 +39997,7 @@ api.post("/mindspace/v1/pages/analyze-chat-save", async (req, res) => { } const existingPage = await resolveExistingSavedPage(req.currentUser.id, { sessionId, - messageId, + messageId: messageId2, relativePath: resolvedHtml?.relativePath ?? analysis.relativePath }); return sendData(res, req, { @@ -37308,7 +40030,7 @@ api.post("/mindspace/v1/pages/quick-share-from-chat", async (req, res) => { if (!bundle.resolvedHtml) { throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u94FE\u63A5\u9875\u9762\u5185\u5BB9"), { code: "static_page_not_found" }); } - const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path30.join(__dirname5, "data", "mindspace"); + const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"); const { html: localizedHtml } = await inlinePrivateAssetsInHtml( authPool, storageRoot, @@ -37316,22 +40038,56 @@ api.post("/mindspace/v1/pages/quick-share-from-chat", async (req, res) => { bundle.resolvedHtml.content ); const publishDir = resolvePublishDir(__dirname5, req.currentUser); - const sharedDir = path30.join(publishDir, PUBLIC_ZONE_DIR, "shared"); - await fsPromises3.mkdir(sharedDir, { recursive: true }); + const sharedDir = path34.join(publishDir, PUBLIC_ZONE_DIR, "shared"); + await fsPromises4.mkdir(sharedDir, { recursive: true }); const basename = bundle.resolvedHtml.filename.replace(/\.html$/i, ""); - const filename = `${basename}-${crypto35.randomUUID().slice(0, 8)}.html`; + const filename = `${basename}-${crypto36.randomUUID().slice(0, 8)}.html`; const sharedRelativePath = `${PUBLIC_ZONE_DIR}/shared/${filename}`; const sharedHtml = rewriteWorkspacePublicAssetReferences(localizedHtml, sharedRelativePath); - const destPath = path30.join(sharedDir, filename); - await fsPromises3.writeFile(destPath, sharedHtml, "utf8"); + const destPath = path34.join(sharedDir, filename); + await fsPromises4.writeFile(destPath, sharedHtml, "utf8"); const publishKey = req.currentUser.id; - const publicUrl2 = `${resolvePublicBaseUrl()}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(publishKey)}/${sharedRelativePath.split("/").map((part) => encodeURIComponent(part)).join("/")}`; + const publicUrl2 = buildPublicUrl(resolvePublicBaseUrl(), publishKey, sharedRelativePath); return res.status(201).json({ data: { publicUrl: publicUrl2, filename } }); } catch (error) { console.error("[quick-share] error:", error); return mindSpaceError(res, req, error); } }); +async function handleChatSaveDocx(req, res) { + if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); + try { + const bundle = await resolveChatSaveBundle(req.currentUser, __dirname5, req.body); + const title = bundle.previewTitle || bundle.resolvedHtml?.suggestedTitle || bundle.analysis?.suggestedTitle || "AI \u521B\u4F5C\u6587\u6863"; + const content = bundle.resolvedHtml?.content ?? bundle.source.content; + const contentFormat = bundle.resolvedHtml?.content ? "html" : "markdown"; + const buffer = generateDocxBuffer({ title, content, contentFormat }); + const filenameBase = String(title).replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "mindspace-document"; + const filename = `${filenameBase}.docx`; + await registerChatDocxArtifactForConversation({ + registry: mindSpaceConversationPackageRegistry, + user: req.currentUser, + bundle, + requestBody: req.body, + filename, + buffer + }).catch((error) => { + console.warn("[MindSpace] failed to record chat docx artifact:", error?.message ?? error); + }); + res.set("Content-Type", DOCX_MIME_TYPE); + res.set( + "Content-Disposition", + `attachment; filename="${encodeURIComponent(filename)}"; filename*=UTF-8''${encodeURIComponent(filename)}` + ); + res.set("Cache-Control", "no-store"); + res.setHeader("X-Request-Id", req.requestId); + return res.send(buffer); + } catch (error) { + return mindSpaceError(res, req, error); + } +} +api.post("/mindspace/v1/pages/chat-save-docx", handleChatSaveDocx); +api.post("/v1/pages/chat-save-docx", handleChatSaveDocx); api.post("/mindspace/v1/pages/save-from-chat", async (req, res) => { if (!mindSpacePages || !mindSpaceAssets) { return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); @@ -38480,7 +41236,15 @@ api.get("/sessions/:sessionId/events", async (req, res, next) => { messages, currentUser: req.currentUser, publishDir, - syncWorkspaceAssets: WORKSPACE_MAINTENANCE_ENABLED && mindSpaceAssets ? (userId, options) => mindSpaceAssets.syncWorkspaceAssets(userId, options) : null + sessionId: sid, + syncWorkspaceAssets: WORKSPACE_MAINTENANCE_ENABLED && mindSpaceAssets ? (userId, options) => mindSpaceAssets.syncWorkspaceAssets(userId, options) : null, + registerPublicHtmlArtifacts: (_userId, options) => registerPublicHtmlArtifactsForConversation({ + user: req.currentUser, + publishDir, + sessionId: options?.sessionId, + relativePaths: options?.relativePaths, + artifactRefs: options?.artifactRefs + }) }); if (Array.isArray(syncResult?.docxSync?.missing) && syncResult.docxSync.missing.length > 0) { console.warn( @@ -38551,6 +41315,11 @@ api.use( }) ); app.use("/api", api); +app.use(`/${PUBLISH_ROOT_DIR}`, async (req, res, next) => { + await userAuthReady; + return serveUserPublishFile(req, res, next); +}); +app.use("/mindspace", api); function scriptSrcDirective({ inline = false, urls = [], hashes = [] } = {}) { const parts = []; if (inline) parts.push("'unsafe-inline'"); @@ -38574,8 +41343,8 @@ function publishedPageCsp(html, { embed = false, raw = false, wechatShare = fals } return "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'"; } -var PUBLIC_FILE_SHARE_SCRIPT = `(function(){var root=document.querySelector('[data-mindspace-public-share]');if(!root)return;var button=root.querySelector('button');var status=root.querySelector('small');var timer=null;function setStatus(text,err){if(!status)return;status.textContent=text||'';status.classList.toggle('is-error',!!err);if(timer)clearTimeout(timer);if(text)timer=setTimeout(function(){status.textContent='';status.classList.remove('is-error');},2200);}function fallbackCopy(text){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();var ok=document.execCommand('copy');ta.remove();if(!ok)throw new Error('\u590D\u5236\u5931\u8D25');}async function copyText(text){if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(text);return;}catch(e){}}fallbackCopy(text);}button&&button.addEventListener('click',async function(){var url=location.href.split('#')[0];var title=document.title||'MindSpace';try{if(navigator.share){await navigator.share({title:title,url:url});setStatus('\u5DF2\u6253\u5F00\u5206\u4EAB');return;}await copyText(url);setStatus('\u94FE\u63A5\u5DF2\u590D\u5236');}catch(e){setStatus(e&&e.message?e.message:'\u5206\u4EAB\u5931\u8D25',true);}});})();`; -var PUBLIC_FILE_SHARE_SCRIPT_HASH = crypto35.createHash("sha256").update(PUBLIC_FILE_SHARE_SCRIPT).digest("base64"); +var PUBLIC_FILE_SHARE_SCRIPT = `(function(){var root=document.querySelector('[data-mindspace-public-share]');if(!root)return;var shareButton=root.querySelector('[data-action="share"]');var captureButton=root.querySelector('[data-action="capture"]');var status=root.querySelector('small');var timer=null;function setStatus(text,err){if(!status)return;status.textContent=text||'';status.classList.toggle('is-error',!!err);if(timer)clearTimeout(timer);if(text)timer=setTimeout(function(){status.textContent='';status.classList.remove('is-error');},2200);}function cleanUrl(){var url=new URL(location.href);url.hash='';url.searchParams.delete('download');url.searchParams.delete('export');return url.toString();}function withParam(url,key,value){var next=new URL(url);next.searchParams.set(key,value);return next.toString();}function fallbackCopy(text){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();var ok=document.execCommand('copy');ta.remove();if(!ok)throw new Error('\u590D\u5236\u5931\u8D25');}async function copyText(text){if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(text);return;}catch(e){}}fallbackCopy(text);}shareButton&&shareButton.addEventListener('click',async function(){var url=cleanUrl();var title=document.title||'MindSpace';try{if(navigator.share){await navigator.share({title:title,url:url});setStatus('\u5DF2\u6253\u5F00\u5206\u4EAB');return;}await copyText(url);setStatus('\u94FE\u63A5\u5DF2\u590D\u5236');}catch(e){setStatus(e&&e.message?e.message:'\u5206\u4EAB\u5931\u8D25',true);}});captureButton&&captureButton.addEventListener('click',function(){setStatus('\u6B63\u5728\u751F\u6210\u957F\u56FE\uFF0C\u8BF7\u7A0D\u5019');var link=document.createElement('a');link.href=withParam(cleanUrl(),'download','long-image');link.download='';document.body.appendChild(link);link.click();link.remove();});})();`; +var PUBLIC_FILE_SHARE_SCRIPT_HASH = crypto36.createHash("sha256").update(PUBLIC_FILE_SHARE_SCRIPT).digest("base64"); function injectPublicFileShareButton(html) { const source = String(html ?? ""); if (!source || source.includes("data-mindspace-public-share")) { @@ -38584,14 +41353,16 @@ function injectPublicFileShareButton(html) { const markup = ` -
+
`; if (/<\/body>/i.test(source)) { return { @@ -38618,6 +41389,15 @@ function appendQueryParam(url, key, value) { const separator = url.includes("?") ? "&" : "?"; return `${url}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`; } +function removeQueryParam(url, key) { + if (!url || !url.includes("?")) return url; + const [base, queryAndHash] = url.split("?", 2); + const [query, hash = ""] = queryAndHash.split("#", 2); + const params = new URLSearchParams(query); + params.delete(key); + const nextQuery = params.toString(); + return `${base}${nextQuery ? `?${nextQuery}` : ""}${hash ? `#${hash}` : ""}`; +} function resolveRequestOrigin(req) { const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim(); if (!host) return ""; @@ -38630,18 +41410,19 @@ function detectPublishedPageTitle(html) { const match = String(html ?? "").match(/]*>([^<]+)<\/title>/i); return match?.[1]?.replace(/\s+/g, " ").trim() || "MindSpace \u9875\u9762"; } -function publishedPageShellHtml({ iframeUrl, shareUrl, title }) { +function publishedPageShellHtml({ iframeUrl, shareUrl, title, longImageUrl }) { const iframeSrc = escapePublicHtml(iframeUrl); const safeShareUrl = escapePublicHtml(shareUrl); const safeTitle = escapePublicHtml(title); const serializedShareUrl = JSON.stringify(shareUrl).replace(/ - + ${safeTitle}