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 0000000..072f9ab
Binary files /dev/null and b/.runtime/portal/public/brand/tkmind-icon.png differ
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 智趣」小图标应同时出现。
+
+
+
连云港三天两夜攻略 🌊
+
三天两夜·沙滩酒店·必吃美食
+
+
+
+
+
+
+
+
\ 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}:
`).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(/([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16)));
+}
+function extractPlainTextFromHtml(html) {
+ return decodeHtmlEntities(
+ String(html ?? "").replace(/`;
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}