diff --git a/.runtime/portal/RUNBOOK.txt b/.runtime/portal/RUNBOOK.txt
deleted file mode 100644
index 478b5fc..0000000
--- a/.runtime/portal/RUNBOOK.txt
+++ /dev/null
@@ -1,63 +0,0 @@
-Portal runtime artifact
-
-This directory is meant to run on production without the source tree.
-Required persisted items to inherit on the host:
- .env
- MindSpace/
- data/
- users/
- .tailscale/
- public/plaza-covers/
- logs/
-
-Bundled alongside server.mjs (required for sandbox-fs MCP):
- mindspace-sandbox-mcp.mjs (esbuild bundle; includes schedule-service deps)
-
-Post-deploy validation (scripts/check-mindspace-public-links.mjs):
- Scans MindSpace/*/public/*.html for missing relative href/src/cover targets.
-
-Runtime install mode: bundle-node-modules
-Bundle target: node24
-
-Platform agent skills (synced into user workspaces):
- skills/
-Key runtime differences must stay in .env, not in the artifact:
- DATABASE_URL / MYSQL_*
- H5_PUBLIC_BASE_URL
- TKMIND_API_TARGETS / TKMIND_API_TARGET / TKMIND_API_TARGET_1
- H5_USERS_ROOT / MINDSPACE_STORAGE_ROOT / MEMIND_SHARED_PUBLISH_ROOT
-
-Deployment and operations transport:
- H5 public domain: mm.tkmind.cn
- Current public path: mm.tkmind.cn -> local nginx -> Portal :8081
- Legacy rollback-only path: m.tkmind.cn -> 105 nginx -> reverse SSH tunnel -> Portal :8081
- Future H5 traffic must not depend on 105 forwarding unless explicitly rolling back.
-
-Streaming runtime operations:
- node scripts/check-stream-runtime.mjs
- node scripts/runtime-worker-drain.mjs status
- node scripts/runtime-worker-drain.mjs reconcile
- node scripts/runtime-worker-drain.mjs reconcile --apply
- 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
- node scripts/check-agent-code-run-entry.mjs
- 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
deleted file mode 100644
index ce459dc..0000000
--- a/.runtime/portal/mindspace-public-links.mjs
+++ /dev/null
@@ -1,151 +0,0 @@
-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
deleted file mode 100755
index f190dd5..0000000
--- a/.runtime/portal/mindspace-sandbox-mcp.mjs
+++ /dev/null
@@ -1,1821 +0,0 @@
-#!/usr/bin/env node
-import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
-
-// mindspace-sandbox-mcp.mjs
-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";
-
-// schedule-service.mjs
-import crypto from "node:crypto";
-
-// schedule-time.mjs
-var DEFAULT_TIMEZONE = "Asia/Shanghai";
-function pad2(value) {
- return String(value).padStart(2, "0");
-}
-function normalizeTimezone(timezone) {
- return String(timezone || process.env.H5_DEFAULT_TIMEZONE || DEFAULT_TIMEZONE).trim() || DEFAULT_TIMEZONE;
-}
-function getLocalParts(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) {
- const formatter = new Intl.DateTimeFormat("en-CA", {
- timeZone: normalizeTimezone(timezone),
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- hour12: false
- });
- const parts = Object.fromEntries(
- formatter.formatToParts(new Date(epochMs)).map((part) => [part.type, part.value])
- );
- return {
- year: Number(parts.year),
- month: Number(parts.month),
- day: Number(parts.day),
- hour: Number(parts.hour === "24" ? 0 : parts.hour),
- minute: Number(parts.minute),
- second: Number(parts.second)
- };
-}
-function localDateLabel(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) {
- const parts = getLocalParts(epochMs, timezone);
- return `${parts.month}\u6708${parts.day}\u65E5`;
-}
-function startOfLocalDay(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) {
- const parts = getLocalParts(epochMs, timezone);
- return zonedTimeToEpochMs(
- {
- year: parts.year,
- month: parts.month,
- day: parts.day,
- hour: 0,
- minute: 0,
- second: 0
- },
- timezone
- );
-}
-function addLocalDays(epochMs, days, timezone = DEFAULT_TIMEZONE) {
- const parts = getLocalParts(epochMs, timezone);
- const utc = Date.UTC(parts.year, parts.month - 1, parts.day + Number(days || 0), parts.hour, parts.minute, parts.second);
- const shifted = new Date(utc);
- return zonedTimeToEpochMs(
- {
- year: shifted.getUTCFullYear(),
- month: shifted.getUTCMonth() + 1,
- day: shifted.getUTCDate(),
- hour: shifted.getUTCHours(),
- minute: shifted.getUTCMinutes(),
- second: shifted.getUTCSeconds()
- },
- timezone
- );
-}
-function zonedTimeToEpochMs(parts, timezone = DEFAULT_TIMEZONE) {
- const tz = normalizeTimezone(timezone);
- let guess = Date.UTC(
- Number(parts.year),
- Number(parts.month) - 1,
- Number(parts.day),
- Number(parts.hour ?? 0),
- Number(parts.minute ?? 0),
- Number(parts.second ?? 0),
- 0
- );
- for (let i = 0; i < 3; i += 1) {
- const actual = getLocalParts(guess, tz);
- const actualAsUtc = Date.UTC(
- actual.year,
- actual.month - 1,
- actual.day,
- actual.hour,
- actual.minute,
- actual.second,
- 0
- );
- const desiredAsUtc = Date.UTC(
- Number(parts.year),
- Number(parts.month) - 1,
- Number(parts.day),
- Number(parts.hour ?? 0),
- Number(parts.minute ?? 0),
- Number(parts.second ?? 0),
- 0
- );
- const delta = actualAsUtc - desiredAsUtc;
- if (delta === 0) break;
- guess -= delta;
- }
- return guess;
-}
-function nextDailyRunAt({ hour, minute = 0, timezone = DEFAULT_TIMEZONE, now = Date.now() }) {
- const tz = normalizeTimezone(timezone);
- const todayStart = startOfLocalDay(now, tz);
- const todayParts = getLocalParts(todayStart, tz);
- let runAt = zonedTimeToEpochMs(
- {
- year: todayParts.year,
- month: todayParts.month,
- day: todayParts.day,
- hour: Number(hour),
- minute: Number(minute),
- second: 0
- },
- tz
- );
- if (runAt <= now) {
- const tomorrowStart = addLocalDays(todayStart, 1, tz);
- const tomorrowParts = getLocalParts(tomorrowStart, tz);
- runAt = zonedTimeToEpochMs(
- {
- year: tomorrowParts.year,
- month: tomorrowParts.month,
- day: tomorrowParts.day,
- hour: Number(hour),
- minute: Number(minute),
- second: 0
- },
- tz
- );
- }
- return runAt;
-}
-function formatLocalTime(epochMs, timezone = DEFAULT_TIMEZONE) {
- const parts = getLocalParts(epochMs, timezone);
- return `${pad2(parts.hour)}:${pad2(parts.minute)}`;
-}
-function parseLocalDateTimeString(value, timezone = DEFAULT_TIMEZONE) {
- const raw = String(value ?? "").trim();
- if (!raw) return null;
- const match = raw.match(
- /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/
- );
- if (!match) {
- throw new Error(`\u65E0\u6CD5\u89E3\u6790\u672C\u5730\u65F6\u95F4\u300C${raw}\u300D\uFF0C\u8BF7\u4F7F\u7528 YYYY-MM-DD HH:mm`);
- }
- return zonedTimeToEpochMs(
- {
- year: Number(match[1]),
- month: Number(match[2]),
- day: Number(match[3]),
- hour: Number(match[4] ?? 0),
- minute: Number(match[5] ?? 0),
- second: Number(match[6] ?? 0)
- },
- timezone
- );
-}
-function resolveScheduleTimestamp({
- epochMs = null,
- localString = null,
- timezone = DEFAULT_TIMEZONE,
- fieldName = "\u65F6\u95F4",
- now = Date.now()
-} = {}) {
- const local = String(localString ?? "").trim();
- if (local) return parseLocalDateTimeString(local, timezone);
- if (epochMs == null || epochMs === "") return null;
- return assertReasonableScheduleEpoch(epochMs, { now, fieldName });
-}
-function assertReasonableScheduleEpoch(epochMs, { now = Date.now(), fieldName = "\u65F6\u95F4", maxPastDays = 2, maxFutureDays = 400 } = {}) {
- const safe = Number(epochMs);
- if (!Number.isFinite(safe) || safe <= 0) {
- throw new Error(`${fieldName}\u65E0\u6548\uFF0C\u8BF7\u6539\u7528 YYYY-MM-DD HH:mm \u7684 local \u5B57\u6BB5`);
- }
- const min = now - maxPastDays * 864e5;
- const max = now + maxFutureDays * 864e5;
- if (safe < min || safe > max) {
- const label = new Intl.DateTimeFormat("zh-CN", {
- timeZone: normalizeTimezone(process.env.H5_DEFAULT_TIMEZONE),
- dateStyle: "short",
- timeStyle: "short"
- }).format(new Date(safe));
- throw new Error(
- `${fieldName}\uFF08${label}\uFF09\u8D85\u51FA\u5408\u7406\u8303\u56F4\u3002\u8BF7\u6539\u7528 startLocal/remindLocal\uFF08YYYY-MM-DD HH:mm\uFF0C\u7528\u6237\u65F6\u533A\uFF09\uFF0C\u7981\u6B62\u81EA\u884C\u4F30\u7B97 Unix \u6BEB\u79D2\u3002`
- );
- }
- return safe;
-}
-
-// schedule-service.mjs
-var DEFAULT_TIMEZONE2 = "Asia/Shanghai";
-function nowMs() {
- return Date.now();
-}
-function rowToItem(row) {
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- kind: row.kind,
- title: row.title,
- description: row.description ?? null,
- status: row.status,
- startAt: row.start_at == null ? null : Number(row.start_at),
- endAt: row.end_at == null ? null : Number(row.end_at),
- dueAt: row.due_at == null ? null : Number(row.due_at),
- allDay: Boolean(row.all_day),
- timezone: row.timezone || DEFAULT_TIMEZONE2,
- location: row.location ?? null,
- createdAt: Number(row.created_at),
- updatedAt: Number(row.updated_at)
- };
-}
-function rowToDigest(row) {
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- hour: Number(row.hour),
- minute: Number(row.minute),
- timezone: row.timezone || DEFAULT_TIMEZONE2,
- channel: row.channel || "wechat",
- status: row.status,
- nextRunAt: Number(row.next_run_at),
- lastRunAt: row.last_run_at == null ? null : Number(row.last_run_at),
- attempts: Number(row.attempts ?? 0)
- };
-}
-function rowToBalanceAlert(row) {
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- thresholdCents: Number(row.threshold_cents),
- channel: row.channel || "wechat",
- status: row.status,
- nextRunAt: Number(row.next_run_at),
- lastRunAt: row.last_run_at == null ? null : Number(row.last_run_at),
- lastNotifiedBalanceCents: row.last_notified_balance_cents == null ? null : Number(row.last_notified_balance_cents),
- attempts: Number(row.attempts ?? 0),
- lastError: row.last_error ?? null,
- lockedUntil: row.locked_until == null ? null : Number(row.locked_until),
- sourceChannel: row.source_channel ?? null,
- sourceSessionId: row.source_session_id ?? null,
- sourceMessageId: row.source_message_id ?? null,
- sourceText: row.source_text ?? null
- };
-}
-function rowToReminder(row) {
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- itemId: row.item_id,
- remindAt: Number(row.remind_at),
- offsetMinutes: row.offset_minutes == null ? null : Number(row.offset_minutes),
- channel: row.channel || "wechat",
- status: row.status,
- attempts: Number(row.attempts ?? 0),
- lastError: row.last_error ?? null,
- lockedUntil: row.locked_until == null ? null : Number(row.locked_until),
- sentAt: row.sent_at == null ? null : Number(row.sent_at),
- createdAt: Number(row.created_at),
- updatedAt: Number(row.updated_at)
- };
-}
-function rowToReminderWithItem(row) {
- const reminder = rowToReminder(row);
- if (!reminder) return null;
- return {
- ...reminder,
- itemTitle: String(row.item_title ?? "").trim(),
- itemKind: row.item_kind === "event" ? "event" : "task",
- itemTimezone: row.item_timezone || DEFAULT_TIMEZONE2,
- itemStartAt: row.item_start_at == null ? null : Number(row.item_start_at),
- itemEndAt: row.item_end_at == null ? null : Number(row.item_end_at)
- };
-}
-function parseJsonColumn(value) {
- if (value == null || value === "") return null;
- if (typeof value === "string") {
- try {
- return JSON.parse(value);
- } catch {
- return null;
- }
- }
- if (typeof value === "object") return value;
- return null;
-}
-function rowToUserNotification(row) {
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- channel: row.channel,
- notificationType: row.notification_type,
- title: row.title,
- body: row.body,
- data: parseJsonColumn(row.data_json),
- status: row.status,
- readAt: row.read_at == null ? null : Number(row.read_at),
- createdAt: Number(row.created_at),
- updatedAt: Number(row.updated_at)
- };
-}
-function formatTodoDigest(items, { now = nowMs(), timezone = DEFAULT_TIMEZONE2 } = {}) {
- const date = localDateLabel(now, timezone);
- if (!items.length) {
- return `${date} \u5F85\u529E\u8BB0\u5F55
-
-\u4ECA\u5929\u6682\u65F6\u6CA1\u6709\u5F85\u529E\u3002`;
- }
- const lines = [`${date} \u5F85\u529E\u8BB0\u5F55`, ""];
- items.forEach((item, index) => {
- const time = item.dueAt || item.startAt ? `\uFF08${formatLocalTime(item.dueAt || item.startAt, timezone)}\uFF09` : "";
- lines.push(`${index + 1}. ${item.title}${time}`);
- });
- return lines.join("\n");
-}
-function createScheduleService(pool, options = {}) {
- const defaultTimezone = normalizeTimezone(options.defaultTimezone || process.env.H5_DEFAULT_TIMEZONE);
- const clock = options.clock || { now: nowMs };
- const createItem = async ({
- userId,
- kind = "task",
- title,
- description = null,
- startAt = null,
- endAt = null,
- dueAt = null,
- allDay = false,
- timezone = defaultTimezone,
- location = null,
- sourceChannel = "agent",
- sourceSessionId = null,
- sourceMessageId = null,
- sourceText = null,
- metadata = null
- }) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const cleanTitle = String(title ?? "").trim();
- if (!cleanTitle) throw new Error("\u7F3A\u5C11\u5F85\u529E\u6807\u9898");
- const safeKind = kind === "event" ? "event" : "task";
- const id = crypto.randomUUID();
- const ts = clock.now();
- await pool.query(
- `INSERT INTO h5_schedule_items
- (id, user_id, kind, title, description, status, start_at, end_at, due_at, all_day,
- timezone, location, source_channel, source_session_id, source_message_id, source_text,
- metadata_json, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- id,
- userId,
- safeKind,
- cleanTitle,
- description,
- startAt,
- endAt,
- dueAt,
- allDay ? 1 : 0,
- normalizeTimezone(timezone),
- location,
- sourceChannel,
- sourceSessionId,
- sourceMessageId,
- sourceText,
- metadata ? JSON.stringify(metadata) : null,
- ts,
- ts
- ]
- );
- return {
- id,
- userId,
- kind: safeKind,
- title: cleanTitle,
- startAt,
- endAt,
- dueAt,
- allDay: Boolean(allDay),
- timezone: normalizeTimezone(timezone),
- status: "active",
- createdAt: ts,
- updatedAt: ts
- };
- };
- const listItems = async ({ userId, from = null, to = null, status = "active", limit = 100 } = {}) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const clauses = ["user_id = ?", "deleted_at IS NULL"];
- const params = [userId];
- if (status) {
- clauses.push("status = ?");
- params.push(status);
- }
- if (from != null && to != null) {
- clauses.push(
- `(
- (start_at IS NOT NULL AND start_at >= ? AND start_at < ?)
- OR (due_at IS NOT NULL AND due_at >= ? AND due_at < ?)
- OR (start_at IS NULL AND due_at IS NULL)
- )`
- );
- params.push(from, to, from, to);
- }
- params.push(Math.max(1, Math.min(500, Number(limit) || 100)));
- const [rows] = await pool.query(
- `SELECT *
- FROM h5_schedule_items
- WHERE ${clauses.join(" AND ")}
- ORDER BY COALESCE(start_at, due_at, 9223372036854775807), created_at
- LIMIT ?`,
- params
- );
- return rows.map(rowToItem);
- };
- const listItemsBySourceMessage = async ({ userId, sourceMessageId, limit = 20 } = {}) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const cleanSourceMessageId = String(sourceMessageId ?? "").trim();
- if (!cleanSourceMessageId) return [];
- const [rows] = await pool.query(
- `SELECT *
- FROM h5_schedule_items
- WHERE user_id = ?
- AND source_message_id = ?
- AND deleted_at IS NULL
- ORDER BY created_at DESC
- LIMIT ?`,
- [userId, cleanSourceMessageId, Math.max(1, Math.min(100, Number(limit) || 20))]
- );
- return rows.map(rowToItem);
- };
- const listTodayTodoItems = async ({ userId, timezone = defaultTimezone, now = clock.now() } = {}) => {
- const start = startOfLocalDay(now, timezone);
- const end = addLocalDays(start, 1, timezone);
- return listItems({ userId, from: start, to: end, status: "active", limit: 200 });
- };
- const listUpcomingItems = async ({
- userId,
- timezone = defaultTimezone,
- days = 7,
- now = clock.now()
- } = {}) => {
- const safeDays = Math.max(1, Math.min(30, Number(days) || 7));
- const start = startOfLocalDay(now, timezone);
- const end = addLocalDays(start, safeDays, timezone);
- return listItems({ userId, from: start, to: end, status: "active", limit: 200 });
- };
- const listUpcomingReminders = async ({
- userId,
- timezone = defaultTimezone,
- days = 7,
- now = clock.now(),
- limit = 100
- } = {}) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const safeDays = Math.max(1, Math.min(30, Number(days) || 7));
- const start = startOfLocalDay(now, timezone);
- const end = addLocalDays(start, safeDays, timezone);
- const [rows] = await pool.query(
- `SELECT r.*,
- i.title AS item_title,
- i.kind AS item_kind,
- i.timezone AS item_timezone,
- i.start_at AS item_start_at,
- i.end_at AS item_end_at
- FROM h5_schedule_reminders r
- INNER JOIN h5_schedule_items i ON i.id = r.item_id
- WHERE r.user_id = ?
- AND r.status IN ('pending', 'locked', 'sent')
- AND r.remind_at >= ?
- AND r.remind_at < ?
- AND i.deleted_at IS NULL
- AND i.status = 'active'
- ORDER BY r.remind_at ASC
- LIMIT ?`,
- [
- userId,
- start,
- end,
- Math.max(1, Math.min(200, Number(limit) || 100))
- ]
- );
- return rows.map(rowToReminderWithItem);
- };
- const getItem = async ({ userId, itemId }) => {
- if (!userId || !itemId) throw new Error("\u7F3A\u5C11\u4E8B\u9879\u53C2\u6570");
- const [rows] = await pool.query(
- `SELECT *
- FROM h5_schedule_items
- WHERE id = ? AND user_id = ? AND deleted_at IS NULL
- LIMIT 1`,
- [itemId, userId]
- );
- return rowToItem(rows[0]);
- };
- const getReminder = async ({ userId, reminderId }) => {
- if (!userId || !reminderId) throw new Error("\u7F3A\u5C11\u63D0\u9192\u53C2\u6570");
- const [rows] = await pool.query(
- `SELECT r.*,
- i.title AS item_title,
- i.kind AS item_kind,
- i.timezone AS item_timezone,
- i.start_at AS item_start_at,
- i.end_at AS item_end_at
- FROM h5_schedule_reminders r
- INNER JOIN h5_schedule_items i ON i.id = r.item_id
- WHERE r.id = ? AND r.user_id = ? AND i.deleted_at IS NULL
- LIMIT 1`,
- [reminderId, userId]
- );
- return rowToReminderWithItem(rows[0]);
- };
- const cancelReminder = async ({ userId, reminderId, reason = "\u7528\u6237\u5FFD\u7565" } = {}) => {
- const reminder = await getReminder({ userId, reminderId });
- if (!reminder) throw new Error("\u63D0\u9192\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE");
- if (reminder.status === "cancelled") return reminder;
- if (reminder.status === "sent") throw new Error("\u5DF2\u901A\u77E5\u7684\u63D0\u9192\u4E0D\u80FD\u5FFD\u7565");
- return markReminderCancelled(reminder, reason);
- };
- const deleteReminder = async ({ userId, reminderId }) => {
- if (!userId || !reminderId) throw new Error("\u7F3A\u5C11\u63D0\u9192\u53C2\u6570");
- const [result] = await pool.query(
- `DELETE FROM h5_schedule_reminders WHERE id = ? AND user_id = ?`,
- [reminderId, userId]
- );
- return Number(result?.affectedRows ?? 0) > 0;
- };
- const deleteReminders = async ({ userId, reminderIds }) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const ids = [...new Set((reminderIds ?? []).map((id) => String(id ?? "").trim()).filter(Boolean))];
- if (ids.length === 0) return 0;
- const [result] = await pool.query(
- `DELETE FROM h5_schedule_reminders WHERE user_id = ? AND id IN (${ids.map(() => "?").join(", ")})`,
- [userId, ...ids]
- );
- return Number(result?.affectedRows ?? 0);
- };
- const createReminder = async ({
- userId,
- itemId,
- remindAt,
- offsetMinutes = null,
- channel = "wechat"
- }) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- if (!itemId) throw new Error("\u7F3A\u5C11\u4E8B\u9879");
- const safeRemindAt = Number(remindAt);
- if (!Number.isFinite(safeRemindAt) || safeRemindAt <= 0) {
- throw new Error("\u63D0\u9192\u65F6\u95F4\u65E0\u6548");
- }
- const safeOffsetMinutes = offsetMinutes == null || offsetMinutes === "" ? null : Number(offsetMinutes);
- if (safeOffsetMinutes != null && !Number.isInteger(safeOffsetMinutes)) {
- throw new Error("\u63D0\u9192\u504F\u79FB\u5206\u949F\u65E0\u6548");
- }
- const safeChannel = channel === "in_app" ? "in_app" : "wechat";
- const [itemRows] = await pool.query(
- `SELECT id
- FROM h5_schedule_items
- WHERE id = ? AND user_id = ? AND deleted_at IS NULL
- LIMIT 1`,
- [itemId, userId]
- );
- if (!itemRows[0]) throw new Error("\u4E8B\u9879\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE");
- const id = crypto.randomUUID();
- const ts = clock.now();
- await pool.query(
- `INSERT INTO h5_schedule_reminders
- (id, user_id, item_id, remind_at, offset_minutes, channel, status, attempts,
- last_error, locked_until, sent_at, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, 'pending', 0, NULL, NULL, NULL, ?, ?)
- ON DUPLICATE KEY UPDATE
- offset_minutes = VALUES(offset_minutes),
- status = 'pending',
- last_error = NULL,
- locked_until = NULL,
- updated_at = VALUES(updated_at)`,
- [id, userId, itemId, safeRemindAt, safeOffsetMinutes, safeChannel, ts, ts]
- );
- const [rows] = await pool.query(
- `SELECT *
- FROM h5_schedule_reminders
- WHERE item_id = ? AND remind_at = ? AND channel = ?
- LIMIT 1`,
- [itemId, safeRemindAt, safeChannel]
- );
- return rowToReminder(rows[0]) || {
- id,
- userId,
- itemId,
- remindAt: safeRemindAt,
- offsetMinutes: safeOffsetMinutes,
- channel: safeChannel,
- status: "pending",
- attempts: 0,
- lastError: null,
- lockedUntil: null,
- sentAt: null,
- createdAt: ts,
- updatedAt: ts
- };
- };
- const listDigestSubscriptions = async ({
- userId,
- digestType = "todo_day",
- channel = null,
- status = null,
- limit = 20
- } = {}) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const clauses = ["user_id = ?", "digest_type = ?"];
- const params = [userId, digestType];
- if (channel) {
- clauses.push("channel = ?");
- params.push(channel);
- }
- if (status) {
- const statuses = Array.isArray(status) ? status : [status];
- clauses.push(`status IN (${statuses.map(() => "?").join(", ")})`);
- params.push(...statuses);
- }
- params.push(Math.max(1, Math.min(200, Number(limit) || 20)));
- const [rows] = await pool.query(
- `SELECT *
- FROM h5_schedule_digest_subscriptions
- WHERE ${clauses.join(" AND ")}
- ORDER BY next_run_at ASC, created_at DESC
- LIMIT ?`,
- params
- );
- return rows.map(rowToDigest);
- };
- const createDailyTodoDigest = async ({
- userId,
- hour,
- minute = 0,
- timezone = defaultTimezone,
- channel = "wechat",
- sourceChannel = "agent",
- sourceSessionId = null,
- sourceMessageId = null,
- sourceText = null
- }) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const safeHour = Number(hour);
- const safeMinute = Number(minute);
- if (!Number.isInteger(safeHour) || safeHour < 0 || safeHour > 23) {
- throw new Error("\u63D0\u9192\u5C0F\u65F6\u65E0\u6548");
- }
- if (!Number.isInteger(safeMinute) || safeMinute < 0 || safeMinute > 59) {
- throw new Error("\u63D0\u9192\u5206\u949F\u65E0\u6548");
- }
- const tz = normalizeTimezone(timezone);
- const nextRunAt = nextDailyRunAt({
- hour: safeHour,
- minute: safeMinute,
- timezone: tz,
- now: clock.now()
- });
- const id = crypto.randomUUID();
- const ts = clock.now();
- await pool.query(
- `INSERT INTO h5_schedule_digest_subscriptions
- (id, user_id, digest_type, hour, minute, timezone, channel, status, next_run_at,
- source_channel, source_session_id, source_message_id, source_text, created_at, updated_at)
- VALUES (?, ?, 'todo_day', ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- hour = VALUES(hour),
- minute = VALUES(minute),
- timezone = VALUES(timezone),
- channel = VALUES(channel),
- status = 'active',
- next_run_at = VALUES(next_run_at),
- source_channel = VALUES(source_channel),
- source_session_id = VALUES(source_session_id),
- source_message_id = VALUES(source_message_id),
- source_text = VALUES(source_text),
- updated_at = VALUES(updated_at)`,
- [
- id,
- userId,
- safeHour,
- safeMinute,
- tz,
- channel,
- nextRunAt,
- sourceChannel,
- sourceSessionId,
- sourceMessageId,
- sourceText,
- ts,
- ts
- ]
- );
- const [rows] = await pool.query(
- `SELECT * FROM h5_schedule_digest_subscriptions
- WHERE user_id = ? AND digest_type = 'todo_day' AND channel = ?
- LIMIT 1`,
- [userId, channel]
- );
- return rowToDigest(rows[0]) || {
- id,
- userId,
- hour: safeHour,
- minute: safeMinute,
- timezone: tz,
- channel,
- status: "active",
- nextRunAt,
- lastRunAt: null,
- attempts: 0
- };
- };
- const createBalanceLowAlert = async ({
- userId,
- thresholdCents,
- channel = "wechat",
- sourceChannel = "agent",
- sourceSessionId = null,
- sourceMessageId = null,
- sourceText = null
- } = {}) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const safeThreshold = Number(thresholdCents);
- if (!Number.isFinite(safeThreshold) || safeThreshold < 0) {
- throw new Error("\u4F59\u989D\u9608\u503C\u65E0\u6548");
- }
- const now = clock.now();
- const id = crypto.randomUUID();
- await pool.query(
- `INSERT INTO h5_balance_alert_subscriptions
- (id, user_id, threshold_cents, channel, status, next_run_at, last_run_at,
- last_notified_balance_cents, attempts, locked_until, last_error,
- source_channel, source_session_id, source_message_id, source_text, created_at, updated_at)
- VALUES (?, ?, ?, ?, 'active', ?, NULL, NULL, 0, NULL, NULL, ?, ?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- threshold_cents = VALUES(threshold_cents),
- channel = VALUES(channel),
- status = 'active',
- next_run_at = VALUES(next_run_at),
- last_error = NULL,
- locked_until = NULL,
- updated_at = VALUES(updated_at)`,
- [id, userId, safeThreshold, channel, now, sourceChannel, sourceSessionId, sourceMessageId, sourceText, now, now]
- );
- const [rows] = await pool.query(
- `SELECT * FROM h5_balance_alert_subscriptions WHERE user_id = ? AND channel = ? LIMIT 1`,
- [userId, channel]
- );
- return rowToBalanceAlert(rows[0]) || {
- id,
- userId,
- thresholdCents: safeThreshold,
- channel,
- status: "active",
- nextRunAt: now,
- lastRunAt: null,
- lastNotifiedBalanceCents: null,
- attempts: 0
- };
- };
- const listDueReminders = async ({ now = clock.now(), limit = 50 } = {}) => {
- const [rows] = await pool.query(
- `SELECT r.*
- FROM h5_schedule_reminders r
- INNER JOIN h5_schedule_items i ON i.id = r.item_id
- WHERE r.status = 'pending'
- AND r.remind_at <= ?
- AND (r.locked_until IS NULL OR r.locked_until <= ?)
- AND i.deleted_at IS NULL
- AND i.status = 'active'
- ORDER BY r.remind_at ASC
- LIMIT ?`,
- [now, now, Math.max(1, Math.min(200, Number(limit) || 50))]
- );
- return rows.map(rowToReminder);
- };
- const lockReminder = async (id, { now = clock.now(), lockMs = 12e4 } = {}) => {
- const lockedUntil = now + lockMs;
- const [result] = await pool.query(
- `UPDATE h5_schedule_reminders
- SET status = 'locked', locked_until = ?, attempts = attempts + 1, updated_at = ?
- WHERE id = ? AND status = 'pending' AND remind_at <= ?`,
- [lockedUntil, now, id, now]
- );
- if (Number(result?.affectedRows ?? 0) !== 1) return null;
- const [rows] = await pool.query(
- `SELECT * FROM h5_schedule_reminders WHERE id = ? LIMIT 1`,
- [id]
- );
- return rowToReminder(rows[0]);
- };
- const markReminderSent = async (reminder, { now = clock.now() } = {}) => {
- await pool.query(
- `UPDATE h5_schedule_reminders
- SET status = 'sent', sent_at = ?, locked_until = NULL, last_error = NULL, updated_at = ?
- WHERE id = ?`,
- [now, now, reminder.id]
- );
- return { ...reminder, status: "sent", sentAt: now, lockedUntil: null };
- };
- const markReminderCancelled = async (reminder, reason, { now = clock.now() } = {}) => {
- const lastError = String(reason ?? "\u5DF2\u53D6\u6D88").slice(0, 500);
- await pool.query(
- `UPDATE h5_schedule_reminders
- SET status = 'cancelled', locked_until = NULL, last_error = ?, updated_at = ?
- WHERE id = ?`,
- [lastError, now, reminder.id]
- );
- return { ...reminder, status: "cancelled", lastError };
- };
- const markReminderFailed = async (reminder, error, { now = clock.now(), retryMs = 10 * 60 * 1e3, maxAttempts = 5 } = {}) => {
- const attempts = Number(reminder.attempts ?? 0);
- const status = attempts >= maxAttempts ? "failed" : "pending";
- const lockedUntil = status === "pending" ? now + retryMs : null;
- await pool.query(
- `UPDATE h5_schedule_reminders
- SET status = ?, locked_until = ?, last_error = ?, updated_at = ?
- WHERE id = ?`,
- [
- status,
- lockedUntil,
- String(error?.message ?? error ?? "\u53D1\u9001\u5931\u8D25").slice(0, 500),
- now,
- reminder.id
- ]
- );
- };
- const buildReminderText = async (reminder) => {
- const item = await getItem({ userId: reminder.userId, itemId: reminder.itemId });
- if (!item || item.status !== "active") return null;
- const timezone = item.timezone || defaultTimezone;
- const remindLabel = formatLocalTime(reminder.remindAt, timezone);
- const eventAt = item.startAt ?? item.dueAt ?? null;
- const eventLabel = eventAt ? formatLocalTime(eventAt, timezone) : null;
- const lines = [`\u3010\u5F85\u529E\u63D0\u9192\u3011${item.title}`];
- if (eventLabel && eventLabel !== remindLabel) {
- lines.push(`\u4E8B\u9879\u65F6\u95F4\uFF1A${eventLabel}`);
- }
- lines.push(`\u63D0\u9192\u65F6\u95F4\uFF1A${remindLabel}`);
- if (item.location) lines.push(`\u5730\u70B9\uFF1A${item.location}`);
- return lines.join("\n");
- };
- const listDueBalanceAlerts = async ({ now = clock.now(), limit = 50 } = {}) => {
- const [rows] = await pool.query(
- `SELECT *
- FROM h5_balance_alert_subscriptions
- WHERE next_run_at <= ?
- AND (status = 'active' OR (status = 'locked' AND locked_until IS NOT NULL AND locked_until <= ?))
- ORDER BY next_run_at ASC
- LIMIT ?`,
- [now, now, Math.max(1, Math.min(200, Number(limit) || 50))]
- );
- return rows.map(rowToBalanceAlert);
- };
- const lockBalanceAlert = async (id, { now = clock.now(), lockMs = 12e4 } = {}) => {
- const lockedUntil = now + lockMs;
- const [result] = await pool.query(
- `UPDATE h5_balance_alert_subscriptions
- SET status = 'locked', locked_until = ?, attempts = attempts + 1, updated_at = ?
- WHERE id = ? AND (status = 'active' OR (status = 'locked' AND locked_until IS NOT NULL AND locked_until <= ?))`,
- [lockedUntil, now, id, now]
- );
- if (Number(result?.affectedRows ?? 0) !== 1) return null;
- const [rows] = await pool.query(
- `SELECT * FROM h5_balance_alert_subscriptions WHERE id = ? LIMIT 1`,
- [id]
- );
- return rowToBalanceAlert(rows[0]);
- };
- const markBalanceAlertSent = async (subscription, { now = clock.now() } = {}) => {
- await pool.query(
- `UPDATE h5_balance_alert_subscriptions
- SET status = 'active', next_run_at = ?, last_run_at = ?, last_notified_balance_cents = ?,
- locked_until = NULL, last_error = NULL, updated_at = ?
- WHERE id = ?`,
- [now + 5 * 60 * 1e3, now, subscription.lastNotifiedBalanceCents ?? null, now, subscription.id]
- );
- return { ...subscription, status: "active", lastRunAt: now };
- };
- const markBalanceAlertFailed = async (subscription, error, { now = clock.now(), retryMs = 10 * 60 * 1e3, maxAttempts = 5 } = {}) => {
- const attempts = Number(subscription.attempts ?? 0);
- const status = attempts >= maxAttempts ? "failed" : "active";
- const nextRunAt = status === "active" ? now + retryMs : subscription.nextRunAt;
- await pool.query(
- `UPDATE h5_balance_alert_subscriptions
- SET status = ?, next_run_at = ?, locked_until = NULL, last_error = ?, updated_at = ?
- WHERE id = ?`,
- [status, nextRunAt, String(error?.message ?? error ?? "\u53D1\u9001\u5931\u8D25").slice(0, 500), now, subscription.id]
- );
- };
- const listDueDigestSubscriptions = async ({ now = clock.now(), limit = 50 } = {}) => {
- const [rows] = await pool.query(
- `SELECT *
- FROM h5_schedule_digest_subscriptions
- WHERE next_run_at <= ?
- AND (status = 'active' OR (status = 'locked' AND locked_until IS NOT NULL AND locked_until <= ?))
- ORDER BY next_run_at ASC
- LIMIT ?`,
- [now, now, Math.max(1, Math.min(200, Number(limit) || 50))]
- );
- return rows.map(rowToDigest);
- };
- const lockDigestSubscription = async (id, { now = clock.now(), lockMs = 12e4 } = {}) => {
- const lockedUntil = now + lockMs;
- const [result] = await pool.query(
- `UPDATE h5_schedule_digest_subscriptions
- SET status = 'locked', locked_until = ?, attempts = attempts + 1, updated_at = ?
- WHERE id = ? AND (status = 'active' OR (status = 'locked' AND locked_until IS NOT NULL AND locked_until <= ?))`,
- [lockedUntil, now, id, now]
- );
- if (Number(result?.affectedRows ?? 0) !== 1) return null;
- const [rows] = await pool.query(
- `SELECT * FROM h5_schedule_digest_subscriptions WHERE id = ? LIMIT 1`,
- [id]
- );
- return rowToDigest(rows[0]);
- };
- const markDigestSent = async (subscription, { now = clock.now() } = {}) => {
- const nextRunAt = nextDailyRunAt({
- hour: subscription.hour,
- minute: subscription.minute,
- timezone: subscription.timezone,
- now: now + 1e3
- });
- await pool.query(
- `UPDATE h5_schedule_digest_subscriptions
- SET status = 'active', next_run_at = ?, last_run_at = ?, locked_until = NULL,
- last_error = NULL, updated_at = ?
- WHERE id = ?`,
- [nextRunAt, now, now, subscription.id]
- );
- return { ...subscription, status: "active", nextRunAt, lastRunAt: now };
- };
- const markDigestFailed = async (subscription, error, { now = clock.now(), retryMs = 10 * 60 * 1e3, maxAttempts = 5 } = {}) => {
- const attempts = Number(subscription.attempts ?? 0);
- const status = attempts >= maxAttempts ? "failed" : "active";
- const nextRunAt = status === "active" ? now + retryMs : subscription.nextRunAt;
- await pool.query(
- `UPDATE h5_schedule_digest_subscriptions
- SET status = ?, next_run_at = ?, locked_until = NULL, last_error = ?, updated_at = ?
- WHERE id = ?`,
- [status, nextRunAt, String(error?.message ?? error ?? "\u53D1\u9001\u5931\u8D25").slice(0, 500), now, subscription.id]
- );
- };
- const logDelivery = async ({
- reminderId = null,
- subscriptionId = null,
- userId,
- channel = "wechat",
- status,
- providerMessageId = null,
- errorCode = null,
- errorMessage = null
- }) => {
- await pool.query(
- `INSERT INTO h5_schedule_delivery_logs
- (id, reminder_id, subscription_id, user_id, channel, status, provider_message_id,
- error_code, error_message, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- crypto.randomUUID(),
- reminderId,
- subscriptionId,
- userId,
- channel,
- status,
- providerMessageId,
- errorCode,
- errorMessage ? String(errorMessage).slice(0, 500) : null,
- clock.now()
- ]
- );
- };
- const buildTodoDigestText = async ({ userId, timezone = defaultTimezone, now = clock.now() }) => {
- const items = await listTodayTodoItems({ userId, timezone, now });
- return formatTodoDigest(items, { now, timezone });
- };
- const getUserWalletSnapshot = async (userId) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const [rows] = await pool.query(
- `SELECT balance_cents AS balanceCents, tokens_used AS tokensUsed
- FROM h5_user_wallets
- WHERE user_id = ?
- LIMIT 1`,
- [userId]
- );
- return rows[0] ?? null;
- };
- const createUserNotification = async ({
- userId,
- channel = "web",
- notificationType,
- title,
- body,
- data = null
- }) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- if (!notificationType) throw new Error("\u7F3A\u5C11\u901A\u77E5\u7C7B\u578B");
- if (!title) throw new Error("\u7F3A\u5C11\u901A\u77E5\u6807\u9898");
- if (!body) throw new Error("\u7F3A\u5C11\u901A\u77E5\u5185\u5BB9");
- const now = clock.now();
- const id = crypto.randomUUID();
- await pool.query(
- `INSERT INTO h5_user_notifications
- (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, 'unread', NULL, ?, ?)`,
- [id, userId, channel, notificationType, title, body, data ? JSON.stringify(data) : null, now, now]
- );
- return { id, userId, channel, notificationType, title, body, data, status: "unread", readAt: null, createdAt: now, updatedAt: now };
- };
- const listUserNotifications = async ({ userId, status = "unread", limit = 20 } = {}) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const clauses = ["user_id = ?"];
- const params = [userId];
- if (status && status !== "all") {
- clauses.push("status = ?");
- params.push(status);
- }
- params.push(Math.max(1, Math.min(100, Number(limit) || 20)));
- const [rows] = await pool.query(
- `SELECT *
- FROM h5_user_notifications
- WHERE ${clauses.join(" AND ")}
- ORDER BY created_at DESC
- LIMIT ?`,
- params
- );
- return rows.map(rowToUserNotification);
- };
- const markUserNotificationRead = async ({ userId, notificationId }) => {
- if (!userId || !notificationId) throw new Error("\u7F3A\u5C11\u901A\u77E5\u53C2\u6570");
- const now = clock.now();
- const [result] = await pool.query(
- `UPDATE h5_user_notifications
- SET status = 'read', read_at = ?, updated_at = ?
- WHERE id = ? AND user_id = ? AND status <> 'read'`,
- [now, now, notificationId, userId]
- );
- return Number(result?.affectedRows ?? 0) > 0;
- };
- const markAllUserNotificationsRead = async ({ userId } = {}) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const now = clock.now();
- const [result] = await pool.query(
- `UPDATE h5_user_notifications
- SET status = 'read', read_at = ?, updated_at = ?
- WHERE user_id = ? AND status <> 'read'`,
- [now, now, userId]
- );
- return Number(result?.affectedRows ?? 0);
- };
- const deleteUserNotification = async ({ userId, notificationId } = {}) => {
- if (!userId || !notificationId) throw new Error("\u7F3A\u5C11\u901A\u77E5\u53C2\u6570");
- const [result] = await pool.query(
- `DELETE FROM h5_user_notifications
- WHERE id = ? AND user_id = ?`,
- [notificationId, userId]
- );
- return Number(result?.affectedRows ?? 0) > 0;
- };
- const clearUserNotifications = async ({ userId, status = "all" } = {}) => {
- if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237");
- const clauses = ["user_id = ?"];
- const params = [userId];
- if (status && status !== "all") {
- clauses.push("status = ?");
- params.push(status);
- }
- const [result] = await pool.query(
- `DELETE FROM h5_user_notifications
- WHERE ${clauses.join(" AND ")}`,
- params
- );
- return Number(result?.affectedRows ?? 0);
- };
- return {
- createItem,
- getItem,
- createReminder,
- listItems,
- listItemsBySourceMessage,
- listTodayTodoItems,
- listUpcomingItems,
- listUpcomingReminders,
- getReminder,
- cancelReminder,
- deleteReminder,
- deleteReminders,
- listDigestSubscriptions,
- createDailyTodoDigest,
- createBalanceLowAlert,
- listDueReminders,
- lockReminder,
- markReminderSent,
- markReminderCancelled,
- markReminderFailed,
- buildReminderText,
- listDueDigestSubscriptions,
- lockDigestSubscription,
- markDigestSent,
- markDigestFailed,
- listDueBalanceAlerts,
- lockBalanceAlert,
- markBalanceAlertSent,
- markBalanceAlertFailed,
- logDelivery,
- buildTodoDigestText,
- getUserWalletSnapshot,
- createUserNotification,
- listUserNotifications,
- markUserNotificationRead,
- markAllUserNotificationsRead,
- deleteUserNotification,
- clearUserNotifications
- };
-}
-
-// 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 = 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);
-var PRIVATE_DATA_MAX_ROWS = Number(process.env.PRIVATE_DATA_MAX_ROWS ?? 200);
-var PRIVATE_DATA_USER_ID = process.env.PRIVATE_DATA_USER_ID?.trim();
-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 = 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 + 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;
-}
-var ALL_TOOLS = [
- {
- name: "read_file",
- description: "\u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\u7684\u6587\u4EF6\uFF09",
- inputSchema: {
- type: "object",
- properties: {
- path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u5DE5\u4F5C\u533A\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09" }
- },
- required: ["path"]
- }
- },
- {
- name: "write_file",
- description: "\u5199\u5165\u6587\u4EF6\u5185\u5BB9\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\u7684\u6587\u4EF6\uFF1B\u4E0D\u5B58\u5728\u5219\u521B\u5EFA\uFF0C\u5DF2\u5B58\u5728\u5219\u8986\u76D6\uFF09",
- inputSchema: {
- type: "object",
- properties: {
- path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84" },
- content: { type: "string", description: "\u6587\u4EF6\u5185\u5BB9" }
- },
- required: ["path", "content"]
- }
- },
- {
- name: "edit_file",
- description: "\u5C06\u6587\u4EF6\u4E2D\u7684\u65E7\u5185\u5BB9\u66FF\u6362\u4E3A\u65B0\u5185\u5BB9\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\u7684\u6587\u4EF6\uFF09",
- inputSchema: {
- type: "object",
- properties: {
- path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84" },
- old_str: { type: "string", description: "\u8981\u66FF\u6362\u7684\u539F\u59CB\u5185\u5BB9\uFF08\u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\uFF09" },
- new_str: { type: "string", description: "\u66FF\u6362\u540E\u7684\u65B0\u5185\u5BB9" }
- },
- required: ["path", "old_str", "new_str"]
- }
- },
- {
- name: "list_dir",
- description: "\u5217\u51FA\u76EE\u5F55\u5185\u5BB9\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\u7684\u76EE\u5F55\uFF09",
- inputSchema: {
- type: "object",
- properties: {
- path: {
- type: "string",
- description: "\u76EE\u5F55\u8DEF\u5F84\uFF08\u7701\u7565\u5219\u5217\u51FA\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF09"
- }
- }
- }
- },
- {
- name: "create_dir",
- description: "\u521B\u5EFA\u76EE\u5F55\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\uFF1B\u5DF2\u5B58\u5728\u4E0D\u62A5\u9519\uFF09",
- inputSchema: {
- type: "object",
- properties: {
- path: { type: "string", description: "\u76EE\u5F55\u8DEF\u5F84" }
- },
- 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",
- inputSchema: { type: "object", properties: {} }
- },
- {
- name: "private_data_schema",
- description: "\u67E5\u770B\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u4E2D\u7684\u8868\u548C\u5B57\u6BB5\u3002\u7528\u4E8E\u4E86\u89E3\u5F53\u524D\u7528\u6237\u81EA\u5DF1\u7684 SQLite \u8868\u7ED3\u6784\u3002",
- inputSchema: { type: "object", properties: {} }
- },
- {
- name: "private_data_query",
- description: "\u53EA\u8BFB\u67E5\u8BE2\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u3002\u4EC5\u5141\u8BB8 SELECT/WITH\uFF0C\u9ED8\u8BA4\u6700\u591A\u8FD4\u56DE 200 \u884C\uFF0C\u9002\u5408\u7EDF\u8BA1\u95EE\u5377\u56DE\u7B54\u3001\u7B5B\u9009\u8868\u5355\u6570\u636E\u3001\u5206\u6790\u7528\u6237\u79C1\u6709\u6570\u636E\u3002",
- inputSchema: {
- type: "object",
- properties: {
- sql: { type: "string", description: "\u53EA\u8BFB SQL\uFF0C\u5FC5\u987B\u662F SELECT/WITH" }
- },
- required: ["sql"]
- }
- },
- {
- name: "private_data_execute",
- description: "\u5199\u5165\u6216\u53D8\u66F4\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u3002\u53EF\u7528\u4E8E\u521B\u5EFA\u95EE\u5377/\u8868\u5355/\u6E05\u5355\u7B49\u79C1\u6709\u8868\u5E76\u5199\u5165\u6570\u636E\uFF1B\u7981\u6B62 ATTACH\u3001load_extension\u3001PRAGMA writable_schema\u3001VACUUM INTO \u7B49\u8D8A\u754C\u6216\u5371\u9669\u64CD\u4F5C\u3002",
- inputSchema: {
- type: "object",
- properties: {
- sql: { type: "string", description: "\u8981\u6267\u884C\u7684 SQLite SQL" }
- },
- required: ["sql"]
- }
- }
-];
-var quotaPool = null;
-var scheduleService = null;
-function isQuotaSyncConfigured() {
- return Boolean(
- PRIVATE_DATA_USER_ID && (process.env.DATABASE_URL || process.env.MYSQL_HOST && process.env.MYSQL_DATABASE)
- );
-}
-function getQuotaPool() {
- if (!isQuotaSyncConfigured()) return null;
- if (quotaPool) return quotaPool;
- quotaPool = process.env.DATABASE_URL ? mysql.createPool(process.env.DATABASE_URL) : mysql.createPool({
- host: process.env.MYSQL_HOST ?? "localhost",
- port: Number(process.env.MYSQL_PORT ?? 3306),
- user: process.env.MYSQL_USER ?? "boot",
- password: process.env.MYSQL_PASSWORD ?? "",
- database: process.env.MYSQL_DATABASE ?? "tkmind",
- waitForConnections: true,
- connectionLimit: 2
- });
- return quotaPool;
-}
-function isScheduleConfigured() {
- return isQuotaSyncConfigured();
-}
-function getScheduleService() {
- if (!PRIVATE_DATA_USER_ID) throw new Error("\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u7528\u6237\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u80FD\u64CD\u4F5C\u5F85\u529E");
- const pool = getQuotaPool();
- if (!pool || !isScheduleConfigured()) {
- throw new Error("\u5F53\u524D\u73AF\u5883\u672A\u914D\u7F6E\u5F85\u529E\u6570\u636E\u5E93\u8FDE\u63A5");
- }
- if (!scheduleService) {
- scheduleService = createScheduleService(pool, {
- defaultTimezone: process.env.H5_DEFAULT_TIMEZONE || "Asia/Shanghai"
- });
- }
- return scheduleService;
-}
-if (isScheduleConfigured()) {
- ALL_TOOLS.push(
- {
- name: "schedule_create_item",
- description: "\u4E3A\u5F53\u524D\u7528\u6237\u521B\u5EFA\u5F85\u529E\u6216\u65E5\u7A0B\u4E8B\u9879\u3002\u4EC5\u5199\u5165\u5F53\u524D\u7528\u6237\u81EA\u5DF1\u7684 h5_schedule_items\uFF0C\u4E0D\u53EF\u64CD\u4F5C\u5176\u4ED6\u7528\u6237\u6570\u636E\u3002",
- inputSchema: {
- type: "object",
- properties: {
- kind: { type: "string", description: "task \u6216 event\uFF0C\u9ED8\u8BA4 task" },
- title: { type: "string", description: "\u4E8B\u9879\u6807\u9898" },
- description: { type: "string", description: "\u4E8B\u9879\u63CF\u8FF0\uFF0C\u53EF\u9009" },
- startAt: { type: "number", description: "\u5F00\u59CB\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009\uFF08\u4F18\u5148\u4F7F\u7528 startLocal\uFF09" },
- startLocal: { type: "string", description: "\u672C\u5730\u5F00\u59CB\u65F6\u95F4 YYYY-MM-DD HH:mm\uFF0C\u63A8\u8350" },
- endAt: { type: "number", description: "\u7ED3\u675F\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009\uFF08\u4F18\u5148\u4F7F\u7528 endLocal\uFF09" },
- endLocal: { type: "string", description: "\u672C\u5730\u7ED3\u675F\u65F6\u95F4 YYYY-MM-DD HH:mm\uFF0C\u63A8\u8350" },
- dueAt: { type: "number", description: "\u622A\u6B62\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009\uFF08\u4F18\u5148\u4F7F\u7528 dueLocal\uFF09" },
- dueLocal: { type: "string", description: "\u672C\u5730\u622A\u6B62\u65F6\u95F4 YYYY-MM-DD HH:mm\uFF0C\u53EF\u9009" },
- allDay: { type: "boolean", description: "\u662F\u5426\u5168\u5929\u4E8B\u9879\uFF0C\u53EF\u9009" },
- timezone: { type: "string", description: "\u65F6\u533A\uFF0C\u53EF\u9009" },
- location: { type: "string", description: "\u5730\u70B9\uFF0C\u53EF\u9009" },
- sourceMessageId: { type: "string", description: "\u670D\u52A1\u53F7\u6D88\u606F ID\uFF1B\u6709\u503C\u65F6\u5FC5\u987B\u539F\u6837\u4F20\u5165" },
- sourceText: { type: "string", description: "\u539F\u59CB\u7528\u6237\u6587\u672C\uFF0C\u53EF\u9009" }
- },
- required: ["title"]
- }
- },
- {
- name: "schedule_create_reminder",
- description: "\u4E3A\u5F53\u524D\u7528\u6237\u5DF2\u6709\u4E8B\u9879\u521B\u5EFA\u5355\u6B21\u63D0\u9192\u3002\u5FC5\u987B\u5148\u6709 itemId\uFF0C\u518D\u5199\u5165 h5_schedule_reminders\u3002",
- inputSchema: {
- type: "object",
- properties: {
- itemId: { type: "string", description: "\u4E8B\u9879 ID" },
- remindAt: { type: "number", description: "\u63D0\u9192\u89E6\u53D1\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF08\u4F18\u5148\u4F7F\u7528 remindLocal\uFF09" },
- remindLocal: { type: "string", description: "\u672C\u5730\u63D0\u9192\u65F6\u95F4 YYYY-MM-DD HH:mm\uFF0C\u63A8\u8350" },
- offsetMinutes: { type: "number", description: "\u76F8\u5BF9\u4E8B\u9879\u65F6\u95F4\u7684\u63D0\u524D\u5206\u949F\u6570\uFF0C\u53EF\u9009" },
- channel: { type: "string", description: "\u63D0\u9192\u901A\u9053\uFF0C\u9ED8\u8BA4 wechat" }
- },
- required: ["itemId"]
- }
- },
- {
- name: "schedule_list_items",
- description: "\u67E5\u8BE2\u5F53\u524D\u7528\u6237\u7684\u5F85\u529E/\u65E5\u7A0B\u4E8B\u9879\u5217\u8868\u3002\u4EC5\u8FD4\u56DE\u5F53\u524D\u7528\u6237\u6570\u636E\uFF0C\u53EF\u6309\u65F6\u95F4\u8303\u56F4\u8FC7\u6EE4\u3002",
- inputSchema: {
- type: "object",
- properties: {
- from: { type: "number", description: "\u8D77\u59CB\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009" },
- to: { type: "number", description: "\u7ED3\u675F\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009" },
- status: { type: "string", description: "\u4E8B\u9879\u72B6\u6001\uFF0C\u9ED8\u8BA4 active\uFF0C\u53EF\u9009" },
- limit: { type: "number", description: "\u8FD4\u56DE\u6570\u91CF\uFF0C\u9ED8\u8BA4 50\uFF0C\u53EF\u9009" }
- }
- }
- }
- );
-}
-var TOOLS = ALLOWED_TOOLS ? ALL_TOOLS.filter((t) => ALLOWED_TOOLS.has(t.name)) : ALL_TOOLS;
-function ensurePrivateDataDb() {
- 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 fs2.existsSync(PRIVATE_DATA_DIR) ? fs2.readdirSync(PRIVATE_DATA_DIR) : []) {
- if (file === "private-data.sqlite" || file.startsWith("private-data.sqlite-")) {
- total += fs2.statSync(path2.join(PRIVATE_DATA_DIR, file)).size;
- }
- }
- return total;
-}
-function runSqlite(args) {
- return execFileSync(SQLITE_BIN, args, {
- encoding: "utf8",
- timeout: PRIVATE_DATA_QUERY_TIMEOUT_MS,
- maxBuffer: 1024 * 1024
- });
-}
-function sqliteScalar(sql) {
- return Number(runSqlite(["-batch", "-noheader", PRIVATE_DATA_DB, sql]).trim() || 0);
-}
-async function getQuotaState({ forUpdate = false, conn = null } = {}) {
- const pool = getQuotaPool();
- if (!pool) return null;
- const db = conn ?? pool;
- const [rows] = await db.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes, status
- FROM h5_user_spaces
- WHERE user_id = ?
- LIMIT 1 ${forUpdate ? "FOR UPDATE" : ""}`,
- [PRIVATE_DATA_USER_ID]
- );
- const row = rows[0];
- if (!row) return null;
- const quotaBytes = Number(row.quota_bytes ?? 0);
- const usedBytes = Number(row.used_bytes ?? 0);
- const reservedBytes = Number(row.reserved_bytes ?? 0);
- return {
- id: row.id,
- status: row.status,
- quotaBytes,
- usedBytes,
- reservedBytes,
- availableBytes: Math.max(0, quotaBytes - usedBytes - reservedBytes)
- };
-}
-async function sqliteMaxPagePragmaForQuota(currentBytes) {
- const quota = await getQuotaState();
- if (!quota) return "";
- if (quota.status !== "active") throw new Error("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u5199");
- if (quota.availableBytes <= 0 && currentBytes >= PRIVATE_DATA_MAX_BYTES) {
- throw new Error("\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u914D\u989D\u4E0D\u8DB3");
- }
- const allowedSize = Math.min(PRIVATE_DATA_MAX_BYTES, currentBytes + quota.availableBytes);
- const pageSize = sqliteScalar("PRAGMA page_size;") || 4096;
- const currentPages = sqliteScalar("PRAGMA page_count;") || 1;
- const maxPages = Math.max(currentPages, Math.max(1, Math.floor(allowedSize / pageSize)));
- return `PRAGMA max_page_count=${maxPages};`;
-}
-async function syncPrivateDataQuota(deltaBytes) {
- const pool = getQuotaPool();
- if (!pool || !deltaBytes) return null;
- const conn = await pool.getConnection();
- try {
- await conn.beginTransaction();
- const quota = await getQuotaState({ forUpdate: true, conn });
- if (!quota) {
- await conn.rollback();
- return null;
- }
- if (quota.status !== "active") throw new Error("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u5199");
- if (deltaBytes > quota.availableBytes) {
- throw new Error(`\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u914D\u989D\u4E0D\u8DB3\uFF1A\u9700\u8981 ${deltaBytes} \u5B57\u8282\uFF0C\u53EF\u7528 ${quota.availableBytes} \u5B57\u8282`);
- }
- await conn.query(
- `UPDATE h5_user_spaces
- SET used_bytes = GREATEST(0, used_bytes + ?), updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [deltaBytes, Date.now(), quota.id, PRIVATE_DATA_USER_ID]
- );
- await conn.commit();
- return { deltaBytes };
- } catch (err) {
- await conn.rollback();
- throw err;
- } finally {
- conn.release();
- }
-}
-function stripSqlComments(sql) {
- return String(sql ?? "").replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
-}
-function rejectDangerousSql(sql, { readonly = false } = {}) {
- const cleaned = stripSqlComments(sql);
- if (!cleaned) throw new Error("SQL \u4E0D\u80FD\u4E3A\u7A7A");
- if (cleaned.length > 2e4) throw new Error("SQL \u8FC7\u957F");
- if (/^\s*\./m.test(cleaned)) {
- throw new Error("SQL \u4E0D\u5141\u8BB8\u4F7F\u7528 sqlite3 dot command");
- }
- if (/\b(ATTACH|DETACH|LOAD_EXTENSION|VACUUM\s+INTO)\b/i.test(cleaned)) {
- throw new Error("SQL \u5305\u542B\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u4E0D\u5141\u8BB8\u7684\u64CD\u4F5C");
- }
- if (/\bPRAGMA\s+writable_schema\b/i.test(cleaned)) {
- throw new Error("SQL \u5305\u542B\u4E0D\u5141\u8BB8\u7684 PRAGMA");
- }
- if (readonly && !/^\s*(SELECT|WITH)\b/i.test(cleaned)) {
- throw new Error("private_data_query \u53EA\u5141\u8BB8 SELECT/WITH");
- }
- return cleaned;
-}
-async function queryPrivateData(sql) {
- const db = ensurePrivateDataDb();
- const cleaned = rejectDangerousSql(sql, { readonly: true });
- const limited = `SELECT * FROM (${cleaned.replace(/;\s*$/, "")}) LIMIT ${PRIVATE_DATA_MAX_ROWS}`;
- const output = runSqlite(["-json", db, limited]);
- return output.trim() || "[]";
-}
-async function executePrivateData(sql) {
- const db = ensurePrivateDataDb();
- const before = privateDataSize();
- if (before > PRIVATE_DATA_MAX_BYTES) throw new Error("\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u5DF2\u8D85\u8FC7\u5927\u5C0F\u9650\u5236");
- const cleaned = rejectDangerousSql(sql);
- const quotaPragma = await sqliteMaxPagePragmaForQuota(before);
- runSqlite(["-batch", db, `${quotaPragma}
-${cleaned}`]);
- const after = privateDataSize();
- if (after > PRIVATE_DATA_MAX_BYTES) {
- throw new Error(`\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u8D85\u8FC7\u5927\u5C0F\u9650\u5236\uFF1A${after}/${PRIVATE_DATA_MAX_BYTES} \u5B57\u8282`);
- }
- const delta = after - before;
- const quotaSync = await syncPrivateDataQuota(delta);
- return `\u5DF2\u6267\u884C\u3002\u5F53\u524D\u6570\u636E\u7A7A\u95F4\u5927\u5C0F ${after} \u5B57\u8282${quotaSync ? `\uFF0C\u5DF2\u540C\u6B65\u7A7A\u95F4\u5360\u7528 ${delta} \u5B57\u8282` : ""}`;
-}
-async function privateDataSchema() {
- const db = ensurePrivateDataDb();
- const output = runSqlite([
- "-json",
- db,
- `SELECT m.name AS table_name, p.cid, p.name AS column_name, p.type, p."notnull" AS not_null, p.pk
- FROM sqlite_master m
- JOIN pragma_table_info(m.name) p
- WHERE m.type = 'table' AND m.name NOT LIKE 'sqlite_%'
- ORDER BY m.name, p.cid`
- ]);
- return output.trim() || "[]";
-}
-async function callTool(name, args) {
- if (ALLOWED_TOOLS && !ALLOWED_TOOLS.has(name)) {
- throw new Error(`\u5DE5\u5177 ${name} \u672A\u6388\u6743`);
- }
- switch (name) {
- case "read_file": {
- const abs = resolveSandboxed(args.path);
- const content = fs2.readFileSync(abs, "utf8");
- return [{ type: "text", text: content }];
- }
- case "write_file": {
- const abs = resolveSandboxed(args.path);
- 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 = 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 ?? "";
- 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 = 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);
- 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();
- const quota = await getQuotaState().catch(() => null);
- return [
- {
- type: "text",
- text: JSON.stringify(
- {
- name: "\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4",
- database: ".mindspace/private-data.sqlite",
- maxBytes: PRIVATE_DATA_MAX_BYTES,
- sizeBytes: size,
- quotaSyncEnabled: Boolean(getQuotaPool()),
- quota: quota ? {
- status: quota.status,
- quotaBytes: quota.quotaBytes,
- usedBytes: quota.usedBytes,
- reservedBytes: quota.reservedBytes,
- availableBytes: quota.availableBytes
- } : null,
- rules: [
- "\u6BCF\u4E2A\u7528\u6237\u53EA\u80FD\u4F7F\u7528\u8FD9\u4E2A\u552F\u4E00 SQLite \u6570\u636E\u5E93",
- "\u9002\u5408\u95EE\u5377\u3001\u8868\u5355\u3001\u6E05\u5355\u3001\u8C03\u7814\u6570\u636E\u548C\u5206\u6790\u4E2D\u95F4\u8868",
- "\u4E0D\u8981\u5B58\u8D26\u53F7\u3001\u8BA1\u8D39\u3001\u6743\u9650\u3001\u5BA1\u8BA1\u3001\u516C\u5F00\u5E73\u53F0\u6570\u636E\u6216\u8DE8\u7528\u6237\u6570\u636E"
- ]
- },
- null,
- 2
- )
- }
- ];
- }
- case "private_data_schema":
- return [{ type: "text", text: await privateDataSchema() }];
- case "private_data_query":
- return [{ type: "text", text: await queryPrivateData(args.sql) }];
- case "private_data_execute":
- return [{ type: "text", text: await executePrivateData(args.sql) }];
- case "schedule_create_item": {
- const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? "Asia/Shanghai";
- const startAt = resolveScheduleTimestamp({
- epochMs: args.startAt,
- localString: args.startLocal,
- timezone,
- fieldName: "\u5F00\u59CB\u65F6\u95F4"
- });
- const endAt = resolveScheduleTimestamp({
- epochMs: args.endAt,
- localString: args.endLocal,
- timezone,
- fieldName: "\u7ED3\u675F\u65F6\u95F4"
- });
- const dueAt = resolveScheduleTimestamp({
- epochMs: args.dueAt,
- localString: args.dueLocal,
- timezone,
- fieldName: "\u622A\u6B62\u65F6\u95F4"
- });
- const item = await getScheduleService().createItem({
- userId: PRIVATE_DATA_USER_ID,
- kind: args.kind,
- title: args.title,
- description: args.description ?? null,
- startAt,
- endAt,
- dueAt,
- allDay: Boolean(args.allDay),
- timezone,
- location: args.location ?? null,
- sourceChannel: "agent",
- sourceMessageId: args.sourceMessageId ?? null,
- sourceText: args.sourceText ?? null,
- metadata: { source: "schedule_assistant_skill" }
- });
- return [{ type: "text", text: JSON.stringify(item, null, 2) }];
- }
- case "schedule_create_reminder": {
- const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? "Asia/Shanghai";
- const remindAt = resolveScheduleTimestamp({
- epochMs: args.remindAt,
- localString: args.remindLocal,
- timezone,
- fieldName: "\u63D0\u9192\u65F6\u95F4"
- });
- if (remindAt == null) throw new Error("\u7F3A\u5C11\u63D0\u9192\u65F6\u95F4 remindLocal \u6216 remindAt");
- const reminder = await getScheduleService().createReminder({
- userId: PRIVATE_DATA_USER_ID,
- itemId: args.itemId,
- remindAt,
- offsetMinutes: args.offsetMinutes ?? null,
- channel: args.channel ?? "wechat"
- });
- return [{ type: "text", text: JSON.stringify(reminder, null, 2) }];
- }
- case "schedule_list_items": {
- const items = await getScheduleService().listItems({
- userId: PRIVATE_DATA_USER_ID,
- from: args.from ?? null,
- to: args.to ?? null,
- status: args.status ?? "active",
- limit: args.limit ?? 50
- });
- return [{ type: "text", text: JSON.stringify(items, null, 2) }];
- }
- default:
- throw new Error(`\u672A\u77E5\u5DE5\u5177\uFF1A${name}`);
- }
-}
-function send(obj) {
- process.stdout.write(JSON.stringify(obj) + "\n");
-}
-function respond(id, result) {
- send({ jsonrpc: "2.0", id, result });
-}
-function respondError(id, message, code = -32603) {
- send({ jsonrpc: "2.0", id, error: { code, message } });
-}
-var rl = readline.createInterface({ input: process.stdin, terminal: false });
-rl.on("line", async (raw) => {
- const line = raw.trim();
- if (!line) return;
- let msg;
- try {
- msg = JSON.parse(line);
- } catch {
- process.stderr.write(`[mindspace-sandbox-mcp] invalid JSON: ${line}
-`);
- return;
- }
- const { id, method, params } = msg;
- switch (method) {
- case "initialize":
- respond(id, {
- protocolVersion: "2024-11-05",
- serverInfo: { name: "mindspace-sandbox", version: "1.0.0" },
- capabilities: { tools: {} }
- });
- break;
- case "initialized":
- break;
- case "tools/list":
- respond(id, { tools: TOOLS });
- break;
- case "tools/call": {
- const { name, arguments: toolArgs } = params ?? {};
- try {
- const content = await callTool(name, toolArgs ?? {});
- respond(id, { content, isError: false });
- } catch (err) {
- respond(id, {
- content: [{ type: "text", text: err.message }],
- isError: true
- });
- }
- break;
- }
- default:
- if (id !== void 0 && id !== null) {
- respondError(id, `Method not found: ${method}`, -32601);
- }
- }
-});
-rl.on("close", () => process.exit(0));
diff --git a/.runtime/portal/package.json b/.runtime/portal/package.json
deleted file mode 100644
index e3e6c30..0000000
--- a/.runtime/portal/package.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "tkmind-h5-portal-runtime",
- "private": true,
- "type": "module",
- "engines": {
- "node": ">=22"
- },
- "dependencies": {
- "@node-rs/argon2": "^2.0.2",
- "@resvg/resvg-js": "^2.6.2",
- "debug": "^4.4.3",
- "express": "^4.21.2",
- "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",
- "undici": "^6.26.0"
- }
-}
diff --git a/.runtime/portal/public/MP_verify_nXjhx0ErC6MhQ0SZ.txt b/.runtime/portal/public/MP_verify_nXjhx0ErC6MhQ0SZ.txt
deleted file mode 100644
index c6b1270..0000000
--- a/.runtime/portal/public/MP_verify_nXjhx0ErC6MhQ0SZ.txt
+++ /dev/null
@@ -1 +0,0 @@
-nXjhx0ErC6MhQ0SZ
diff --git a/.runtime/portal/public/brand/tkmind-icon.png b/.runtime/portal/public/brand/tkmind-icon.png
deleted file mode 100644
index 072f9ab..0000000
Binary files a/.runtime/portal/public/brand/tkmind-icon.png and /dev/null differ
diff --git a/.runtime/portal/public/dev/wechat-share-demo.html b/.runtime/portal/public/dev/wechat-share-demo.html
deleted file mode 100644
index 2b5f0a7..0000000
--- a/.runtime/portal/public/dev/wechat-share-demo.html
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
- 微信分享预览 · 连云港三天两夜攻略 🌊
-
-
-
-
-
微信链接卡片预览
-
本地模拟的是「粘贴链接后看到的卡片」,不是 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
deleted file mode 100644
index 761f5ee..0000000
--- a/.runtime/portal/public/dev/wechat-share-preview.html
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
- 微信分享预览工具
-
-
-
-
-
微信分享预览
-
输入已发布页面路径,本地模拟微信「粘贴链接后」看到的卡片。开发环境走 Portal API:/dev/wechat-share-preview
-
-
-
-
-
diff --git a/.runtime/portal/public/hello-john.html b/.runtime/portal/public/hello-john.html
deleted file mode 100644
index 992c5f1..0000000
--- a/.runtime/portal/public/hello-john.html
+++ /dev/null
@@ -1,159 +0,0 @@
-
-
-
-
-
-
-
- Hello, John!
-
-
-
-
-
-
-
-
-
👋
-
Hello!
-
Welcome, John
-
-
-
-
diff --git a/.runtime/portal/public/plaza-covers/business-1bt5ajr.jpg b/.runtime/portal/public/plaza-covers/business-1bt5ajr.jpg
deleted file mode 100644
index 80de088..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-1bt5ajr.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-1dpghh8.jpg b/.runtime/portal/public/plaza-covers/business-1dpghh8.jpg
deleted file mode 100644
index 76d994a..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-1dpghh8.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-30-84q88u.jpg b/.runtime/portal/public/plaza-covers/business-30-84q88u.jpg
deleted file mode 100644
index a22930d..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-30-84q88u.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-coffee-shop.jpg b/.runtime/portal/public/plaza-covers/business-coffee-shop.jpg
deleted file mode 100644
index 9117550..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-coffee-shop.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-e09bef.jpg b/.runtime/portal/public/plaza-covers/business-e09bef.jpg
deleted file mode 100644
index 80de088..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-e09bef.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-nfc9ph.jpg b/.runtime/portal/public/plaza-covers/business-nfc9ph.jpg
deleted file mode 100644
index 27b08b0..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-nfc9ph.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-pilates.jpg b/.runtime/portal/public/plaza-covers/business-pilates.jpg
deleted file mode 100644
index e7ad1cc..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-pilates.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-sop-33nw2t.jpg b/.runtime/portal/public/plaza-covers/business-sop-33nw2t.jpg
deleted file mode 100644
index 27b08b0..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-sop-33nw2t.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-zfht2z.jpg b/.runtime/portal/public/plaza-covers/business-zfht2z.jpg
deleted file mode 100644
index 2da05d3..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-zfht2z.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-宠物洗护上门服务-sop-手册.jpg b/.runtime/portal/public/plaza-covers/business-宠物洗护上门服务-sop-手册.jpg
deleted file mode 100644
index 27b08b0..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-宠物洗护上门服务-sop-手册.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-日式美甲沙龙会员体系设计.jpg b/.runtime/portal/public/plaza-covers/business-日式美甲沙龙会员体系设计.jpg
deleted file mode 100644
index 80de088..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-日式美甲沙龙会员体系设计.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-民宿旺季定价与房源包装指南.jpg b/.runtime/portal/public/plaza-covers/business-民宿旺季定价与房源包装指南.jpg
deleted file mode 100644
index 2da05d3..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-民宿旺季定价与房源包装指南.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-独立书店文创周边选品策略.jpg b/.runtime/portal/public/plaza-covers/business-独立书店文创周边选品策略.jpg
deleted file mode 100644
index 76d994a..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-独立书店文创周边选品策略.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-社区健身房私教转化漏斗优化.jpg b/.runtime/portal/public/plaza-covers/business-社区健身房私教转化漏斗优化.jpg
deleted file mode 100644
index 27b08b0..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-社区健身房私教转化漏斗优化.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-精酿啤酒馆开业-30-天数据复盘.jpg b/.runtime/portal/public/plaza-covers/business-精酿啤酒馆开业-30-天数据复盘.jpg
deleted file mode 100644
index a22930d..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-精酿啤酒馆开业-30-天数据复盘.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/business-轻食沙拉店午餐高峰运营方案.jpg b/.runtime/portal/public/plaza-covers/business-轻食沙拉店午餐高峰运营方案.jpg
deleted file mode 100644
index 80de088..0000000
Binary files a/.runtime/portal/public/plaza-covers/business-轻食沙拉店午餐高峰运营方案.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-17bb2q5.jpg b/.runtime/portal/public/plaza-covers/creative-17bb2q5.jpg
deleted file mode 100644
index 3330d02..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-17bb2q5.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-1a69un3.jpg b/.runtime/portal/public/plaza-covers/creative-1a69un3.jpg
deleted file mode 100644
index dbf1769..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-1a69un3.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-1wuxajv.jpg b/.runtime/portal/public/plaza-covers/creative-1wuxajv.jpg
deleted file mode 100644
index 1e93c59..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-1wuxajv.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-ai-1m59cjr.jpg b/.runtime/portal/public/plaza-covers/creative-ai-1m59cjr.jpg
deleted file mode 100644
index dbf1769..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-ai-1m59cjr.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-ceramics-1o5zmj5.jpg b/.runtime/portal/public/plaza-covers/creative-ceramics-1o5zmj5.jpg
deleted file mode 100644
index 495339d..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-ceramics-1o5zmj5.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-cyber-city.jpg b/.runtime/portal/public/plaza-covers/creative-cyber-city.jpg
deleted file mode 100644
index 980e792..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-cyber-city.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-music-ep.jpg b/.runtime/portal/public/plaza-covers/creative-music-ep.jpg
deleted file mode 100644
index 980e792..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-music-ep.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-ui-200-icons-12lpy7p.jpg b/.runtime/portal/public/plaza-covers/creative-ui-200-icons-12lpy7p.jpg
deleted file mode 100644
index 3330d02..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-ui-200-icons-12lpy7p.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-vis-tkrnir.jpg b/.runtime/portal/public/plaza-covers/creative-vis-tkrnir.jpg
deleted file mode 100644
index 1e93c59..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-vis-tkrnir.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-国潮茶饮品牌-vis-视觉系统.jpg b/.runtime/portal/public/plaza-covers/creative-国潮茶饮品牌-vis-视觉系统.jpg
deleted file mode 100644
index 1e93c59..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-国潮茶饮品牌-vis-视觉系统.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-城市夜景长曝光摄影系列.jpg b/.runtime/portal/public/plaza-covers/creative-城市夜景长曝光摄影系列.jpg
deleted file mode 100644
index 3330d02..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-城市夜景长曝光摄影系列.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-复古胶片人像调色预设包.jpg b/.runtime/portal/public/plaza-covers/creative-复古胶片人像调色预设包.jpg
deleted file mode 100644
index dbf1769..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-复古胶片人像调色预设包.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-手作-ceramics-工作室品牌摄影.jpg b/.runtime/portal/public/plaza-covers/creative-手作-ceramics-工作室品牌摄影.jpg
deleted file mode 100644
index 495339d..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-手作-ceramics-工作室品牌摄影.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-极简-ui-图标集-200-icons.jpg b/.runtime/portal/public/plaza-covers/creative-极简-ui-图标集-200-icons.jpg
deleted file mode 100644
index 3330d02..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-极简-ui-图标集-200-icons.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-水墨风-ai-插画实验合集.jpg b/.runtime/portal/public/plaza-covers/creative-水墨风-ai-插画实验合集.jpg
deleted file mode 100644
index dbf1769..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-水墨风-ai-插画实验合集.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/creative-科幻短片-归途-分镜脚本.jpg b/.runtime/portal/public/plaza-covers/creative-科幻短片-归途-分镜脚本.jpg
deleted file mode 100644
index 1e93c59..0000000
Binary files a/.runtime/portal/public/plaza-covers/creative-科幻短片-归途-分镜脚本.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-1293jxp.jpg b/.runtime/portal/public/plaza-covers/data-analysis-1293jxp.jpg
deleted file mode 100644
index 1b25426..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-1293jxp.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-a-b-1slrtg.jpg b/.runtime/portal/public/plaza-covers/data-analysis-a-b-1slrtg.jpg
deleted file mode 100644
index 0969588..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-a-b-1slrtg.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-a-b-测试显著性检验指南.jpg b/.runtime/portal/public/plaza-covers/data-analysis-a-b-测试显著性检验指南.jpg
deleted file mode 100644
index 0969588..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-a-b-测试显著性检验指南.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-arima-vs-prophet-144eeao.jpg b/.runtime/portal/public/plaza-covers/data-analysis-arima-vs-prophet-144eeao.jpg
deleted file mode 100644
index bdf35c2..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-arima-vs-prophet-144eeao.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-cohort.jpg b/.runtime/portal/public/plaza-covers/data-analysis-cohort.jpg
deleted file mode 100644
index b5db113..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-cohort.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-g6tcgg.jpg b/.runtime/portal/public/plaza-covers/data-analysis-g6tcgg.jpg
deleted file mode 100644
index bdf35c2..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-g6tcgg.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-gmv.jpg b/.runtime/portal/public/plaza-covers/data-analysis-gmv.jpg
deleted file mode 100644
index 5b146d7..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-gmv.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-shapley-vs-markov-11a7w0y.jpg b/.runtime/portal/public/plaza-covers/data-analysis-shapley-vs-markov-11a7w0y.jpg
deleted file mode 100644
index a39faf3..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-shapley-vs-markov-11a7w0y.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-sql-9sel2d.jpg b/.runtime/portal/public/plaza-covers/data-analysis-sql-9sel2d.jpg
deleted file mode 100644
index 1b25426..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-sql-9sel2d.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-ztijjg.jpg b/.runtime/portal/public/plaza-covers/data-analysis-ztijjg.jpg
deleted file mode 100644
index 0969588..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-ztijjg.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-实时大屏指标设计与-sql-模板.jpg b/.runtime/portal/public/plaza-covers/data-analysis-实时大屏指标设计与-sql-模板.jpg
deleted file mode 100644
index 1b25426..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-实时大屏指标设计与-sql-模板.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-数据质量监控规则库搭建.jpg b/.runtime/portal/public/plaza-covers/data-analysis-数据质量监控规则库搭建.jpg
deleted file mode 100644
index bdf35c2..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-数据质量监控规则库搭建.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-文本情感分析-评论洞察报告.jpg b/.runtime/portal/public/plaza-covers/data-analysis-文本情感分析-评论洞察报告.jpg
deleted file mode 100644
index 1b25426..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-文本情感分析-评论洞察报告.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-用户行为漏斗分析实战案例.jpg b/.runtime/portal/public/plaza-covers/data-analysis-用户行为漏斗分析实战案例.jpg
deleted file mode 100644
index 0969588..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-用户行为漏斗分析实战案例.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-营销归因-shapley-vs-markov.jpg b/.runtime/portal/public/plaza-covers/data-analysis-营销归因-shapley-vs-markov.jpg
deleted file mode 100644
index a39faf3..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-营销归因-shapley-vs-markov.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/data-analysis-销售预测模型-arima-vs-prophet.jpg b/.runtime/portal/public/plaza-covers/data-analysis-销售预测模型-arima-vs-prophet.jpg
deleted file mode 100644
index bdf35c2..0000000
Binary files a/.runtime/portal/public/plaza-covers/data-analysis-销售预测模型-arima-vs-prophet.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-100-40-l2vjup.jpg b/.runtime/portal/public/plaza-covers/lifestyle-100-40-l2vjup.jpg
deleted file mode 100644
index e2e61e9..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-100-40-l2vjup.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-1o3s87z.jpg b/.runtime/portal/public/plaza-covers/lifestyle-1o3s87z.jpg
deleted file mode 100644
index e9f9bda..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-1o3s87z.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-1wfbcos.jpg b/.runtime/portal/public/plaza-covers/lifestyle-1wfbcos.jpg
deleted file mode 100644
index f73669e..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-1wfbcos.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-30-173irnz.jpg b/.runtime/portal/public/plaza-covers/lifestyle-30-173irnz.jpg
deleted file mode 100644
index e9f9bda..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-30-173irnz.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-30-天早睡挑战执行记录.jpg b/.runtime/portal/public/plaza-covers/lifestyle-30-天早睡挑战执行记录.jpg
deleted file mode 100644
index e9f9bda..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-30-天早睡挑战执行记录.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-7-1cwt66m.jpg b/.runtime/portal/public/plaza-covers/lifestyle-7-1cwt66m.jpg
deleted file mode 100644
index ea7b9b7..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-7-1cwt66m.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-desk.jpg b/.runtime/portal/public/plaza-covers/lifestyle-desk.jpg
deleted file mode 100644
index 8599dcc..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-desk.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-m33c9i.jpg b/.runtime/portal/public/plaza-covers/lifestyle-m33c9i.jpg
deleted file mode 100644
index ea7b9b7..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-m33c9i.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-o90i4e.jpg b/.runtime/portal/public/plaza-covers/lifestyle-o90i4e.jpg
deleted file mode 100644
index ea7b9b7..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-o90i4e.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-writing.jpg b/.runtime/portal/public/plaza-covers/lifestyle-writing.jpg
deleted file mode 100644
index 8599dcc..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-writing.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-厨房收纳改造-小空间大利用.jpg b/.runtime/portal/public/plaza-covers/lifestyle-厨房收纳改造-小空间大利用.jpg
deleted file mode 100644
index f73669e..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-厨房收纳改造-小空间大利用.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-周末徒步装备清单与路线.jpg b/.runtime/portal/public/plaza-covers/lifestyle-周末徒步装备清单与路线.jpg
deleted file mode 100644
index ea7b9b7..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-周末徒步装备清单与路线.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-周末窑烤面包新手日记.jpg b/.runtime/portal/public/plaza-covers/lifestyle-周末窑烤面包新手日记.jpg
deleted file mode 100644
index e9f9bda..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-周末窑烤面包新手日记.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-手冲咖啡入门-7-日练习.jpg b/.runtime/portal/public/plaza-covers/lifestyle-手冲咖啡入门-7-日练习.jpg
deleted file mode 100644
index ea7b9b7..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-手冲咖啡入门-7-日练习.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-断舍离-衣柜-100-件到-40-件.jpg b/.runtime/portal/public/plaza-covers/lifestyle-断舍离-衣柜-100-件到-40-件.jpg
deleted file mode 100644
index e2e61e9..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-断舍离-衣柜-100-件到-40-件.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/lifestyle-阳台花园从零搭建指南.jpg b/.runtime/portal/public/plaza-covers/lifestyle-阳台花园从零搭建指南.jpg
deleted file mode 100644
index ea7b9b7..0000000
Binary files a/.runtime/portal/public/plaza-covers/lifestyle-阳台花园从零搭建指南.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-0-1000-35irqz.jpg b/.runtime/portal/public/plaza-covers/other-0-1000-35irqz.jpg
deleted file mode 100644
index 6d3eb2b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-0-1000-35irqz.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-176h7rn.jpg b/.runtime/portal/public/plaza-covers/other-176h7rn.jpg
deleted file mode 100644
index 6d3eb2b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-176h7rn.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-187ouof.jpg b/.runtime/portal/public/plaza-covers/other-187ouof.jpg
deleted file mode 100644
index 3c2e59b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-187ouof.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-1nd2tz4.jpg b/.runtime/portal/public/plaza-covers/other-1nd2tz4.jpg
deleted file mode 100644
index 5dca65c..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-1nd2tz4.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-20-1de41cz.jpg b/.runtime/portal/public/plaza-covers/other-20-1de41cz.jpg
deleted file mode 100644
index 6d3eb2b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-20-1de41cz.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-opensource.jpg b/.runtime/portal/public/plaza-covers/other-opensource.jpg
deleted file mode 100644
index 21dfb28..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-opensource.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-vr-60-1yt52pj.jpg b/.runtime/portal/public/plaza-covers/other-vr-60-1yt52pj.jpg
deleted file mode 100644
index 3c2e59b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-vr-60-1yt52pj.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-vr-健身-60-天体验报告.jpg b/.runtime/portal/public/plaza-covers/other-vr-健身-60-天体验报告.jpg
deleted file mode 100644
index 3c2e59b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-vr-健身-60-天体验报告.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-wishlist.jpg b/.runtime/portal/public/plaza-covers/other-wishlist.jpg
deleted file mode 100644
index 6bef7a3..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-wishlist.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-xu49pe.jpg b/.runtime/portal/public/plaza-covers/other-xu49pe.jpg
deleted file mode 100644
index 15cbd88..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-xu49pe.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-个人知识库搭建方法论.jpg b/.runtime/portal/public/plaza-covers/other-个人知识库搭建方法论.jpg
deleted file mode 100644
index 3c2e59b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-个人知识库搭建方法论.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-二手交易向可持续生活转型.jpg b/.runtime/portal/public/plaza-covers/other-二手交易向可持续生活转型.jpg
deleted file mode 100644
index 6d3eb2b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-二手交易向可持续生活转型.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-城市观鸟入门-常见-20-种鸟.jpg b/.runtime/portal/public/plaza-covers/other-城市观鸟入门-常见-20-种鸟.jpg
deleted file mode 100644
index 6d3eb2b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-城市观鸟入门-常见-20-种鸟.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-播客频道从-0-到-1000-订阅.jpg b/.runtime/portal/public/plaza-covers/other-播客频道从-0-到-1000-订阅.jpg
deleted file mode 100644
index 6d3eb2b..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-播客频道从-0-到-1000-订阅.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-社群运营-从群聊到价值观.jpg b/.runtime/portal/public/plaza-covers/other-社群运营-从群聊到价值观.jpg
deleted file mode 100644
index 15cbd88..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-社群运营-从群聊到价值观.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/other-自由职业者财务规划入门.jpg b/.runtime/portal/public/plaza-covers/other-自由职业者财务规划入门.jpg
deleted file mode 100644
index 5dca65c..0000000
Binary files a/.runtime/portal/public/plaza-covers/other-自由职业者财务规划入门.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-6owsyr.jpg b/.runtime/portal/public/plaza-covers/study-notes-6owsyr.jpg
deleted file mode 100644
index a48a853..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-6owsyr.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-docker-1adiwh7.jpg b/.runtime/portal/public/plaza-covers/study-notes-docker-1adiwh7.jpg
deleted file mode 100644
index a48a853..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-docker-1adiwh7.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-docker-容器化从零到部署.jpg b/.runtime/portal/public/plaza-covers/study-notes-docker-容器化从零到部署.jpg
deleted file mode 100644
index a48a853..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-docker-容器化从零到部署.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-fobvok.jpg b/.runtime/portal/public/plaza-covers/study-notes-fobvok.jpg
deleted file mode 100644
index 15cbd88..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-fobvok.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-git-code-review-1avjty.jpg b/.runtime/portal/public/plaza-covers/study-notes-git-code-review-1avjty.jpg
deleted file mode 100644
index edd5abf..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-git-code-review-1avjty.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-git-团队分支规范与-code-review.jpg b/.runtime/portal/public/plaza-covers/study-notes-git-团队分支规范与-code-review.jpg
deleted file mode 100644
index edd5abf..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-git-团队分支规范与-code-review.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-llm.jpg b/.runtime/portal/public/plaza-covers/study-notes-llm.jpg
deleted file mode 100644
index 32c7d0c..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-llm.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-pandas-1mg4mom.jpg b/.runtime/portal/public/plaza-covers/study-notes-pandas-1mg4mom.jpg
deleted file mode 100644
index edd5abf..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-pandas-1mg4mom.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-pandas-数据分析速查手册.jpg b/.runtime/portal/public/plaza-covers/study-notes-pandas-数据分析速查手册.jpg
deleted file mode 100644
index edd5abf..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-pandas-数据分析速查手册.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-pca-54364r.jpg b/.runtime/portal/public/plaza-covers/study-notes-pca-54364r.jpg
deleted file mode 100644
index a48a853..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-pca-54364r.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-rust.jpg b/.runtime/portal/public/plaza-covers/study-notes-rust.jpg
deleted file mode 100644
index 3fa0a55..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-rust.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-typescript-10-1sddjtb.jpg b/.runtime/portal/public/plaza-covers/study-notes-typescript-10-1sddjtb.jpg
deleted file mode 100644
index a48a853..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-typescript-10-1sddjtb.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-typescript-类型体操-10-道经典题解析.jpg b/.runtime/portal/public/plaza-covers/study-notes-typescript-类型体操-10-道经典题解析.jpg
deleted file mode 100644
index a48a853..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-typescript-类型体操-10-道经典题解析.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-技术文档英语阅读训练计划.jpg b/.runtime/portal/public/plaza-covers/study-notes-技术文档英语阅读训练计划.jpg
deleted file mode 100644
index 15cbd88..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-技术文档英语阅读训练计划.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-系统设计面试-分布式缓存怎么答.jpg b/.runtime/portal/public/plaza-covers/study-notes-系统设计面试-分布式缓存怎么答.jpg
deleted file mode 100644
index a48a853..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-系统设计面试-分布式缓存怎么答.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/study-notes-线性代数直觉笔记-特征值与-pca.jpg b/.runtime/portal/public/plaza-covers/study-notes-线性代数直觉笔记-特征值与-pca.jpg
deleted file mode 100644
index a48a853..0000000
Binary files a/.runtime/portal/public/plaza-covers/study-notes-线性代数直觉笔记-特征值与-pca.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-10-ap683v.jpg b/.runtime/portal/public/plaza-covers/travel-10-ap683v.jpg
deleted file mode 100644
index d7437c5..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-10-ap683v.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-15qb47w.jpg b/.runtime/portal/public/plaza-covers/travel-15qb47w.jpg
deleted file mode 100644
index a178c53..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-15qb47w.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-3-1lbpfse.jpg b/.runtime/portal/public/plaza-covers/travel-3-1lbpfse.jpg
deleted file mode 100644
index f5e5f1f..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-3-1lbpfse.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-4-70oznz.jpg b/.runtime/portal/public/plaza-covers/travel-4-70oznz.jpg
deleted file mode 100644
index 4490c59..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-4-70oznz.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-48-p5s92m.jpg b/.runtime/portal/public/plaza-covers/travel-48-p5s92m.jpg
deleted file mode 100644
index f5e5f1f..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-48-p5s92m.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-6-1hwz8po.jpg b/.runtime/portal/public/plaza-covers/travel-6-1hwz8po.jpg
deleted file mode 100644
index a178c53..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-6-1hwz8po.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-fjj9j0.jpg b/.runtime/portal/public/plaza-covers/travel-fjj9j0.jpg
deleted file mode 100644
index a178c53..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-fjj9j0.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-kyoto.jpg b/.runtime/portal/public/plaza-covers/travel-kyoto.jpg
deleted file mode 100644
index 2bd1d1d..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-kyoto.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-malaysia.jpg b/.runtime/portal/public/plaza-covers/travel-malaysia.jpg
deleted file mode 100644
index 2bd1d1d..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-malaysia.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-云南大理丽江-6-日省钱攻略.jpg b/.runtime/portal/public/plaza-covers/travel-云南大理丽江-6-日省钱攻略.jpg
deleted file mode 100644
index a178c53..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-云南大理丽江-6-日省钱攻略.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-冰岛环岛-10-日自驾攻略.jpg b/.runtime/portal/public/plaza-covers/travel-冰岛环岛-10-日自驾攻略.jpg
deleted file mode 100644
index d7437c5..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-冰岛环岛-10-日自驾攻略.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-新加坡亲子-3-日轻松游.jpg b/.runtime/portal/public/plaza-covers/travel-新加坡亲子-3-日轻松游.jpg
deleted file mode 100644
index f5e5f1f..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-新加坡亲子-3-日轻松游.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-清迈慢生活-4-日深度游.jpg b/.runtime/portal/public/plaza-covers/travel-清迈慢生活-4-日深度游.jpg
deleted file mode 100644
index 4490c59..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-清迈慢生活-4-日深度游.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-西藏林芝桃花季摄影路线.jpg b/.runtime/portal/public/plaza-covers/travel-西藏林芝桃花季摄影路线.jpg
deleted file mode 100644
index a178c53..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-西藏林芝桃花季摄影路线.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-釜山海鲜美食-48-小时.jpg b/.runtime/portal/public/plaza-covers/travel-釜山海鲜美食-48-小时.jpg
deleted file mode 100644
index f5e5f1f..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-釜山海鲜美食-48-小时.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/travel-阿尔卑斯徒步-少女峰周边.jpg b/.runtime/portal/public/plaza-covers/travel-阿尔卑斯徒步-少女峰周边.jpg
deleted file mode 100644
index a178c53..0000000
Binary files a/.runtime/portal/public/plaza-covers/travel-阿尔卑斯徒步-少女峰周边.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-1y4i5h1.jpg b/.runtime/portal/public/plaza-covers/work-report-1y4i5h1.jpg
deleted file mode 100644
index 206f41a..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-1y4i5h1.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-2025-1fz5i3.jpg b/.runtime/portal/public/plaza-covers/work-report-2025-1fz5i3.jpg
deleted file mode 100644
index 884182f..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-2025-1fz5i3.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-72ek9j.jpg b/.runtime/portal/public/plaza-covers/work-report-72ek9j.jpg
deleted file mode 100644
index 00bf9c0..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-72ek9j.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-ai-service.jpg b/.runtime/portal/public/plaza-covers/work-report-ai-service.jpg
deleted file mode 100644
index dc1d298..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-ai-service.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-b2b-saas-9-4vrvjf.jpg b/.runtime/portal/public/plaza-covers/work-report-b2b-saas-9-4vrvjf.jpg
deleted file mode 100644
index 884182f..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-b2b-saas-9-4vrvjf.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-b2b-saas-客户成功月报-9-月.jpg b/.runtime/portal/public/plaza-covers/work-report-b2b-saas-客户成功月报-9-月.jpg
deleted file mode 100644
index 884182f..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-b2b-saas-客户成功月报-9-月.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-disney.jpg b/.runtime/portal/public/plaza-covers/work-report-disney.jpg
deleted file mode 100644
index 49f866d..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-disney.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-explore.jpg b/.runtime/portal/public/plaza-covers/work-report-explore.jpg
deleted file mode 100644
index ef2d817..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-explore.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-growth.jpg b/.runtime/portal/public/plaza-covers/work-report-growth.jpg
deleted file mode 100644
index d8959b2..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-growth.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-hello.jpg b/.runtime/portal/public/plaza-covers/work-report-hello.jpg
deleted file mode 100644
index 5c69410..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-hello.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-mindspace-qwuy2q.jpg b/.runtime/portal/public/plaza-covers/work-report-mindspace-qwuy2q.jpg
deleted file mode 100644
index bdf35c2..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-mindspace-qwuy2q.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-mindspace-公开发布指南.jpg b/.runtime/portal/public/plaza-covers/work-report-mindspace-公开发布指南.jpg
deleted file mode 100644
index bdf35c2..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-mindspace-公开发布指南.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-okr-ivflp3.jpg b/.runtime/portal/public/plaza-covers/work-report-okr-ivflp3.jpg
deleted file mode 100644
index 00bf9c0..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-okr-ivflp3.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-q3-1v4dgld.jpg b/.runtime/portal/public/plaza-covers/work-report-q3-1v4dgld.jpg
deleted file mode 100644
index 509e7ba..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-q3-1v4dgld.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-sprint-42-8a7tiy.jpg b/.runtime/portal/public/plaza-covers/work-report-sprint-42-8a7tiy.jpg
deleted file mode 100644
index bdf35c2..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-sprint-42-8a7tiy.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-supply-chain.jpg b/.runtime/portal/public/plaza-covers/work-report-supply-chain.jpg
deleted file mode 100644
index 1782e04..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-supply-chain.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-产品经理年度-okr-回顾与反思.jpg b/.runtime/portal/public/plaza-covers/work-report-产品经理年度-okr-回顾与反思.jpg
deleted file mode 100644
index 00bf9c0..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-产品经理年度-okr-回顾与反思.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-品牌升级方案执行进度追踪.jpg b/.runtime/portal/public/plaza-covers/work-report-品牌升级方案执行进度追踪.jpg
deleted file mode 100644
index 00bf9c0..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-品牌升级方案执行进度追踪.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-研发团队-sprint-回顾-第-42-期.jpg b/.runtime/portal/public/plaza-covers/work-report-研发团队-sprint-回顾-第-42-期.jpg
deleted file mode 100644
index bdf35c2..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-研发团队-sprint-回顾-第-42-期.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-跨部门项目协同周报-模板与实践.jpg b/.runtime/portal/public/plaza-covers/work-report-跨部门项目协同周报-模板与实践.jpg
deleted file mode 100644
index 206f41a..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-跨部门项目协同周报-模板与实践.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-远程办公效率调研-2025-秋季.jpg b/.runtime/portal/public/plaza-covers/work-report-远程办公效率调研-2025-秋季.jpg
deleted file mode 100644
index 884182f..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-远程办公效率调研-2025-秋季.jpg and /dev/null differ
diff --git a/.runtime/portal/public/plaza-covers/work-report-销售团队-q3-业绩复盘与目标拆解.jpg b/.runtime/portal/public/plaza-covers/work-report-销售团队-q3-业绩复盘与目标拆解.jpg
deleted file mode 100644
index 509e7ba..0000000
Binary files a/.runtime/portal/public/plaza-covers/work-report-销售团队-q3-业绩复盘与目标拆解.jpg and /dev/null differ
diff --git a/.runtime/portal/public/thumbnail-demo/index.html b/.runtime/portal/public/thumbnail-demo/index.html
deleted file mode 100644
index 6c63f7f..0000000
--- a/.runtime/portal/public/thumbnail-demo/index.html
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
- MindSpace 预览图 Demo
-
-
-
- MindSpace 预览图升级 Demo
- 左侧为旧版横版缩略图,中间为新版 3:4 信息流封面(自动从 HTML 提取标题、配色、emoji),右侧为保存页面时实际写入的 thumbnail.svg。下方是小红书风格卡片效果。
-
-
-
- 样本 1
-
-
旧版(横版字卡)
-
新版(3:4 精美封面)
-
generateHtmlThumbnail 输出
-
-
-
-
-
-
马来西亚旅游攻略
-
旅行攻略 · 新版 3:4 封面
-
-
-
-
- 样本 2
-
-
旧版(横版字卡)
-
新版(3:4 精美封面)
-
generateHtmlThumbnail 输出
-
-
-
-
-
-
麻婆豆腐
-
美食专题 · 新版 3:4 封面
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.runtime/portal/public/thumbnail-demo/malaysia-travel-feed.svg b/.runtime/portal/public/thumbnail-demo/malaysia-travel-feed.svg
deleted file mode 100644
index aca64f1..0000000
--- a/.runtime/portal/public/thumbnail-demo/malaysia-travel-feed.svg
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
\ No newline at end of file
diff --git a/.runtime/portal/public/thumbnail-demo/malaysia-travel-generated.svg b/.runtime/portal/public/thumbnail-demo/malaysia-travel-generated.svg
deleted file mode 100644
index aca64f1..0000000
--- a/.runtime/portal/public/thumbnail-demo/malaysia-travel-generated.svg
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
\ No newline at end of file
diff --git a/.runtime/portal/public/thumbnail-demo/malaysia-travel-legacy.svg b/.runtime/portal/public/thumbnail-demo/malaysia-travel-legacy.svg
deleted file mode 100644
index e82c0f0..0000000
--- a/.runtime/portal/public/thumbnail-demo/malaysia-travel-legacy.svg
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
\ No newline at end of file
diff --git a/.runtime/portal/public/thumbnail-demo/mapo-tofu-feed.svg b/.runtime/portal/public/thumbnail-demo/mapo-tofu-feed.svg
deleted file mode 100644
index 0129022..0000000
--- a/.runtime/portal/public/thumbnail-demo/mapo-tofu-feed.svg
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
\ No newline at end of file
diff --git a/.runtime/portal/public/thumbnail-demo/mapo-tofu-generated.svg b/.runtime/portal/public/thumbnail-demo/mapo-tofu-generated.svg
deleted file mode 100644
index 0129022..0000000
--- a/.runtime/portal/public/thumbnail-demo/mapo-tofu-generated.svg
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
\ No newline at end of file
diff --git a/.runtime/portal/public/thumbnail-demo/mapo-tofu-legacy.svg b/.runtime/portal/public/thumbnail-demo/mapo-tofu-legacy.svg
deleted file mode 100644
index 3362f19..0000000
--- a/.runtime/portal/public/thumbnail-demo/mapo-tofu-legacy.svg
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
\ No newline at end of file
diff --git a/.runtime/portal/schema.sql b/.runtime/portal/schema.sql
deleted file mode 100644
index 984e3cd..0000000
--- a/.runtime/portal/schema.sql
+++ /dev/null
@@ -1,1174 +0,0 @@
-CREATE TABLE IF NOT EXISTS h5_users (
- id CHAR(36) PRIMARY KEY,
- username VARCHAR(64) NOT NULL UNIQUE,
- slug VARCHAR(64) NULL,
- email VARCHAR(255) NULL,
- display_name VARCHAR(128) NOT NULL,
- salt VARCHAR(64) NOT NULL,
- password_hash VARCHAR(128) NOT NULL,
- password_algorithm VARCHAR(32) NOT NULL DEFAULT 'pbkdf2-sha512',
- role ENUM('user', 'admin') NOT NULL DEFAULT 'user',
- 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),
- UNIQUE KEY uq_h5_users_email (email)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_spaces (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- space_name VARCHAR(128) NOT NULL DEFAULT '我的空间',
- quota_bytes BIGINT NOT NULL DEFAULT 5242880,
- used_bytes BIGINT NOT NULL DEFAULT 0,
- reserved_bytes BIGINT NOT NULL DEFAULT 0,
- status ENUM('active', 'locked', 'deleted') NOT NULL DEFAULT 'active',
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_user_space_user (user_id),
- CONSTRAINT fk_h5_space_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT chk_h5_space_quota CHECK (
- quota_bytes >= 0 AND used_bytes >= 0 AND reserved_bytes >= 0
- )
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS mindspace_config (
- `key` VARCHAR(64) PRIMARY KEY,
- value TEXT NOT NULL,
- description VARCHAR(255) NULL,
- updated_at BIGINT NOT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_space_categories (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- space_id CHAR(36) NOT NULL,
- category_code VARCHAR(32) NOT NULL,
- category_name VARCHAR(128) NOT NULL,
- visibility_policy VARCHAR(64) NOT NULL,
- ai_access_policy VARCHAR(64) NOT NULL,
- publish_policy VARCHAR(64) NOT NULL,
- is_system TINYINT(1) NOT NULL DEFAULT 0,
- sort_order INT NOT NULL DEFAULT 0,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_space_category_code (space_id, category_code),
- KEY idx_h5_space_category_user (user_id, sort_order),
- CONSTRAINT fk_h5_category_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_category_space FOREIGN KEY (space_id) REFERENCES h5_user_spaces(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_assets (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- space_id CHAR(36) NOT NULL,
- category_id CHAR(36) NOT NULL,
- parent_id CHAR(36) NULL,
- asset_type VARCHAR(32) NOT NULL,
- mime_type VARCHAR(128) NOT NULL,
- original_filename VARCHAR(255) NOT NULL,
- display_name VARCHAR(255) NOT NULL,
- current_version_id CHAR(36) NULL,
- size_bytes BIGINT NOT NULL DEFAULT 0,
- checksum CHAR(64) NOT NULL,
- risk_level ENUM('none', 'low', 'medium', 'high', 'critical') NOT NULL DEFAULT 'none',
- visibility ENUM('private', 'internal', 'public_candidate') NOT NULL DEFAULT 'private',
- status ENUM('uploaded', 'processing', 'ready', 'quarantined', 'archived', 'deleted') NOT NULL DEFAULT 'ready',
- source_type ENUM('upload', 'chat', 'agent', 'template', 'generated', 'workspace') NOT NULL DEFAULT 'upload',
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- deleted_at BIGINT NULL,
- KEY idx_h5_assets_category (user_id, category_id, updated_at),
- KEY idx_h5_assets_parent (user_id, parent_id),
- KEY idx_h5_assets_checksum (user_id, checksum),
- CONSTRAINT fk_h5_asset_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_asset_space FOREIGN KEY (space_id) REFERENCES h5_user_spaces(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_asset_category FOREIGN KEY (category_id) REFERENCES h5_space_categories(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_asset_parent FOREIGN KEY (parent_id) REFERENCES h5_assets(id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_asset_versions (
- id CHAR(36) PRIMARY KEY,
- asset_id CHAR(36) NOT NULL,
- version_no INT NOT NULL,
- storage_key VARCHAR(512) NOT NULL,
- size_bytes BIGINT NOT NULL,
- checksum CHAR(64) NOT NULL,
- mime_type VARCHAR(128) NOT NULL,
- created_by CHAR(36) NOT NULL,
- change_note VARCHAR(512) NULL,
- scan_status ENUM('pending', 'passed', 'warned', 'blocked') NOT NULL DEFAULT 'pending',
- created_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_asset_version (asset_id, version_no),
- UNIQUE KEY uq_h5_asset_storage_key (storage_key),
- CONSTRAINT fk_h5_asset_version_asset FOREIGN KEY (asset_id) REFERENCES h5_assets(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_asset_version_user FOREIGN KEY (created_by) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_upload_sessions (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- space_id CHAR(36) NOT NULL,
- category_id CHAR(36) NOT NULL,
- filename VARCHAR(255) NOT NULL,
- expected_size BIGINT NOT NULL,
- actual_size BIGINT NULL,
- declared_mime_type VARCHAR(128) NULL,
- detected_mime_type VARCHAR(128) NULL,
- reserved_bytes BIGINT NOT NULL,
- 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
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_page_records (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- space_id CHAR(36) NOT NULL,
- category_id CHAR(36) NOT NULL,
- source_session_id VARCHAR(128) NULL,
- source_message_id VARCHAR(128) NULL,
- source_asset_id CHAR(36) NULL,
- title VARCHAR(255) NOT NULL,
- summary VARCHAR(1000) NULL,
- cover_image_asset_id CHAR(36) NULL,
- page_type VARCHAR(32) NOT NULL DEFAULT 'article',
- template_id VARCHAR(64) NOT NULL DEFAULT 'editorial',
- draft_content_ref VARCHAR(512) NULL,
- current_version_id CHAR(36) NULL,
- current_publish_id CHAR(36) NULL,
- status ENUM('draft', 'reviewing', 'risk_found', 'ready', 'published', 'protected', 'expired', 'offline', 'deleted') NOT NULL DEFAULT 'draft',
- visibility ENUM('private', 'internal', 'public') NOT NULL DEFAULT 'private',
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- deleted_at BIGINT NULL,
- KEY idx_h5_pages_user_status (user_id, status, updated_at),
- KEY idx_h5_pages_category (user_id, category_id, updated_at),
- KEY idx_h5_pages_source_session (user_id, source_session_id),
- CONSTRAINT fk_h5_page_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_page_space FOREIGN KEY (space_id) REFERENCES h5_user_spaces(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_page_category FOREIGN KEY (category_id) REFERENCES h5_space_categories(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_page_source_asset FOREIGN KEY (source_asset_id) REFERENCES h5_assets(id) ON DELETE SET NULL,
- CONSTRAINT fk_h5_page_cover_asset FOREIGN KEY (cover_image_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_page_versions (
- id CHAR(36) PRIMARY KEY,
- page_id CHAR(36) NOT NULL,
- version_no INT NOT NULL,
- content_asset_id CHAR(36) NOT NULL,
- bundle_asset_id CHAR(36) NULL,
- source_snapshot_json JSON NULL,
- security_scan_id CHAR(36) NULL,
- created_by CHAR(36) NOT NULL,
- change_note VARCHAR(512) NULL,
- immutable TINYINT(1) NOT NULL DEFAULT 0,
- created_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_page_version (page_id, version_no),
- CONSTRAINT fk_h5_page_version_page FOREIGN KEY (page_id) REFERENCES h5_page_records(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_page_version_content FOREIGN KEY (content_asset_id) REFERENCES h5_assets(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_page_version_bundle FOREIGN KEY (bundle_asset_id) REFERENCES h5_assets(id) ON DELETE SET NULL,
- CONSTRAINT fk_h5_page_version_user FOREIGN KEY (created_by) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_security_scans (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- target_type ENUM('asset', 'page_version', 'publication_bundle') NOT NULL,
- target_id CHAR(36) NOT NULL,
- scanner_version VARCHAR(64) NOT NULL,
- status ENUM('queued', 'running', 'passed', 'warned', 'blocked', 'failed') NOT NULL,
- risk_level ENUM('none', 'low', 'medium', 'high', 'critical') NOT NULL DEFAULT 'none',
- findings_count INT NOT NULL DEFAULT 0,
- summary_json JSON NULL,
- started_at BIGINT NOT NULL,
- completed_at BIGINT NULL,
- KEY idx_h5_security_scan_target (user_id, target_type, target_id, completed_at),
- CONSTRAINT fk_h5_security_scan_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_security_findings (
- id CHAR(36) PRIMARY KEY,
- scan_id CHAR(36) NOT NULL,
- finding_type VARCHAR(64) NOT NULL,
- risk_level ENUM('low', 'medium', 'high', 'critical') NOT NULL,
- occurrence_count INT NOT NULL DEFAULT 1,
- sample_masked VARCHAR(255) NULL,
- blocking TINYINT(1) NOT NULL DEFAULT 0,
- created_at BIGINT NOT NULL,
- KEY idx_h5_security_finding_scan (scan_id, risk_level),
- CONSTRAINT fk_h5_security_finding_scan FOREIGN KEY (scan_id) REFERENCES h5_security_scans(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_publish_records (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- page_id CHAR(36) NOT NULL,
- page_version_id CHAR(36) NOT NULL,
- publish_type ENUM('page', 'share') NOT NULL DEFAULT 'page',
- url_slug VARCHAR(64) NOT NULL,
- public_url VARCHAR(512) NOT NULL,
- access_mode ENUM('public', 'password', 'private_link', 'time_limited', 'login_required', 'owner_only') NOT NULL,
- password_hash VARCHAR(255) NULL,
- token_hash CHAR(64) NULL,
- token_prefix VARCHAR(16) NULL,
- expires_at BIGINT NULL,
- user_confirmed_at BIGINT NULL,
- published_at BIGINT NOT NULL,
- offline_at BIGINT NULL,
- status ENUM('draft', 'online', 'expired', 'offline', 'blocked') NOT NULL DEFAULT 'online',
- view_count BIGINT NOT NULL DEFAULT 0,
- security_scan_id CHAR(36) NOT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_h5_publish_route (url_slug, status, published_at),
- KEY idx_h5_publish_page (user_id, page_id, published_at),
- KEY idx_h5_publish_auto_private (access_mode, status, user_confirmed_at, expires_at),
- UNIQUE KEY uq_h5_publish_token_hash (token_hash),
- CONSTRAINT fk_h5_publish_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_publish_page FOREIGN KEY (page_id) REFERENCES h5_page_records(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_publish_page_version FOREIGN KEY (page_version_id) REFERENCES h5_page_versions(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_publish_scan FOREIGN KEY (security_scan_id) REFERENCES h5_security_scans(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_publication_asset_refs (
- publication_id CHAR(36) NOT NULL,
- asset_id CHAR(36) NOT NULL,
- created_at BIGINT NOT NULL,
- PRIMARY KEY (publication_id, asset_id),
- KEY idx_asset_id (asset_id),
- KEY idx_created_at (created_at),
- CONSTRAINT fk_h5_pub_asset_ref_pub FOREIGN KEY (publication_id) REFERENCES h5_publish_records(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_pub_asset_ref_asset FOREIGN KEY (asset_id) REFERENCES h5_assets(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_publication_events (
- id CHAR(36) PRIMARY KEY,
- publish_id CHAR(36) NOT NULL,
- event_type ENUM('published', 'republished', 'settings_changed', 'expired', 'offlined', 'blocked') NOT NULL,
- actor_id CHAR(36) NOT NULL,
- old_page_version_id CHAR(36) NULL,
- new_page_version_id CHAR(36) NULL,
- access_mode VARCHAR(32) NOT NULL,
- detail_json JSON NULL,
- created_at BIGINT NOT NULL,
- KEY idx_h5_publication_event (publish_id, created_at),
- CONSTRAINT fk_h5_publication_event_publish FOREIGN KEY (publish_id) REFERENCES h5_publish_records(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_publication_event_actor FOREIGN KEY (actor_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_publication_views (
- id CHAR(36) PRIMARY KEY,
- publish_id CHAR(36) NOT NULL,
- viewer_user_id CHAR(36) NULL,
- referrer_host VARCHAR(255) NULL,
- device_type ENUM('desktop', 'mobile', 'tablet', 'bot', 'unknown') NOT NULL,
- viewed_at BIGINT NOT NULL,
- KEY idx_h5_publication_view_time (publish_id, viewed_at),
- KEY idx_h5_publication_viewer (viewer_user_id, viewed_at),
- CONSTRAINT fk_h5_publication_view_publish FOREIGN KEY (publish_id) REFERENCES h5_publish_records(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_publication_view_user FOREIGN KEY (viewer_user_id) REFERENCES h5_users(id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_agent_jobs (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- session_id VARCHAR(128) NULL,
- job_type VARCHAR(64) NOT NULL,
- instruction TEXT NOT NULL,
- permission_scope JSON NULL,
- user_context_json JSON NULL,
- output_category_id CHAR(36) NOT NULL,
- output_type VARCHAR(32) NOT NULL,
- max_output_bytes BIGINT NOT NULL DEFAULT 2097152,
- status ENUM('queued', 'running', 'completed', 'failed', 'cancelled', 'timed_out') NOT NULL DEFAULT 'queued',
- idempotency_key VARCHAR(128) NULL,
- progress_json JSON NULL,
- job_token_hash CHAR(64) NULL,
- result_page_id CHAR(36) NULL,
- result_asset_id CHAR(36) NULL,
- error_code VARCHAR(64) NULL,
- error_message VARCHAR(1000) NULL,
- queued_at BIGINT NOT NULL,
- started_at BIGINT NULL,
- heartbeat_at BIGINT NULL,
- completed_at BIGINT NULL,
- expires_at BIGINT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_agent_job_idempotency (user_id, idempotency_key),
- KEY idx_h5_agent_job_user_status (user_id, status, queued_at),
- KEY idx_h5_agent_job_token (job_token_hash),
- CONSTRAINT fk_h5_agent_job_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_agent_job_output_category FOREIGN KEY (output_category_id) REFERENCES h5_space_categories(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_agent_job_result_page FOREIGN KEY (result_page_id) REFERENCES h5_page_records(id) ON DELETE SET NULL,
- CONSTRAINT fk_h5_agent_job_result_asset FOREIGN KEY (result_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_agent_job_assets (
- id CHAR(36) PRIMARY KEY,
- job_id CHAR(36) NOT NULL,
- asset_id CHAR(36) NOT NULL,
- asset_version_id CHAR(36) NOT NULL,
- permission ENUM('read', 'write', 'create_derivative') NOT NULL DEFAULT 'read',
- created_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_agent_job_asset (job_id, asset_id, asset_version_id),
- KEY idx_h5_agent_job_asset_job (job_id, created_at),
- CONSTRAINT fk_h5_agent_job_asset_job FOREIGN KEY (job_id) REFERENCES h5_agent_jobs(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_agent_job_asset_asset FOREIGN KEY (asset_id) REFERENCES h5_assets(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_agent_job_asset_version FOREIGN KEY (asset_version_id) REFERENCES h5_asset_versions(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_wallets (
- user_id CHAR(36) PRIMARY KEY,
- balance_cents BIGINT NOT NULL DEFAULT 0,
- tokens_used BIGINT NOT NULL DEFAULT 0,
- updated_at BIGINT NOT NULL,
- CONSTRAINT fk_h5_wallet_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_path_grants (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- path VARCHAR(512) NOT NULL,
- mode ENUM('read', 'readwrite') NOT NULL DEFAULT 'readwrite',
- UNIQUE KEY uq_h5_user_path (user_id, path),
- CONSTRAINT fk_h5_path_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-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
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_agent_runs (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- agent_session_id VARCHAR(128) NULL,
- request_id VARCHAR(128) NOT NULL,
- status ENUM('queued', 'running', 'retryable', 'succeeded', 'failed') NOT NULL DEFAULT 'queued',
- attempts INT NOT NULL DEFAULT 0,
- user_message_json LONGTEXT NOT NULL,
- error_message TEXT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- started_at BIGINT NULL,
- completed_at BIGINT NULL,
- UNIQUE KEY uq_h5_agent_run_request (user_id, request_id),
- KEY idx_h5_agent_run_user_status (user_id, status, updated_at),
- KEY idx_h5_agent_run_session (agent_session_id, updated_at),
- CONSTRAINT fk_h5_agent_run_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_agent_run_session FOREIGN KEY (agent_session_id) REFERENCES h5_user_sessions(agent_session_id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_agent_run_events (
- id CHAR(36) PRIMARY KEY,
- run_id CHAR(36) NOT NULL,
- event_type VARCHAR(64) NOT NULL,
- data_json JSON NULL,
- created_at BIGINT NOT NULL,
- KEY idx_h5_agent_run_event_run (run_id, created_at),
- 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,
- last_accumulated_cost DOUBLE NULL,
- last_input_tokens BIGINT NOT NULL DEFAULT 0,
- last_output_tokens BIGINT NOT NULL DEFAULT 0,
- updated_at BIGINT NOT NULL,
- CONSTRAINT fk_h5_billing_state_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_usage_records (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- agent_session_id VARCHAR(128) NOT NULL,
- request_id VARCHAR(128) NULL,
- input_tokens INT NOT NULL DEFAULT 0,
- output_tokens INT NOT NULL DEFAULT 0,
- cost_cents BIGINT NOT NULL,
- balance_after_cents BIGINT NOT NULL,
- created_at BIGINT NOT NULL,
- UNIQUE KEY uniq_h5_usage_request_id (request_id),
- KEY idx_h5_usage_user_time (user_id, created_at),
- CONSTRAINT fk_h5_usage_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_policies (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- subject_type ENUM('role', 'user') NOT NULL,
- subject_id VARCHAR(64) NOT NULL,
- policy_key VARCHAR(64) NOT NULL,
- policy_value VARCHAR(512) NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_user_policy (subject_type, subject_id, policy_key),
- KEY idx_h5_policy_subject (subject_type, subject_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_skill_grants (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- subject_type ENUM('role', 'user') NOT NULL,
- subject_id VARCHAR(64) NOT NULL,
- skill_name VARCHAR(64) NOT NULL,
- enabled TINYINT(1) NOT NULL DEFAULT 0,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_user_skill_grant (subject_type, subject_id, skill_name),
- KEY idx_h5_skill_subject (subject_type, subject_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_capability_grants (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- subject_type ENUM('role', 'user') NOT NULL,
- subject_id VARCHAR(64) NOT NULL,
- capability_key VARCHAR(64) NOT NULL,
- allowed TINYINT(1) NOT NULL DEFAULT 0,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_capability_grant (subject_type, subject_id, capability_key),
- KEY idx_h5_capability_subject (subject_type, subject_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_login_sessions (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- token_hash CHAR(64) NOT NULL,
- expires_at BIGINT NOT NULL,
- created_at BIGINT NOT NULL,
- revoked_at BIGINT NULL,
- UNIQUE KEY uq_h5_login_token_hash (token_hash),
- KEY idx_h5_login_session_user (user_id),
- KEY idx_h5_login_session_expires (expires_at),
- CONSTRAINT fk_h5_login_session_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_mindspace_audit_logs (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- action VARCHAR(64) NOT NULL,
- object_type VARCHAR(32) NOT NULL,
- object_id CHAR(36) NOT NULL,
- ip VARCHAR(64) NULL,
- result VARCHAR(32) NOT NULL,
- risk_level VARCHAR(16) NULL,
- created_at BIGINT NOT NULL,
- KEY idx_h5_mindspace_audit_user_time (user_id, created_at),
- CONSTRAINT fk_h5_mindspace_audit_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_billing_ledger (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- type ENUM('recharge', 'deduct', 'refund', 'adjust') NOT NULL,
- amount_cents BIGINT NOT NULL,
- tokens BIGINT NOT NULL DEFAULT 0,
- session_id VARCHAR(128) NULL,
- note VARCHAR(512) NULL,
- operator_id CHAR(36) NULL,
- created_at BIGINT NOT NULL,
- KEY idx_h5_ledger_user (user_id, created_at),
- CONSTRAINT fk_h5_ledger_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_llm_provider_keys (
- id CHAR(36) PRIMARY KEY,
- provider_id VARCHAR(64) NOT NULL,
- provider_kind ENUM('builtin', 'custom') NOT NULL DEFAULT 'builtin',
- api_url VARCHAR(512) NULL,
- base_path VARCHAR(256) NULL,
- models_json TEXT NULL,
- goosed_provider_id VARCHAR(64) NULL,
- engine VARCHAR(32) NOT NULL DEFAULT 'openai',
- relay_provider VARCHAR(64) NULL,
- name VARCHAR(128) NOT NULL,
- api_key_ciphertext TEXT NOT NULL,
- api_key_iv VARCHAR(24) NOT NULL,
- api_key_tag VARCHAR(24) NOT NULL,
- default_model VARCHAR(128) NOT NULL,
- status ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
- is_selected TINYINT(1) NOT NULL DEFAULT 0,
- is_vision_selected TINYINT(1) NOT NULL DEFAULT 0,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_llm_key_name (name),
- KEY idx_h5_llm_key_provider (provider_id),
- KEY idx_h5_llm_key_selected (is_selected),
- KEY idx_h5_llm_key_vision (is_vision_selected)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_llm_executor_bindings (
- id CHAR(36) PRIMARY KEY,
- executor ENUM('goose', 'aider', 'openhands') NOT NULL,
- purpose VARCHAR(32) NOT NULL DEFAULT 'default',
- provider_key_id CHAR(36) NULL,
- model VARCHAR(128) NOT NULL,
- enabled TINYINT(1) NOT NULL DEFAULT 1,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_llm_executor_binding (executor, purpose),
- KEY idx_h5_llm_executor_provider (provider_key_id),
- CONSTRAINT fk_h5_llm_executor_provider FOREIGN KEY (provider_key_id) REFERENCES h5_llm_provider_keys(id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_payment_orders (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- amount_cents BIGINT NOT NULL,
- channel ENUM('wechat') NOT NULL DEFAULT 'wechat',
- status ENUM('pending', 'paid', 'failed', 'expired', 'refunded') NOT NULL DEFAULT 'pending',
- pay_mode ENUM('native', 'h5', 'jsapi') NOT NULL DEFAULT 'native',
- out_trade_no VARCHAR(64) NOT NULL,
- provider_txn VARCHAR(128) NULL,
- code_url VARCHAR(512) NULL,
- h5_url VARCHAR(1024) NULL,
- client_ip VARCHAR(64) NULL,
- expire_at BIGINT NOT NULL,
- paid_at BIGINT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_payment_out_trade_no (out_trade_no),
- KEY idx_h5_payment_user_time (user_id, created_at),
- KEY idx_h5_payment_status_expire (status, expire_at),
- CONSTRAINT fk_h5_payment_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_categories (
- id CHAR(36) PRIMARY KEY,
- name VARCHAR(50) NOT NULL,
- slug VARCHAR(50) NOT NULL,
- icon VARCHAR(10) NOT NULL DEFAULT '',
- description VARCHAR(200) NOT NULL DEFAULT '',
- sort_order INT NOT NULL DEFAULT 0,
- is_active TINYINT(1) NOT NULL DEFAULT 1,
- created_at BIGINT NOT NULL,
- UNIQUE KEY uq_plaza_category_slug (slug),
- KEY idx_plaza_category_sort (is_active, sort_order)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_posts (
- id CHAR(36) PRIMARY KEY,
- publication_id CHAR(36) NOT NULL,
- user_id CHAR(36) NOT NULL,
- title VARCHAR(200) NOT NULL,
- summary VARCHAR(500) NOT NULL DEFAULT '',
- cover_url VARCHAR(500) NOT NULL DEFAULT '',
- user_slug VARCHAR(100) NOT NULL,
- user_display_name VARCHAR(100) NOT NULL,
- user_avatar_url VARCHAR(500) NOT NULL DEFAULT '',
- category_id CHAR(36) NOT NULL,
- tags JSON NOT NULL,
- status ENUM('pending_review', 'published', 'hidden', 'rejected') NOT NULL DEFAULT 'pending_review',
- view_count INT UNSIGNED NOT NULL DEFAULT 0,
- like_count INT UNSIGNED NOT NULL DEFAULT 0,
- collect_count INT UNSIGNED NOT NULL DEFAULT 0,
- comment_count INT UNSIGNED NOT NULL DEFAULT 0,
- share_count INT UNSIGNED NOT NULL DEFAULT 0,
- hot_score DOUBLE NOT NULL DEFAULT 0,
- hot_updated_at BIGINT NULL,
- allow_comment TINYINT(1) NOT NULL DEFAULT 1,
- published_at BIGINT NOT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_plaza_post_publication (publication_id),
- KEY idx_plaza_post_user_status (user_id, status),
- KEY idx_plaza_post_category_hot (category_id, status, hot_score),
- KEY idx_plaza_post_published_at (published_at),
- KEY idx_plaza_post_status_hot (status, hot_score),
- CONSTRAINT fk_plaza_post_publication FOREIGN KEY (publication_id) REFERENCES h5_publish_records(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_post_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_post_category FOREIGN KEY (category_id) REFERENCES plaza_categories(id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_reactions (
- id CHAR(36) PRIMARY KEY,
- post_id CHAR(36) NOT NULL,
- user_id CHAR(36) NOT NULL,
- type ENUM('like', 'collect', 'share') NOT NULL,
- created_at BIGINT NOT NULL,
- UNIQUE KEY uq_plaza_reaction (post_id, user_id, type),
- KEY idx_plaza_reaction_post (post_id, type),
- KEY idx_plaza_reaction_user (user_id, type),
- CONSTRAINT fk_plaza_reaction_post FOREIGN KEY (post_id) REFERENCES plaza_posts(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_reaction_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_comments (
- id CHAR(36) PRIMARY KEY,
- post_id CHAR(36) NOT NULL,
- user_id CHAR(36) NOT NULL,
- parent_id CHAR(36) NULL,
- content VARCHAR(500) NOT NULL,
- status ENUM('visible', 'deleted', 'flagged', 'hidden') NOT NULL DEFAULT 'visible',
- like_count INT UNSIGNED NOT NULL DEFAULT 0,
- reply_count INT UNSIGNED NOT NULL DEFAULT 0,
- deleted_by ENUM('user', 'ops') NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_plaza_comment_post (post_id, status, created_at),
- KEY idx_plaza_comment_parent (parent_id),
- KEY idx_plaza_comment_user (user_id, created_at),
- CONSTRAINT fk_plaza_comment_post FOREIGN KEY (post_id) REFERENCES plaza_posts(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_comment_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_comment_reactions (
- id CHAR(36) PRIMARY KEY,
- comment_id CHAR(36) NOT NULL,
- user_id CHAR(36) NOT NULL,
- created_at BIGINT NOT NULL,
- UNIQUE KEY uq_plaza_comment_reaction (comment_id, user_id),
- KEY idx_plaza_comment_reaction_comment (comment_id),
- CONSTRAINT fk_plaza_comment_reaction_comment FOREIGN KEY (comment_id) REFERENCES plaza_comments(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_comment_reaction_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_follows (
- follower_id CHAR(36) NOT NULL,
- followee_id CHAR(36) NOT NULL,
- created_at BIGINT NOT NULL,
- PRIMARY KEY (follower_id, followee_id),
- KEY idx_plaza_follow_followee (followee_id, created_at),
- CONSTRAINT fk_plaza_follow_follower FOREIGN KEY (follower_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_follow_followee FOREIGN KEY (followee_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_algorithm_config (
- `key` VARCHAR(100) NOT NULL,
- value DOUBLE NOT NULL,
- description VARCHAR(200) NOT NULL DEFAULT '',
- updated_at BIGINT NOT NULL,
- PRIMARY KEY (`key`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_attribution_events (
- id CHAR(36) PRIMARY KEY,
- event_type ENUM('landing', 'signup') NOT NULL,
- utm_source VARCHAR(100) NOT NULL,
- utm_medium VARCHAR(100) NOT NULL DEFAULT '',
- utm_campaign VARCHAR(100) NOT NULL DEFAULT '',
- ref_id VARCHAR(200) NOT NULL DEFAULT '',
- user_id CHAR(36) NULL,
- ip_hash VARCHAR(64) NOT NULL DEFAULT '',
- created_at BIGINT NOT NULL,
- KEY idx_plaza_attr_created (created_at),
- KEY idx_plaza_attr_campaign (utm_source, utm_campaign, created_at),
- CONSTRAINT fk_plaza_attr_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_reports (
- id CHAR(36) PRIMARY KEY,
- target_type ENUM('post', 'comment') NOT NULL,
- target_id CHAR(36) NOT NULL,
- reporter_id CHAR(36) NOT NULL,
- reason ENUM('spam', 'violence', 'porn', 'political', 'privacy', 'other') NOT NULL,
- detail VARCHAR(500) NOT NULL DEFAULT '',
- status ENUM('pending', 'processed', 'dismissed') NOT NULL DEFAULT 'pending',
- processed_by CHAR(36) NULL,
- processed_at BIGINT NULL,
- action_taken VARCHAR(200) NOT NULL DEFAULT '',
- created_at BIGINT NOT NULL,
- KEY idx_plaza_report_status (status, target_type, created_at),
- KEY idx_plaza_report_target (target_type, target_id),
- CONSTRAINT fk_plaza_report_reporter FOREIGN KEY (reporter_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_report_processor FOREIGN KEY (processed_by) REFERENCES h5_users(id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
--- Session snapshot cache: stores userVisible messages for fast history reload.
--- Rollback: DROP TABLE h5_session_snapshots (data-only, no dependants).
-CREATE TABLE IF NOT EXISTS h5_session_snapshots (
- agent_session_id VARCHAR(128) NOT NULL PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- name VARCHAR(512) NOT NULL DEFAULT '',
- display_title VARCHAR(512) NOT NULL DEFAULT '',
- working_dir VARCHAR(1024) NOT NULL DEFAULT '',
- created_at_str VARCHAR(64) NOT NULL DEFAULT '',
- updated_at_str VARCHAR(64) NOT NULL DEFAULT '',
- user_set_name TINYINT(1) NOT NULL DEFAULT 0,
- recipe_json TEXT NULL,
- synced_msg_count INT NOT NULL DEFAULT 0,
- source_updated_at VARCHAR(64) NOT NULL DEFAULT '',
- messages_json LONGTEXT NOT NULL,
- synced_at BIGINT NOT NULL,
- KEY idx_h5_snapshots_user (user_id),
- KEY idx_h5_snapshots_synced (synced_at),
- CONSTRAINT fk_h5_snapshot_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_featured (
- id CHAR(36) PRIMARY KEY,
- post_id CHAR(36) NOT NULL,
- position VARCHAR(50) NOT NULL,
- sort_order INT NOT NULL DEFAULT 0,
- starts_at BIGINT NOT NULL,
- expires_at BIGINT NULL,
- created_by CHAR(36) NOT NULL,
- created_at BIGINT NOT NULL,
- KEY idx_plaza_featured_position (position, starts_at, expires_at),
- CONSTRAINT fk_plaza_featured_post FOREIGN KEY (post_id) REFERENCES plaza_posts(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_featured_creator FOREIGN KEY (created_by) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS plaza_user_events (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NULL,
- session_id VARCHAR(64) NOT NULL,
- post_id CHAR(36) NOT NULL,
- event_type ENUM(
- 'impression', 'click', 'view', 'dwell',
- 'like', 'collect', 'comment', 'share',
- 'dislike', 'hide'
- ) NOT NULL,
- dwell_ms INT UNSIGNED NULL,
- feed_sort VARCHAR(32) NOT NULL DEFAULT '',
- feed_category VARCHAR(64) NOT NULL DEFAULT '',
- position INT UNSIGNED NULL,
- created_at BIGINT NOT NULL,
- KEY idx_plaza_event_user_time (user_id, created_at),
- KEY idx_plaza_event_session_time (session_id, created_at),
- KEY idx_plaza_event_post (post_id, event_type, created_at),
- CONSTRAINT fk_plaza_event_post FOREIGN KEY (post_id) REFERENCES plaza_posts(id) ON DELETE CASCADE,
- CONSTRAINT fk_plaza_event_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS ops_audit_log (
- id CHAR(36) PRIMARY KEY,
- operator_id CHAR(36) NOT NULL,
- action VARCHAR(100) NOT NULL,
- target_type VARCHAR(50) NOT NULL,
- target_id CHAR(36) NOT NULL,
- detail JSON NOT NULL,
- created_at BIGINT NOT NULL,
- KEY idx_ops_audit_operator (operator_id, created_at),
- KEY idx_ops_audit_target (target_type, target_id, created_at),
- CONSTRAINT fk_ops_audit_operator FOREIGN KEY (operator_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_wechat_identities (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- app_id VARCHAR(32) NOT NULL,
- openid VARCHAR(64) NOT NULL,
- unionid VARCHAR(64) NULL,
- nickname VARCHAR(128) NULL,
- avatar_url VARCHAR(512) NULL,
- last_login_at BIGINT NOT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_wechat_openid (app_id, openid),
- UNIQUE KEY uq_wechat_user (user_id, app_id),
- KEY idx_wechat_unionid (unionid),
- CONSTRAINT fk_wechat_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_wechat_oauth_states (
- state VARCHAR(64) PRIMARY KEY,
- return_to VARCHAR(512) NULL,
- utm_source VARCHAR(64) NULL,
- utm_medium VARCHAR(64) NULL,
- utm_campaign VARCHAR(64) NULL,
- intent VARCHAR(16) NOT NULL DEFAULT 'login',
- bind_user_id CHAR(36) NULL,
- auth_mode VARCHAR(8) NOT NULL DEFAULT 'mp',
- status VARCHAR(16) NOT NULL DEFAULT 'pending',
- result_kind VARCHAR(32) NULL,
- result_token VARCHAR(512) NULL,
- result_message VARCHAR(255) NULL,
- expires_at BIGINT NOT NULL,
- created_at BIGINT NOT NULL,
- KEY idx_wechat_oauth_state_expires (expires_at),
- KEY idx_wechat_oauth_state_status (status, expires_at)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_wechat_pending_binds (
- token VARCHAR(64) PRIMARY KEY,
- app_id VARCHAR(32) NOT NULL,
- openid VARCHAR(64) NOT NULL,
- unionid VARCHAR(64) NULL,
- nickname VARCHAR(128) NULL,
- avatar_url VARCHAR(512) NULL,
- return_to VARCHAR(512) NULL,
- utm_source VARCHAR(64) NULL,
- utm_medium VARCHAR(64) NULL,
- utm_campaign VARCHAR(64) NULL,
- expires_at BIGINT NOT NULL,
- created_at BIGINT NOT NULL,
- KEY idx_wechat_pending_expires (expires_at)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_wechat_agent_routes (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- app_id VARCHAR(32) NOT NULL,
- openid VARCHAR(64) NOT NULL,
- agent_session_id VARCHAR(128) NOT NULL,
- status ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_wechat_agent_route (app_id, openid),
- KEY idx_wechat_agent_route_user (user_id, updated_at),
- CONSTRAINT fk_wechat_agent_route_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_wechat_mp_messages (
- app_id VARCHAR(32) NOT NULL,
- openid VARCHAR(64) NOT NULL,
- msg_id VARCHAR(128) NOT NULL,
- status ENUM('processing', 'done', 'failed') NOT NULL DEFAULT 'processing',
- agent_session_id VARCHAR(128) NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- PRIMARY KEY (app_id, openid, msg_id),
- KEY idx_wechat_mp_messages_updated (updated_at),
- KEY idx_wechat_mp_messages_session (agent_session_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_wechat_mp_message_details (
- id CHAR(36) PRIMARY KEY,
- app_id VARCHAR(32) NOT NULL,
- openid VARCHAR(64) NOT NULL,
- user_id CHAR(36) NULL,
- msg_id VARCHAR(128) NULL,
- msg_type VARCHAR(32) NOT NULL,
- display_text TEXT NULL,
- agent_text TEXT NULL,
- media_id VARCHAR(256) NULL,
- media_url TEXT NULL,
- media_public_url TEXT NULL,
- media_format VARCHAR(64) NULL,
- location_lat DECIMAL(10,7) NULL,
- location_lng DECIMAL(10,7) NULL,
- location_label VARCHAR(255) NULL,
- link_url TEXT NULL,
- link_title VARCHAR(255) NULL,
- raw_xml_hash CHAR(40) NULL,
- raw_json JSON NULL,
- created_at BIGINT NOT NULL,
- KEY idx_wechat_mp_message_details_user (app_id, openid, created_at),
- KEY idx_wechat_mp_message_details_msg (msg_id),
- KEY idx_wechat_mp_message_details_type (msg_type, created_at),
- CONSTRAINT fk_wechat_mp_message_details_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_schedule_items (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- kind ENUM('task', 'event') NOT NULL,
- title VARCHAR(255) NOT NULL,
- description TEXT NULL,
- status ENUM('active', 'completed', 'cancelled', 'deleted') NOT NULL DEFAULT 'active',
- start_at BIGINT NULL,
- end_at BIGINT NULL,
- due_at BIGINT NULL,
- all_day TINYINT(1) NOT NULL DEFAULT 0,
- timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
- location VARCHAR(255) NULL,
- source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent',
- source_session_id VARCHAR(128) NULL,
- source_message_id VARCHAR(128) NULL,
- source_text TEXT NULL,
- metadata_json JSON NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- deleted_at BIGINT NULL,
- KEY idx_schedule_user_time (user_id, status, start_at, due_at),
- KEY idx_schedule_user_updated (user_id, updated_at),
- CONSTRAINT fk_schedule_item_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_schedule_reminders (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- item_id CHAR(36) NOT NULL,
- remind_at BIGINT NOT NULL,
- offset_minutes INT NULL,
- channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat',
- status ENUM('pending', 'locked', 'sent', 'failed', 'cancelled') NOT NULL DEFAULT 'pending',
- attempts INT NOT NULL DEFAULT 0,
- last_error VARCHAR(500) NULL,
- locked_until BIGINT NULL,
- sent_at BIGINT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_schedule_item_remind_at (item_id, remind_at, channel),
- KEY idx_reminder_due (status, remind_at),
- KEY idx_reminder_user (user_id, status, remind_at),
- CONSTRAINT fk_schedule_reminder_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_schedule_reminder_item FOREIGN KEY (item_id) REFERENCES h5_schedule_items(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_schedule_digest_subscriptions (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- digest_type ENUM('todo_day') NOT NULL DEFAULT 'todo_day',
- hour TINYINT UNSIGNED NOT NULL,
- minute TINYINT UNSIGNED NOT NULL DEFAULT 0,
- timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
- channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat',
- status ENUM('active', 'locked', 'failed', 'cancelled') NOT NULL DEFAULT 'active',
- next_run_at BIGINT NOT NULL,
- last_run_at BIGINT NULL,
- attempts INT NOT NULL DEFAULT 0,
- locked_until BIGINT NULL,
- last_error VARCHAR(500) NULL,
- source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent',
- source_session_id VARCHAR(128) NULL,
- source_message_id VARCHAR(128) NULL,
- source_text TEXT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_schedule_digest_user_type_channel (user_id, digest_type, channel),
- KEY idx_schedule_digest_due (status, next_run_at),
- CONSTRAINT fk_schedule_digest_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_balance_alert_subscriptions (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- threshold_cents BIGINT NOT NULL,
- channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat',
- status ENUM('active', 'locked', 'failed', 'cancelled') NOT NULL DEFAULT 'active',
- next_run_at BIGINT NOT NULL,
- last_run_at BIGINT NULL,
- last_notified_balance_cents BIGINT NULL,
- attempts INT NOT NULL DEFAULT 0,
- locked_until BIGINT NULL,
- last_error VARCHAR(500) NULL,
- source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent',
- source_session_id VARCHAR(128) NULL,
- source_message_id VARCHAR(128) NULL,
- source_text TEXT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_balance_alert_user_channel (user_id, channel),
- KEY idx_balance_alert_due (status, next_run_at),
- KEY idx_balance_alert_user (user_id, status, next_run_at),
- CONSTRAINT fk_balance_alert_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_schedule_delivery_logs (
- id CHAR(36) PRIMARY KEY,
- reminder_id CHAR(36) NULL,
- subscription_id CHAR(36) NULL,
- user_id CHAR(36) NOT NULL,
- channel ENUM('wechat', 'in_app') NOT NULL,
- status ENUM('success', 'failed') NOT NULL,
- provider_message_id VARCHAR(128) NULL,
- error_code VARCHAR(64) NULL,
- error_message VARCHAR(500) NULL,
- created_at BIGINT NOT NULL,
- KEY idx_delivery_reminder (reminder_id, created_at),
- KEY idx_delivery_subscription (subscription_id, created_at),
- KEY idx_delivery_user (user_id, created_at),
- CONSTRAINT fk_schedule_delivery_reminder FOREIGN KEY (reminder_id) REFERENCES h5_schedule_reminders(id) ON DELETE CASCADE,
- CONSTRAINT fk_schedule_delivery_subscription FOREIGN KEY (subscription_id) REFERENCES h5_schedule_digest_subscriptions(id) ON DELETE CASCADE,
- CONSTRAINT fk_schedule_delivery_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_notifications (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- channel ENUM('web', 'wechat') NOT NULL DEFAULT 'web',
- notification_type VARCHAR(64) NOT NULL,
- title VARCHAR(255) NOT NULL,
- body TEXT NOT NULL,
- data_json JSON NULL,
- status ENUM('unread', 'read') NOT NULL DEFAULT 'unread',
- read_at BIGINT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_user_notifications_user_status_created (user_id, status, created_at),
- KEY idx_user_notifications_user_created (user_id, created_at),
- CONSTRAINT fk_user_notifications_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_plan_catalog (
- plan_type VARCHAR(32) PRIMARY KEY,
- name VARCHAR(64) NOT NULL,
- price_cents INT NOT NULL DEFAULT 0,
- period_days INT NOT NULL DEFAULT 30,
- period_tokens BIGINT NOT NULL DEFAULT 0,
- period_images INT NOT NULL DEFAULT 0,
- model_tier VARCHAR(32) NOT NULL DEFAULT 'basic',
- overage_rate DECIMAL(4,2) NOT NULL DEFAULT 1.00,
- sort_order INT NOT NULL DEFAULT 0,
- is_active TINYINT(1) NOT NULL DEFAULT 1,
- description VARCHAR(512) NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_subscriptions (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- plan_type VARCHAR(32) NOT NULL DEFAULT 'free',
- status ENUM('active', 'expired', 'cancelled') NOT NULL DEFAULT 'active',
- period_tokens_limit BIGINT NOT NULL DEFAULT 0,
- period_tokens_used BIGINT NOT NULL DEFAULT 0,
- period_images_limit INT NOT NULL DEFAULT 0,
- period_images_used INT NOT NULL DEFAULT 0,
- period_start BIGINT NOT NULL,
- period_end BIGINT NOT NULL,
- expires_at BIGINT NOT NULL,
- overage_rate DECIMAL(4,2) NOT NULL DEFAULT 1.00,
- auto_renew TINYINT(1) NOT NULL DEFAULT 0,
- operator_id CHAR(36) NULL,
- note VARCHAR(512) NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_h5_sub_user_status (user_id, status),
- KEY idx_h5_sub_expires (expires_at, status),
- CONSTRAINT fk_h5_sub_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_blocked_words (
- id CHAR(36) PRIMARY KEY,
- word VARCHAR(200) NOT NULL,
- replacement VARCHAR(200) NOT NULL DEFAULT '***',
- note VARCHAR(500) NULL,
- status ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uk_blocked_word (word)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
--- Shared experience store (etat C): all goosed Worker instances read/write the
--- same learned experience so capability is not siloed per instance. Built on
--- MySQL first with a keyword + recency retrieval; `embedding` is reserved for a
--- later move to PostgreSQL + pgvector without changing the calling contract.
-CREATE TABLE IF NOT EXISTS h5_experience (
- id CHAR(36) PRIMARY KEY,
- scope VARCHAR(64) NOT NULL DEFAULT 'global',
- kind VARCHAR(32) NOT NULL DEFAULT 'lesson',
- title VARCHAR(255) NOT NULL,
- body MEDIUMTEXT NOT NULL,
- tags_json JSON NULL,
- source_session_id VARCHAR(128) NULL,
- source_user_id CHAR(36) NULL,
- use_count BIGINT NOT NULL DEFAULT 0,
- embedding JSON NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_h5_experience_scope_updated (scope, updated_at),
- FULLTEXT KEY ftx_h5_experience (title, body)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_conversation_messages (
- id CHAR(64) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- agent_session_id VARCHAR(128) NOT NULL,
- message_key VARCHAR(128) NOT NULL,
- sequence_no INT NOT NULL DEFAULT 0,
- role VARCHAR(32) NOT NULL,
- text MEDIUMTEXT NOT NULL,
- raw_json LONGTEXT NULL,
- analyzed_at BIGINT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_conversation_message (agent_session_id, message_key),
- KEY idx_h5_conversation_user_created (user_id, created_at),
- KEY idx_h5_conversation_user_analyzed (user_id, analyzed_at),
- CONSTRAINT fk_h5_conversation_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_conversation_session FOREIGN KEY (agent_session_id) REFERENCES h5_user_sessions(agent_session_id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_memory_items (
- id CHAR(64) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- label ENUM('preference', 'habit', 'interest', 'goal', 'fact', 'experience', 'knowledge') NOT NULL DEFAULT 'fact',
- memory_hash CHAR(64) NOT NULL,
- memory_text TEXT NOT NULL,
- evidence_message_id CHAR(64) NULL,
- source_session_id VARCHAR(128) NULL,
- confidence DECIMAL(4,3) NOT NULL DEFAULT 0.600,
- status ENUM('active', 'archived', 'deleted') NOT NULL DEFAULT 'active',
- raw_json JSON NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_user_memory_hash (user_id, memory_hash),
- KEY idx_h5_user_memory_user_label (user_id, label, updated_at),
- KEY idx_h5_user_memory_session (source_session_id),
- CONSTRAINT fk_h5_user_memory_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_user_memory_evidence FOREIGN KEY (evidence_message_id) REFERENCES h5_conversation_messages(id) ON DELETE SET NULL,
- CONSTRAINT fk_h5_user_memory_session FOREIGN KEY (source_session_id) REFERENCES h5_user_sessions(agent_session_id) ON DELETE SET NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE IF NOT EXISTS h5_user_feedback (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- type ENUM('bug', 'feature', 'other') NOT NULL,
- title VARCHAR(120) NOT NULL,
- description TEXT NOT NULL,
- contact VARCHAR(120) NULL,
- status ENUM('pending', 'reviewing', 'resolved', 'closed') NOT NULL DEFAULT 'pending',
- images_json JSON NULL,
- context_json JSON NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_h5_user_feedback_user_created (user_id, created_at),
- KEY idx_h5_user_feedback_status_created (status, created_at),
- CONSTRAINT fk_h5_user_feedback_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/.runtime/portal/scripts/agent-run-guard.mjs b/.runtime/portal/scripts/agent-run-guard.mjs
deleted file mode 100755
index 8d1d507..0000000
--- a/.runtime/portal/scripts/agent-run-guard.mjs
+++ /dev/null
@@ -1,346 +0,0 @@
-#!/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
deleted file mode 100755
index dde6e05..0000000
--- a/.runtime/portal/scripts/agent-run-worker.mjs
+++ /dev/null
@@ -1,9403 +0,0 @@
-#!/usr/bin/env node
-import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
-
-// scripts/agent-run-worker.mjs
-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();
-}
-function safeJsonParse(value, fallback = null) {
- try {
- return JSON.parse(value);
- } catch {
- return fallback;
- }
-}
-function serializeMessage(message) {
- return JSON.stringify(message ?? {});
-}
-function positiveInteger(value, fallback) {
- const n = Number(value);
- if (!Number.isFinite(n) || n <= 0) return fallback;
- return Math.floor(n);
-}
-function timeoutError(ms) {
- const err = new Error(`agent run timed out after ${ms}ms`);
- err.code = "AGENT_RUN_TIMEOUT";
- return err;
-}
-function envFlag(value, fallback = false) {
- const raw = String(value ?? "").trim().toLowerCase();
- if (!raw) return fallback;
- return ["1", "true", "yes", "on"].includes(raw);
-}
-function normalizeAgentRunToolMode(value) {
- const normalized = String(value ?? "chat").trim().toLowerCase();
- if (!normalized || normalized === "chat") return "chat";
- if (CODE_TOOL_MODES.has(normalized)) return "code";
- throw new Error(`\u4E0D\u652F\u6301\u7684 tool_mode: ${value}`);
-}
-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 } : {};
- metadata[RUN_METADATA_KEY] = {
- ...metadata[RUN_METADATA_KEY] && typeof metadata[RUN_METADATA_KEY] === "object" ? metadata[RUN_METADATA_KEY] : {},
- toolMode: normalizeAgentRunToolMode(toolMode),
- ...taskType ? { taskType } : {}
- };
- return { ...message, metadata };
-}
-function getRunOptionsFromMessage(userMessage) {
- const metadata = userMessage?.metadata;
- const runMetadata = metadata?.[RUN_METADATA_KEY] ?? metadata?.agentRun ?? {};
- let toolMode = "chat";
- try {
- toolMode = normalizeAgentRunToolMode(runMetadata?.toolMode ?? metadata?.toolMode ?? "chat");
- } catch {
- toolMode = "chat";
- }
- return {
- toolMode,
- taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType),
- validation: normalizeToolGatewayValidation(runMetadata?.validation ?? metadata?.toolGatewayValidation)
- };
-}
-function projectRun(row) {
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- sessionId: row.agent_session_id ?? null,
- requestId: row.request_id,
- status: row.status,
- attempts: Number(row.attempts ?? 0),
- error: row.error_message ?? null,
- createdAt: Number(row.created_at ?? 0),
- updatedAt: Number(row.updated_at ?? 0),
- startedAt: row.started_at == null ? null : Number(row.started_at),
- completedAt: row.completed_at == null ? null : Number(row.completed_at)
- };
-}
-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(
- process.env.MEMIND_AGENT_RUN_QUEUE_CONCURRENCY,
- DEFAULT_MAX_CONCURRENT_RUNS
- ),
- 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();
- const queuedDispatches = [];
- const queuedDispatchSet = /* @__PURE__ */ new Set();
- function enqueueRun(runId) {
- if (!runId || inFlight.has(runId) || queuedDispatchSet.has(runId)) return false;
- queuedDispatchSet.add(runId);
- queuedDispatches.push(runId);
- return true;
- }
- function nextQueuedRunId() {
- const runId = queuedDispatches.shift();
- if (runId) queuedDispatchSet.delete(runId);
- return runId ?? null;
- }
- async function appendEvent(runId, type, data = null) {
- await pool2.query(
- `INSERT INTO h5_agent_run_events (id, run_id, event_type, data_json, created_at)
- VALUES (?, ?, ?, ?, ?)`,
- [
- crypto.randomUUID(),
- runId,
- type,
- data == null ? null : JSON.stringify(data),
- nowMs()
- ]
- );
- }
- async function getRunById(runId) {
- const [rows] = await pool2.query(
- `SELECT * FROM h5_agent_runs WHERE id = ? LIMIT 1`,
- [runId]
- );
- return rows[0] ?? null;
- }
- async function getRunForUser(userId, runId) {
- const [rows] = await pool2.query(
- `SELECT * FROM h5_agent_runs WHERE id = ? AND user_id = ? LIMIT 1`,
- [runId, userId]
- );
- return projectRun(rows[0] ?? null);
- }
- async function getRunByRequest(userId, requestId) {
- const [rows] = await pool2.query(
- `SELECT * FROM h5_agent_runs WHERE user_id = ? AND request_id = ? LIMIT 1`,
- [userId, requestId]
- );
- return rows[0] ?? null;
- }
- async function createRun(userId, {
- sessionId = null,
- requestId,
- userMessage,
- toolMode = "chat",
- taskType = null
- }) {
- const normalizedRequestId = String(requestId ?? "").trim();
- if (!normalizedRequestId) {
- throw new Error("\u7F3A\u5C11 request_id");
- }
- const normalizedToolMode = normalizeAgentRunToolMode(toolMode);
- const normalizedTaskType = normalizeTaskType(taskType);
- const existing = await getRunByRequest(userId, normalizedRequestId);
- if (existing) {
- if (autoDispatch && !TERMINAL_STATUSES.has(existing.status)) dispatchRun(existing.id);
- return projectRun(existing);
- }
- const runId = crypto.randomUUID();
- const createdAt = nowMs();
- const runMessage = withRunMetadata(userMessage, {
- toolMode: normalizedToolMode,
- taskType: normalizedTaskType
- });
- await pool2.query(
- `INSERT INTO h5_agent_runs
- (id, user_id, agent_session_id, request_id, status, attempts,
- user_message_json, error_message, created_at, updated_at, started_at, completed_at)
- VALUES (?, ?, ?, ?, 'queued', 0, ?, NULL, ?, ?, NULL, NULL)`,
- [
- runId,
- userId,
- sessionId || null,
- normalizedRequestId,
- serializeMessage(runMessage),
- createdAt,
- createdAt
- ]
- );
- await appendEvent(runId, "queued", {
- sessionId: sessionId || null,
- toolMode: normalizedToolMode,
- taskType: normalizedTaskType
- });
- if (autoDispatch) dispatchRun(runId);
- return projectRun(await getRunById(runId));
- }
- async function markRun(runId, status, fields = {}) {
- const updates = ["status = ?", "updated_at = ?"];
- const values = [status, nowMs()];
- for (const [key, value] of Object.entries(fields)) {
- updates.push(`${key} = ?`);
- values.push(value);
- }
- values.push(runId);
- await pool2.query(
- `UPDATE h5_agent_runs SET ${updates.join(", ")} WHERE id = ?`,
- values
- );
- 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) => {
- timer = setTimeout(() => reject(timeoutError(runTimeoutMs)), runTimeoutMs);
- });
- try {
- return await Promise.race([task(), timeout]);
- } finally {
- if (timer) clearTimeout(timer);
- }
- }
- 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 = {};
- if (runOptions.toolMode === "code" && userAuth2?.getCodeAgentSessionPolicy) {
- sessionOptions.sessionPolicy = await userAuth2.getCodeAgentSessionPolicy(row.user_id);
- }
- const session = await tkmindProxy2.startSessionForUser(row.user_id, sessionOptions);
- sessionId = session.id;
- await pool2.query(
- `UPDATE h5_agent_runs SET agent_session_id = ?, updated_at = ? WHERE id = ?`,
- [sessionId, nowMs(), runId]
- );
- await appendEvent(runId, "session_started", {
- sessionId,
- toolMode: runOptions.toolMode,
- taskType: runOptions.taskType
- });
- }
- await tkmindProxy2.submitSessionReplyForUser(
- row.user_id,
- sessionId,
- row.request_id,
- userMessage,
- { toolMode: runOptions.toolMode }
- );
- return { sessionId };
- }
- async function processRun(runId) {
- const row = await getRunById(runId);
- if (!row || TERMINAL_STATUSES.has(row.status)) return;
- const nextAttempt = Number(row.attempts ?? 0) + 1;
- const [claim] = await pool2.query(
- `UPDATE h5_agent_runs
- SET status = 'running', attempts = ?, started_at = COALESCE(started_at, ?), updated_at = ?, error_message = NULL
- WHERE id = ? AND status IN ('queued', 'retryable')`,
- [nextAttempt, nowMs(), nowMs(), runId]
- );
- 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", {
- agent_session_id: sessionId,
- completed_at: nowMs()
- });
- } catch (err) {
- const message = err instanceof Error ? err.message : String(err);
- const timedOut = err?.code === "AGENT_RUN_TIMEOUT";
- if (timedOut) {
- await appendEvent(runId, "timeout", { timeoutMs: runTimeoutMs });
- }
- const retryable = !timedOut && err?.retryable !== false && nextAttempt < retryDelaysMs.length;
- await markRun(runId, retryable ? "retryable" : "failed", {
- error_message: message,
- completed_at: retryable ? null : nowMs()
- });
- if (retryable && autoDispatch) {
- setTimeout(() => dispatchRun(runId), retryDelaysMs[nextAttempt - 1]);
- }
- } finally {
- stopHeartbeat();
- }
- }
- function drainQueue() {
- while (inFlight.size < maxConcurrentRuns) {
- const runId = nextQueuedRunId();
- if (!runId) break;
- inFlight.add(runId);
- void processRun(runId).finally(() => {
- inFlight.delete(runId);
- drainQueue();
- });
- }
- }
- function dispatchRun(runId) {
- if (!enqueueRun(runId)) return;
- drainQueue();
- }
- async function getQueueStatus() {
- const [rows] = await pool2.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 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,
- 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()
- };
- }
- const take = Math.min(dispatchLimit, available);
- const [rows] = await pool2.query(
- `SELECT id
- FROM h5_agent_runs
- WHERE status IN ('queued', 'retryable')
- ORDER BY updated_at ASC
- LIMIT ?`,
- [take]
- );
- for (const row of rows) {
- dispatchRun(row.id);
- }
- return {
- considered: rows.length,
- dispatched: rows.length,
- staleRecovery,
- queue: await getQueueStatus()
- };
- }
- return {
- createRun,
- getRunForUser,
- dispatchRun,
- getQueueStatus,
- dispatchQueuedRuns,
- recoverStaleRunningRuns
- };
-}
-
-// db.mjs
-import mysql from "mysql2/promise";
-import path2 from "node:path";
-import { fileURLToPath } from "node:url";
-
-// mindspace.mjs
-import crypto2 from "node:crypto";
-var DEFAULT_SPACE_QUOTA_BYTES = 5 * 1024 * 1024;
-var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
-var SYSTEM_CATEGORIES = Object.freeze([
- {
- code: "oa",
- name: "OA \u5DE5\u4F5C\u533A",
- visibilityPolicy: "private",
- aiAccessPolicy: "selected_assets",
- publishPolicy: "derivative_only",
- sortOrder: 10
- },
- {
- code: "public",
- name: "\u516C\u5F00\u533A",
- visibilityPolicy: "public_candidate",
- aiAccessPolicy: "selected_assets",
- publishPolicy: "security_scan_required",
- sortOrder: 20
- },
- {
- code: "draft",
- name: "\u9875\u9762\u8349\u7A3F",
- visibilityPolicy: "private",
- aiAccessPolicy: "job_output",
- publishPolicy: "security_scan_required",
- sortOrder: 40
- },
- {
- code: "archive",
- name: "\u5F52\u6863\u533A",
- visibilityPolicy: "private",
- aiAccessPolicy: "none",
- publishPolicy: "forbidden",
- sortOrder: 50
- }
-]);
-async function initializeDefaultSpace(db, userId, {
- quotaBytes = DEFAULT_SPACE_QUOTA_BYTES,
- spaceName = "\u6211\u7684\u7A7A\u95F4",
- now = Date.now(),
- idFactory = () => crypto2.randomUUID()
-} = {}) {
- const spaceId = idFactory();
- await db.query(
- `INSERT INTO h5_user_spaces
- (id, user_id, space_name, quota_bytes, used_bytes, reserved_bytes, status, created_at, updated_at)
- VALUES (?, ?, ?, ?, 0, 0, 'active', ?, ?)
- ON DUPLICATE KEY UPDATE user_id = user_id`,
- [spaceId, userId, spaceName, quotaBytes, now, now]
- );
- const [spaces] = await db.query(
- `SELECT id FROM h5_user_spaces WHERE user_id = ? LIMIT 1`,
- [userId]
- );
- const resolvedSpaceId = spaces[0]?.id;
- if (!resolvedSpaceId) {
- throw new Error("\u7528\u6237\u7A7A\u95F4\u521D\u59CB\u5316\u5931\u8D25");
- }
- for (const category of SYSTEM_CATEGORIES) {
- await db.query(
- `INSERT INTO h5_space_categories
- (id, user_id, space_id, category_code, category_name, visibility_policy,
- ai_access_policy, publish_policy, is_system, sort_order, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- category_name = VALUES(category_name),
- visibility_policy = VALUES(visibility_policy),
- ai_access_policy = VALUES(ai_access_policy),
- publish_policy = VALUES(publish_policy),
- is_system = 1,
- sort_order = VALUES(sort_order),
- updated_at = VALUES(updated_at)`,
- [
- idFactory(),
- userId,
- resolvedSpaceId,
- category.code,
- category.name,
- category.visibilityPolicy,
- category.aiAccessPolicy,
- category.publishPolicy,
- category.sortOrder,
- now,
- now
- ]
- );
- }
- return resolvedSpaceId;
-}
-
-// db.mjs
-var __dirname = path2.dirname(fileURLToPath(import.meta.url));
-function isDatabaseConfigured() {
- return Boolean(
- process.env.DATABASE_URL || process.env.MYSQL_HOST && process.env.MYSQL_DATABASE
- );
-}
-function resolvePoolOptions() {
- const connectionLimit = Math.max(1, Number(process.env.MYSQL_POOL_SIZE ?? 50));
- const queueLimit = Math.max(0, Number(process.env.MYSQL_POOL_QUEUE_LIMIT ?? 0));
- return { waitForConnections: true, connectionLimit, queueLimit };
-}
-function createDbPool() {
- if (!isDatabaseConfigured()) {
- throw new Error("MySQL \u672A\u914D\u7F6E\uFF0C\u8BF7\u8BBE\u7F6E DATABASE_URL \u6216 MYSQL_* \u73AF\u5883\u53D8\u91CF");
- }
- const poolOptions = resolvePoolOptions();
- if (process.env.DATABASE_URL) {
- return mysql.createPool({ uri: process.env.DATABASE_URL, ...poolOptions });
- }
- return mysql.createPool({
- host: process.env.MYSQL_HOST ?? "localhost",
- port: Number(process.env.MYSQL_PORT ?? 3306),
- user: process.env.MYSQL_USER ?? "boot",
- password: process.env.MYSQL_PASSWORD ?? "",
- database: process.env.MYSQL_DATABASE ?? "tkmind",
- ...poolOptions
- });
-}
-
-// 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 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 as Agent2, fetch as undiciFetch2 } from "undici";
-
-// sse-billing.mjs
-import { Transform } from "node:stream";
-function parseSseChunk(chunk) {
- const events = [];
- const blocks = chunk.split("\n\n");
- for (const block of blocks) {
- if (!block.trim()) continue;
- let data;
- for (const line of block.split("\n")) {
- if (line.startsWith("data:")) {
- data = line.slice(5).trim();
- }
- }
- if (!data) continue;
- try {
- events.push(JSON.parse(data));
- } catch {
- }
- }
- return events;
-}
-function finishBillingKey(event) {
- const requestId = event.request_id ?? event.chat_request_id;
- if (requestId) return String(requestId);
- const state = event.token_state ?? {};
- const input = state.accumulatedInputTokens ?? state.accumulated_input_tokens ?? state.inputTokens ?? 0;
- const output = state.accumulatedOutputTokens ?? state.accumulated_output_tokens ?? state.outputTokens ?? 0;
- return `${input}:${output}`;
-}
-function createSseBillingTransform({ onFinish }) {
- let buffer = "";
- const billedFinishKeys = /* @__PURE__ */ new Set();
- const billFinishOnce = (event) => {
- if (event?.type !== "Finish" || !event.token_state) return;
- const key = finishBillingKey(event);
- if (billedFinishKeys.has(key)) return;
- billedFinishKeys.add(key);
- void onFinish(event).catch(() => {
- });
- };
- return new Transform({
- transform(chunk, _encoding, callback) {
- buffer += chunk.toString("utf8");
- const parts = buffer.split("\n\n");
- buffer = parts.pop() ?? "";
- for (const block of parts) {
- if (!block.trim()) continue;
- let dataLine;
- for (const line of block.split("\n")) {
- if (line.startsWith("data:")) dataLine = line.slice(5).trim();
- }
- if (!dataLine) continue;
- try {
- billFinishOnce(JSON.parse(dataLine));
- } catch {
- }
- }
- callback(null, chunk);
- },
- flush(callback) {
- if (buffer.trim()) {
- for (const event of parseSseChunk(`${buffer}
-
-`)) {
- billFinishOnce(event);
- }
- }
- callback();
- }
- });
-}
-function appendBalanceEvent(payload) {
- const data = typeof payload === "number" ? { balanceCents: payload } : payload && typeof payload === "object" ? payload : { balanceCents: 0 };
- return `event: balance
-data: ${JSON.stringify(data)}
-
-`;
-}
-
-// capabilities.mjs
-import path4 from "node:path";
-import { fileURLToPath as fileURLToPath2 } from "node:url";
-
-// policies.mjs
-var POLICY_CATALOG = [
- {
- key: "goose_mode",
- label: "TKMind \u6A21\u5F0F",
- description: "H5 \u7EC8\u7AEF\u7528\u6237\u4E0D\u5728\u804A\u5929\u91CC\u70B9\u300C\u5141\u8BB8/\u62D2\u7EDD\u300D\uFF1B\u53EF\u7528\u5DE5\u5177\u7531\u300C\u80FD\u529B\u6743\u9650\u300D\u51B3\u5B9A\u3002chat \u4E0D\u8C03\u7528\u5DE5\u5177\uFF0Cauto \u5728\u6388\u6743\u8303\u56F4\u5185\u81EA\u52A8\u6267\u884C\u3002",
- type: "select",
- options: [
- { value: "chat", label: "\u4EC5\u804A\u5929 (chat)" },
- { value: "auto", label: "\u81EA\u52A8\u6267\u884C (auto)" }
- ],
- defaultValue: "chat",
- category: "goose",
- risk: "high"
- },
- {
- key: "workspace_access",
- label: "\u5DE5\u4F5C\u533A\u8BBF\u95EE",
- description: "readonly \u65F6\u7981\u6B62 shell \u4E0E\u6587\u4EF6\u5199\u5165\uFF0C\u4EC5\u5141\u8BB8\u6D4F\u89C8\u4E0E\u5206\u6790",
- type: "select",
- options: [
- { value: "readwrite", label: "\u8BFB\u5199" },
- { value: "readonly", label: "\u53EA\u8BFB" }
- ],
- defaultValue: "readwrite",
- category: "workspace",
- risk: "medium"
- },
- {
- key: "network_egress",
- label: "\u7F51\u7EDC\u51FA\u7AD9",
- description: "deny \u65F6\u7981\u7528 shell\u3001\u7535\u8111\u63A7\u5236\u3001\u6C99\u7BB1\u811A\u672C\u7B49\u53EF\u80FD\u8BBF\u95EE\u7F51\u7EDC\u7684\u6269\u5C55",
- type: "select",
- options: [
- { value: "deny", label: "\u7981\u6B62\uFF08\u63A8\u8350\uFF09" },
- { value: "allow", label: "\u5141\u8BB8\uFF08\u9700\u540C\u65F6\u5F00\u542F\u5BF9\u5E94\u80FD\u529B\uFF09" }
- ],
- defaultValue: "deny",
- category: "network",
- risk: "high"
- },
- {
- key: "api_lockdown",
- label: "API \u4EE3\u7406\u9501\u5B9A",
- description: "\u5F00\u542F\u540E\u4EC5\u5141\u8BB8\u804A\u5929\u76F8\u5173 API\uFF0C\u62E6\u622A\u6539\u6A21\u5F0F\u3001\u52A0\u6269\u5C55\u3001\u5199\u914D\u7F6E\u7B49\u5371\u9669\u8BF7\u6C42",
- type: "boolean",
- defaultValue: true,
- category: "proxy",
- risk: "medium"
- },
- {
- key: "code_delegate_executor",
- label: "\u4EE3\u7801\u59D4\u6258\u6267\u884C\u5668",
- description: "\u63A7\u5236 Goose \u5728\u591A\u6587\u4EF6\u7F16\u7801\u3001\u4FEE\u590D\u4E0E\u91CD\u6784\u4EFB\u52A1\u4E2D\u4F18\u5148\u59D4\u6258\u7ED9\u8C01\u3002auto \u7531 Goose \u7ED3\u5408\u5F53\u524D\u53EF\u7528\u6269\u5C55\u81EA\u884C\u5224\u65AD\uFF1Baider / openhands \u5219\u4F18\u5148\u4F7F\u7528\u6307\u5B9A\u6267\u884C\u5668\u3002",
- type: "select",
- options: [
- { value: "auto", label: "\u81EA\u52A8\u9009\u62E9 (auto)" },
- { value: "aider", label: "\u4F18\u5148 Aider" },
- { value: "openhands", label: "\u4F18\u5148 OpenHands" }
- ],
- defaultValue: "auto",
- category: "routing",
- risk: "medium"
- },
- {
- key: "code_task_routing",
- label: "\u4EE3\u7801\u4EFB\u52A1\u8DEF\u7531\u7B56\u7565",
- description: "\u5B9A\u4E49\u4EE3\u7801\u4EFB\u52A1\u5982\u4F55\u5728 Aider \u4E0E OpenHands \u4E4B\u95F4\u5206\u6D41\u3002balanced \u7531 Goose \u6309\u4EFB\u52A1\u590D\u6742\u5EA6\u5224\u65AD\uFF1Bsplit \u5EFA\u8BAE\u5C0F\u6539\u52A8\u8D70 Aider\u3001\u590D\u6742\u4EFB\u52A1\u8D70 OpenHands\uFF1Bforce_* \u5219\u5F3A\u5236\u4F18\u5148\u5355\u4E00\u8DEF\u5F84\u3002",
- type: "select",
- options: [
- { value: "balanced", label: "\u5E73\u8861\u8DEF\u7531 (balanced)" },
- { value: "split", label: "\u5C0F\u6539\u52A8 Aider\uFF0C\u590D\u6742\u4EFB\u52A1 OpenHands" },
- { value: "force_aider", label: "\u5C3D\u91CF\u7EDF\u4E00\u8D70 Aider" },
- { value: "force_openhands", label: "\u5C3D\u91CF\u7EDF\u4E00\u8D70 OpenHands" }
- ],
- defaultValue: "balanced",
- category: "routing",
- risk: "medium"
- }
-];
-var DEFAULT_USER_POLICIES = Object.fromEntries(
- POLICY_CATALOG.map((item) => [item.key, item.defaultValue])
-);
-var POLICY_KEYS = new Set(POLICY_CATALOG.map((item) => item.key));
-var GOOSE_MODES = /* @__PURE__ */ new Set(["auto", "approve", "smart_approve", "chat"]);
-var WORKSPACE_ACCESS = /* @__PURE__ */ new Set(["readwrite", "readonly"]);
-var NETWORK_EGRESS = /* @__PURE__ */ new Set(["allow", "deny"]);
-var CODE_DELEGATE_EXECUTORS = /* @__PURE__ */ new Set(["auto", "aider", "openhands"]);
-var CODE_TASK_ROUTINGS = /* @__PURE__ */ new Set(["balanced", "split", "force_aider", "force_openhands"]);
-function policyKeys() {
- return [...POLICY_KEYS];
-}
-function isValidPolicyKey(key) {
- return POLICY_KEYS.has(key);
-}
-function normalizePolicyPatch(patch) {
- const normalized = {};
- for (const [key, raw] of Object.entries(patch ?? {})) {
- if (!isValidPolicyKey(key)) continue;
- const def = POLICY_CATALOG.find((item) => item.key === key);
- if (!def) continue;
- if (def.type === "boolean") {
- normalized[key] = raw === true || raw === "true" || raw === 1 || raw === "1";
- continue;
- }
- const value = String(raw ?? "").trim();
- if (def.key === "goose_mode" && GOOSE_MODES.has(value)) normalized[key] = value;
- if (def.key === "workspace_access" && WORKSPACE_ACCESS.has(value)) normalized[key] = value;
- if (def.key === "network_egress" && NETWORK_EGRESS.has(value)) normalized[key] = value;
- if (def.key === "code_delegate_executor" && CODE_DELEGATE_EXECUTORS.has(value)) {
- normalized[key] = value;
- }
- if (def.key === "code_task_routing" && CODE_TASK_ROUTINGS.has(value)) {
- normalized[key] = value;
- }
- }
- return normalized;
-}
-function resolvePolicies(rolePolicies, userOverrides) {
- const resolved = { ...DEFAULT_USER_POLICIES };
- for (const [key, value] of Object.entries(rolePolicies ?? {})) {
- if (isValidPolicyKey(key)) resolved[key] = value;
- }
- for (const [key, value] of Object.entries(userOverrides ?? {})) {
- if (isValidPolicyKey(key)) resolved[key] = value;
- }
- if (resolved.goose_mode === "approve" || resolved.goose_mode === "smart_approve") {
- resolved.goose_mode = "auto";
- }
- return resolved;
-}
-function hasExecutableTools(capabilities) {
- return Boolean(
- capabilities?.static_publish || capabilities?.shell || capabilities?.filesystem || capabilities?.code_browse || capabilities?.subagent || capabilities?.code_sandbox || capabilities?.computer || capabilities?.charts || capabilities?.aider || capabilities?.openhands || capabilities?.apps || capabilities?.todo || capabilities?.skills || capabilities?.chat_recall
- );
-}
-function resolveAgentGooseMode(capabilities, policies) {
- const requested = policies?.goose_mode ?? DEFAULT_USER_POLICIES.goose_mode;
- if (!hasExecutableTools(capabilities)) {
- return "chat";
- }
- if (requested === "chat" || requested === "approve" || requested === "smart_approve") {
- return "auto";
- }
- return "auto";
-}
-function applyPoliciesToCapabilities(capabilities, policies) {
- const effective = { ...capabilities };
- if (policies.workspace_access === "readonly") {
- effective.shell = false;
- effective.filesystem = false;
- effective.static_publish = false;
- effective.aider = false;
- effective.openhands = false;
- effective.code_sandbox = false;
- effective.image_read = false;
- }
- if (policies.network_egress === "deny") {
- if (!effective.static_publish) {
- effective.shell = false;
- }
- effective.computer = false;
- effective.code_sandbox = false;
- }
- return effective;
-}
-var USER_API_ALLOWLIST = [
- { method: "GET", pattern: /^\/status$/ },
- { method: "POST", pattern: /^\/agent\/start$/ },
- { method: "POST", pattern: /^\/agent\/resume$/ },
- { method: "POST", pattern: /^\/agent\/runs$/ },
- { method: "GET", pattern: /^\/agent\/runs\/[^/]+$/ },
- { method: "GET", pattern: /^\/agent\/runs\/[^/]+\/events$/ },
- { method: "GET", pattern: /^\/sessions$/ },
- { method: "GET", pattern: /^\/sessions\/[^/]+$/ },
- { method: "DELETE", pattern: /^\/sessions\/[^/]+$/ },
- { method: "GET", pattern: /^\/sessions\/[^/]+\/events$/ },
- { method: "POST", pattern: /^\/sessions\/[^/]+\/reply$/ },
- { method: "POST", pattern: /^\/sessions\/[^/]+\/cancel$/ },
- { method: "POST", pattern: /^\/action-required\/tool-confirmation$/ },
- { method: "POST", pattern: /^\/agent\/update_provider$/ },
- { method: "POST", pattern: /^\/config\/read$/ },
- { method: "POST", pattern: /^\/agent\/harness_bootstrap$/ },
- { method: "POST", pattern: /^\/agent\/harness_remember$/ }
-];
-function isNativeH5ApiPath(pathname) {
- const path13 = String(pathname ?? "");
- return path13.startsWith("/mindspace/") || path13.startsWith("/plaza/");
-}
-function evaluateProxyRequest(method, pathname, policies, { unrestricted = false } = {}) {
- if (unrestricted) return { allowed: true };
- if (!policies.api_lockdown) return { allowed: true };
- if (isNativeH5ApiPath(pathname)) {
- return {
- allowed: false,
- reason: `H5 \u672C\u5730\u63A5\u53E3\u4E0D\u5E94\u8D70 Agent \u4EE3\u7406\uFF1A${method.toUpperCase()} ${pathname}`
- };
- }
- const upper = method.toUpperCase();
- const allowed = USER_API_ALLOWLIST.some(
- (rule) => rule.method === upper && rule.pattern.test(pathname)
- );
- if (allowed) return { allowed: true };
- return {
- allowed: false,
- reason: `\u7B56\u7565\u5DF2\u9501\u5B9A API\uFF1A${upper} ${pathname}`
- };
-}
-
-// capabilities.mjs
-function resolveSandboxMcpServerPath(overridePath) {
- const normalized = String(overridePath ?? "").trim();
- if (normalized) return normalized;
- return path4.join(path4.dirname(fileURLToPath2(import.meta.url)), "mindspace-sandbox-mcp.mjs");
-}
-function resolveSandboxMcpNodeExecPath(overridePath) {
- const normalized = String(overridePath ?? "").trim();
- if (normalized) return normalized;
- return process.execPath;
-}
-var CAPABILITY_CATALOG = [
- {
- key: "shell",
- label: "Shell \u547D\u4EE4",
- description: "\u6267\u884C bash/shell \u547D\u4EE4\uFF08developer.shell\uFF09",
- risk: "high",
- category: "developer"
- },
- {
- key: "static_publish",
- label: "\u7528\u6237\u6C99\u7BB1\u76EE\u5F55",
- description: "\u5728 MindSpace/<\u7528\u6237ID>/ \u5185\u53EF\u4F7F\u7528 sandbox-fs \u7684 write_file/edit_file/read_file/create_dir\uFF08\u4EE5\u53CA\u6309\u6743\u9650\u5F00\u653E\u7684 list_dir\uFF1B\u4E0D\u53EF\u8D8A\u51FA\u8BE5\u76EE\u5F55\uFF09",
- risk: "medium",
- category: "publisher"
- },
- {
- key: "private_data_space",
- label: "\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4",
- description: "\u4E3A\u7528\u6237\u63D0\u4F9B\u552F\u4E00\u7684\u79C1\u6709 SQLite \u6570\u636E\u7A7A\u95F4\uFF0C\u4F9B Agent \u521B\u5EFA\u95EE\u5377\u3001\u8868\u5355\u3001\u6E05\u5355\u7B49\u79C1\u6709\u7ED3\u6784\u5316\u6570\u636E\u8868",
- risk: "medium",
- category: "publisher"
- },
- {
- key: "filesystem",
- label: "\u6587\u4EF6\u8BFB\u5199\uFF08\u5168\u76EE\u5F55\uFF09",
- description: "\u5728\u5DE5\u4F5C\u533A\u5185\u4EFB\u610F\u8BFB\u5199\u6587\u4EF6\uFF08developer.write / edit\uFF09\uFF0C\u4EC5\u9AD8\u7EA7\u7528\u6237",
- risk: "high",
- category: "developer"
- },
- {
- key: "code_browse",
- label: "\u4EE3\u7801\u6D4F\u89C8",
- description: "\u76EE\u5F55\u6811\u3001\u4EE3\u7801\u7ED3\u6784\u5206\u6790\uFF08developer.tree\u3001analyze\uFF09",
- risk: "low",
- category: "developer"
- },
- {
- key: "image_read",
- label: "\u56FE\u7247\u8BFB\u53D6",
- description: "\u8BFB\u53D6\u672C\u5730\u6216\u7F51\u7EDC\u56FE\u7247\uFF08developer.read_image\uFF09",
- risk: "low",
- category: "developer"
- },
- {
- key: "skills",
- label: "\u6280\u80FD\u52A0\u8F7D",
- description: "\u52A0\u8F7D\u6280\u80FD\u4E0E\u77E5\u8BC6\uFF08skills / summon.load\uFF09",
- risk: "low",
- category: "knowledge"
- },
- {
- key: "subagent",
- label: "\u5B50\u4EFB\u52A1\u6D3E\u53D1",
- description: "delegate \u5B50 Agent \u6267\u884C\u590D\u6742\u4EFB\u52A1\uFF08summon\uFF09",
- risk: "high",
- category: "agent"
- },
- {
- key: "code_sandbox",
- label: "\u6C99\u7BB1\u811A\u672C",
- description: "execute_typescript \u5728\u6C99\u7BB1\u4E2D\u6279\u91CF\u8C03\u7528\u5DE5\u5177\uFF08code_execution\uFF09",
- risk: "high",
- category: "agent"
- },
- {
- key: "chat_recall",
- label: "\u5BF9\u8BDD\u56DE\u6EAF",
- description: "\u68C0\u7D22\u5386\u53F2\u4F1A\u8BDD\uFF08chatrecall\uFF09",
- risk: "low",
- category: "memory"
- },
- {
- key: "context_memory",
- label: "\u9879\u76EE\u8BB0\u5FC6",
- description: "\u4F1A\u8BDD\u7EA7\u9879\u76EE\u8BB0\u5FC6\u6CE8\u5165\uFF08projectmemory\uFF0CH5 \u5F15\u5BFC\u7528\uFF09",
- risk: "low",
- category: "memory"
- },
- {
- key: "memory_store",
- label: "\u957F\u671F\u8BB0\u5FC6",
- description: "\u8BB0\u4F4F/\u68C0\u7D22\u7528\u6237\u504F\u597D\uFF08memory \u6269\u5C55\uFF09",
- risk: "medium",
- category: "memory"
- },
- {
- key: "extension_admin",
- label: "\u6269\u5C55\u7BA1\u7406",
- description: "\u641C\u7D22\u3001\u542F\u7528\u3001\u7981\u7528\u6269\u5C55\uFF08extensionmanager\uFF09",
- risk: "high",
- category: "admin"
- },
- {
- key: "apps",
- label: "\u5E94\u7528\u7BA1\u7406",
- description: "\u521B\u5EFA\u4E0E\u7BA1\u7406 TKMind \u5E94\u7528\uFF08apps\uFF09",
- risk: "medium",
- category: "agent"
- },
- {
- key: "todo",
- label: "\u5F85\u529E\u4E8B\u9879",
- description: "\u4EFB\u52A1\u5217\u8868\u8DDF\u8E2A\uFF08todo\uFF09",
- risk: "low",
- category: "productivity"
- },
- {
- key: "computer",
- label: "\u7535\u8111\u63A7\u5236",
- description: "\u81EA\u52A8\u5316\u811A\u672C\u3001\u7F51\u9875\u6293\u53D6\u3001\u6587\u6863\u5904\u7406\uFF08computercontroller\uFF09",
- risk: "high",
- category: "automation"
- },
- {
- key: "charts",
- label: "\u6570\u636E\u53EF\u89C6\u5316",
- description: "\u56FE\u8868\u4E0E\u53EF\u89C6\u5316\uFF08autovisualiser\uFF09",
- risk: "low",
- category: "automation"
- },
- {
- key: "aider",
- label: "Aider \u7F16\u7801",
- description: "\u591A\u6587\u4EF6\u7F16\u7801\u59D4\u6258\uFF08aider\uFF09",
- risk: "high",
- category: "developer"
- },
- {
- key: "openhands",
- label: "OpenHands \u7F16\u7801",
- description: "\u590D\u6742\u591A\u6587\u4EF6\u7F16\u7801\u4E0E\u4ED3\u5E93\u7EA7\u4EFB\u52A1\u59D4\u6258\uFF08openhands\uFF09",
- risk: "high",
- category: "developer"
- }
-];
-var USER_NON_GRANTABLE_CAPABILITIES = /* @__PURE__ */ new Set(["extension_admin"]);
-function clampUserCapabilities(capabilities) {
- const clamped = { ...capabilities };
- for (const key of USER_NON_GRANTABLE_CAPABILITIES) {
- clamped[key] = false;
- }
- return clamped;
-}
-var DEFAULT_USER_CAPABILITIES = Object.fromEntries(
- CAPABILITY_CATALOG.map(({ key }) => {
- const defaults = {
- shell: false,
- static_publish: false,
- private_data_space: true,
- filesystem: false,
- code_browse: false,
- image_read: true,
- skills: true,
- subagent: false,
- code_sandbox: false,
- chat_recall: true,
- context_memory: true,
- memory_store: true,
- extension_admin: false,
- apps: false,
- todo: false,
- computer: false,
- charts: false,
- aider: false,
- openhands: false
- };
- return [key, defaults[key] ?? false];
- })
-);
-var CATALOG_KEYS = new Set(CAPABILITY_CATALOG.map((item) => item.key));
-function catalogKeys() {
- return [...CATALOG_KEYS];
-}
-function isValidCapabilityKey(key) {
- return CATALOG_KEYS.has(key);
-}
-function makeExtension(type, name, tools = []) {
- return {
- type,
- name,
- description: "",
- display_name: name,
- bundled: true,
- available_tools: tools
- };
-}
-function sandboxDeveloperTools(capabilities) {
- const tools = ["write", "edit"];
- if (capabilities.shell) {
- tools.push("shell", "tree");
- } else if (capabilities.code_browse) {
- tools.push("tree");
- }
- if (capabilities.image_read) tools.push("read_image");
- return tools;
-}
-function sandboxMcpTools(capabilities) {
- const tools = [];
- if (capabilities.static_publish) {
- 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) {
- tools.push(
- "private_data_info",
- "private_data_schema",
- "private_data_query",
- "private_data_execute",
- "schedule_create_item",
- "schedule_create_reminder",
- "schedule_list_items"
- );
- }
- return tools;
-}
-function developerToolsFromPolicy(sessionPolicy) {
- const developer = sessionPolicy?.extensionOverrides?.find((ext) => ext.name === "developer");
- const sandboxFs = sessionPolicy?.extensionOverrides?.find((ext) => ext.name === "sandbox-fs");
- return (sandboxFs ?? developer)?.available_tools ?? [];
-}
-function mergeDeveloperTools(capabilities) {
- const tools = [];
- if (capabilities.shell) tools.push("shell");
- if (capabilities.filesystem) tools.push("write", "edit");
- if (capabilities.code_browse) tools.push("tree");
- if (capabilities.image_read) tools.push("read_image");
- return tools;
-}
-function sandboxMcpEnvs(sandboxMcp, mcpTools) {
- const envs = {
- SANDBOX_ROOT: sandboxMcp.sandboxRoot,
- ALLOWED_TOOLS: mcpTools.join(",")
- };
- if (sandboxMcp.userId) envs.PRIVATE_DATA_USER_ID = sandboxMcp.userId;
- for (const key of [
- "DATABASE_URL",
- "MYSQL_HOST",
- "MYSQL_PORT",
- "MYSQL_USER",
- "MYSQL_PASSWORD",
- "MYSQL_DATABASE",
- "PRIVATE_DATA_MAX_BYTES"
- ]) {
- if (process.env[key]) envs[key] = process.env[key];
- }
- return envs;
-}
-function buildAgentExtensionPolicy(capabilities, { unrestricted = false, policies = null, sandboxMcp = null, toolMode = "chat" } = {}) {
- if (unrestricted) {
- return { extensionOverrides: null, enableContextMemory: true, gooseMode: "auto" };
- }
- const extensions = [];
- if (capabilities.static_publish || capabilities.private_data_space && sandboxMcp) {
- if (sandboxMcp?.serverPath && sandboxMcp?.sandboxRoot) {
- const mcpTools = sandboxMcpTools(capabilities);
- if (mcpTools.length > 0) {
- extensions.push({
- type: "stdio",
- name: "sandbox-fs",
- description: "\u5DE5\u4F5C\u533A\u6C99\u7BB1\u6587\u4EF6\u7CFB\u7EDF\u4E0E\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u3002\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u662F\u5F53\u524D\u7528\u6237\u552F\u4E00\u7684 SQLite \u6570\u636E\u5E93\uFF0C\u9002\u5408\u95EE\u5377\u3001\u8868\u5355\u3001\u6E05\u5355\u3001\u8C03\u7814\u6570\u636E\u548C\u5206\u6790\u4E2D\u95F4\u8868\uFF1B\u4E0D\u8981\u7528\u4E8E\u8D26\u53F7\u3001\u8BA1\u8D39\u3001\u6743\u9650\u3001\u5BA1\u8BA1\u3001\u516C\u5F00\u5E73\u53F0\u6570\u636E\u6216\u8DE8\u7528\u6237\u6570\u636E\u3002",
- display_name: "sandbox-fs",
- bundled: false,
- cmd: resolveSandboxMcpNodeExecPath(sandboxMcp.nodeExecPath),
- // sandboxRoot passed as argv[2] so it works even if goosed doesn't forward envs
- args: [sandboxMcp.serverPath, sandboxMcp.sandboxRoot],
- // envs (goosed field name) as belt-and-suspenders backup
- envs: sandboxMcpEnvs(sandboxMcp, mcpTools),
- available_tools: mcpTools
- });
- }
- if (capabilities.image_read) {
- extensions.push(makeExtension("platform", "developer", ["read_image"]));
- }
- } else if (capabilities.static_publish) {
- const sandboxTools = sandboxDeveloperTools(capabilities);
- if (sandboxTools.length > 0) {
- extensions.push(makeExtension("platform", "developer", sandboxTools));
- }
- }
- if (capabilities.static_publish) {
- extensions.push(makeExtension("platform", "skills", []));
- extensions.push(makeExtension("platform", "summon", ["load_skill"]));
- extensions.push(makeExtension("platform", "projectmemory", []));
- }
- } else {
- const developerTools = mergeDeveloperTools(capabilities);
- if (developerTools.length > 0) {
- extensions.push(makeExtension("platform", "developer", developerTools));
- }
- if (capabilities.code_browse) {
- extensions.push(makeExtension("platform", "analyze", []));
- }
- if (capabilities.skills) {
- extensions.push(makeExtension("platform", "skills", []));
- }
- if (capabilities.skills || capabilities.subagent) {
- const summonTools = [];
- if (capabilities.skills) summonTools.push("load", "load_skill");
- if (capabilities.subagent) summonTools.push("delegate");
- extensions.push(makeExtension("platform", "summon", summonTools));
- }
- }
- const enableCodeExecutionExtension = /^(1|true|yes)$/i.test(
- process.env.TKMIND_ENABLE_CODE_EXECUTION_EXTENSION ?? ""
- );
- if (capabilities.code_sandbox && enableCodeExecutionExtension) {
- extensions.push(makeExtension("platform", "code_execution", []));
- }
- if (capabilities.chat_recall) {
- extensions.push(makeExtension("platform", "chatrecall", []));
- }
- if (capabilities.context_memory) {
- extensions.push(makeExtension("platform", "projectmemory", []));
- }
- if (capabilities.memory_store) {
- extensions.push(makeExtension("builtin", "memory", []));
- }
- if (capabilities.extension_admin) {
- extensions.push(makeExtension("platform", "extensionmanager", []));
- }
- if (capabilities.apps) {
- extensions.push(makeExtension("platform", "apps", []));
- }
- if (capabilities.todo) {
- extensions.push(makeExtension("platform", "todo", []));
- }
- if (capabilities.computer) {
- extensions.push(makeExtension("builtin", "computercontroller", []));
- }
- if (capabilities.charts) {
- extensions.push(makeExtension("builtin", "autovisualiser", []));
- }
- const codeToolMode = toolMode === "code" || toolMode === "code-task";
- if (codeToolMode && capabilities.aider) {
- extensions.push({
- ...makeExtension("platform", "aider", []),
- timeout_ms: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5),
- metadata: { runtime_scope: "code_tool_task" }
- });
- }
- if (codeToolMode && capabilities.openhands) {
- extensions.push({
- ...makeExtension("platform", "openhands", []),
- timeout_ms: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5),
- metadata: { runtime_scope: "code_tool_task" }
- });
- }
- return {
- extensionOverrides: extensions,
- enableContextMemory: Boolean(
- capabilities.context_memory || capabilities.chat_recall || capabilities.static_publish
- ),
- gooseMode: resolveAgentGooseMode(capabilities, policies)
- };
-}
-function normalizeCapabilityPatch(patch) {
- const normalized = {};
- for (const [key, value] of Object.entries(patch ?? {})) {
- if (!isValidCapabilityKey(key)) continue;
- normalized[key] = Boolean(value);
- }
- return normalized;
-}
-
-// user-publish.mjs
-import fs3 from "node:fs";
-import path5 from "node:path";
-import { fileURLToPath as fileURLToPath3 } from "node: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 = 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) {
- const name = userAddressName || "\u7528\u6237";
- return `## \u54C1\u724C\u4E0E\u79F0\u547C\uFF08\u786C\u6027\uFF09
-
-- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4ECB\u7ECD\u4EA7\u54C1\u65F6\u7528 TKMind\uFF0C\u4E0D\u8981\u79F0 goose\u3001Goose\u3001goosed
-- \u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\uFF0C\u7528 **${name}** \u79F0\u547C\u7528\u6237\uFF08\u53EF\u8F85\u4EE5\u300C\u4F60/\u60A8\u300D\uFF09\uFF0C**\u7981\u6B62**\u628A\u7528\u6237\u53EB\u4F5C TKMind
-- \u4EC5\u5728\u5F00\u573A\u6216\u7528\u6237\u6253\u62DB\u547C\u65F6\u4F7F\u7528\u65F6\u6BB5\u95EE\u5019\uFF0C\u4E14\u987B\u4E0E\u300CTKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6\u300D\u4E00\u81F4\uFF08\u5982\u300C${name}\uFF0C\u65E9\u4E0A\u597D\u300D\uFF09\uFF1B\u666E\u901A\u56DE\u590D\u76F4\u63A5\u4F5C\u7B54\uFF0C\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\u95EE\u5019\uFF1B\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind
-- \u4E0D\u8981\u63CF\u8FF0\u672C\u5DE5\u4F5C\u533A\u4E3A\u300CRust goose \u9879\u76EE\u300D\u6216\u300Cgoose AI \u6846\u67B6\u300D
-- \u672C\u5DE5\u4F5C\u533A\u662F TKMind **MindSpace \u7528\u6237\u7A7A\u95F4**\uFF0C\u7528\u4E8E\u6587\u4EF6\u7BA1\u7406\u4E0E\u9759\u6001\u9875\u9762\u751F\u6210
-`;
-}
-var INTERNAL_ID_PATTERN = /^wx_[a-z0-9_]{4,64}$|^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
-function resolveUserAddressName({ displayName, username, slug } = {}) {
- const preferred = String(displayName ?? "").trim();
- if (preferred && !INTERNAL_ID_PATTERN.test(preferred)) return preferred;
- const uname = String(username ?? "").trim();
- if (uname && !INTERNAL_ID_PATTERN.test(uname)) return uname;
- const fallback = String(slug ?? "").trim();
- if (fallback && !INTERNAL_ID_PATTERN.test(fallback)) return fallback;
- return "\u7528\u6237";
-}
-function resolvePublicBaseUrl(env = process.env) {
- return (env.H5_PUBLIC_BASE_URL ?? "https://mm.tkmind.cn").replace(/\/$/, "");
-}
-function resolvePublishKey(user) {
- if (!user?.id) throw new Error("\u7F3A\u5C11\u7528\u6237 ID");
- return String(user.id).trim().toLowerCase();
-}
-function resolveUsernameSlug(user) {
- if (!user?.username) throw new Error("\u7F3A\u5C11\u7528\u6237\u540D");
- return String(user.username).trim().toLowerCase();
-}
-function resolvePublishDir(h5Root, 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 path5.join(path5.resolve(canonicalRoot), resolvePublishKey(user));
- }
- return resolvePublishDir(h5Root, user);
-}
-function resolveLegacyPublishDir(h5Root, user) {
- if (!user?.username) return null;
- return path5.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 = path5.join(sourceDir, entry.name);
- const to = path5.join(targetDir, entry.name);
- if (entry.isDirectory()) {
- fs3.mkdirSync(to, { recursive: true });
- mergePublishTrees(from, to);
- } else if (!fs3.existsSync(to)) {
- fs3.copyFileSync(from, to);
- }
- }
-}
-function migrateUserPublishDir(h5Root, user, legacyUsersRoot = null) {
- const target = resolvePublishDir(h5Root, user);
- fs3.mkdirSync(target, { recursive: true });
- const legacyDirs = [
- resolveLegacyPublishDir(h5Root, user),
- legacyUsersRoot ? path5.join(legacyUsersRoot, resolveUsernameSlug(user)) : null
- ].filter(Boolean);
- for (const legacyDir of legacyDirs) {
- if (path5.resolve(legacyDir) === path5.resolve(target)) continue;
- mergePublishTrees(legacyDir, target);
- }
- return target;
-}
-function buildPublicUrl(publicBaseUrl, publishKey, filename = "") {
- if (!filename) {
- return createMindSpaceUserRootUrl({ publicBaseUrl, ownerKey: publishKey });
- }
- return createMindSpacePublicUrl({ publicBaseUrl, ownerKey: publishKey, relativePath: filename });
-}
-function buildPublicZonePageUrl(publicBaseUrl, publishKey, filename) {
- return createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey: publishKey, filename });
-}
-function renderPublishSkill({ slug, username, publicBaseUrl, publishDir, displayName }) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- const exampleUrl = buildPublicZonePageUrl(publicBaseUrl, slug, "report.html");
- return `---
-name: ${PUBLISH_SKILL_NAME}
-description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u5F00\u8BBF\u95EE\u7684\u9759\u6001 HTML \u62A5\u544A\u4E0E\u9875\u9762\uFF08TKMind H5\uFF09
----
-
-# \u9759\u6001\u9875\u9762 / \u62A5\u544A\u53D1\u5E03\uFF08TKMind\uFF09
-
-\u5F53\u7528\u6237\u9700\u8981**\u7F51\u9875\u3001HTML \u62A5\u544A\u3001\u53EF\u89C6\u5316\u9875\u9762\u3001\u53EF\u5206\u4EAB\u94FE\u63A5**\u65F6\uFF0C**\u5FC5\u987B\u5148\u52A0\u8F7D\u672C\u6280\u80FD**\u5E76\u6309\u4EE5\u4E0B\u89C4\u5219\u6267\u884C\u3002
-
-## \u786C\u6027\u7EA6\u675F\uFF08\u4E0D\u53EF\u8FDD\u53CD\uFF09
-
-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\`\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
- \`${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${slug}/public/<\u6587\u4EF6\u540D>\`
- \uFF08\u5199\u5165 \`public/\u9875\u9762.html\` \u65F6 URL **\u5FC5\u987B**\u542B \`public/\`\uFF1B\u4EC5\u5F53 HTML \u76F4\u63A5\u5199\u5728 workspace \u6839\u76EE\u5F55\u65F6\u624D\u7701\u7565\uFF09
-
-## \u63A8\u8350\u5DE5\u4F5C\u6D41
-
-1. \u786E\u8BA4\u9700\u6C42\uFF08\u6807\u9898\u3001\u7AE0\u8282\u3001\u662F\u5426\u8981\u56FE\u8868/\u6837\u5F0F\uFF09
-2. \u4F7F\u7528 \`write_file\` \u521B\u5EFA \`public/\u9875\u9762.html\`\uFF08\u53EF\u542B\u5185\u8054 CSS\uFF1B\u9700\u8981\u65F6\u5728 \`public/assets/\` \u6216\u5DE5\u4F5C\u533A \`assets/\` \u653E\u8D44\u6E90\uFF09
-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. \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
-
-## \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\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\`
-- \u751F\u6210\u540E\u5FC5\u987B\u7528 \`list_dir public\`\uFF08\u6216\u7B49\u4EF7\u65B9\u5F0F\uFF09\u786E\u8BA4\u76EE\u6807 \`.docx\` \u5DF2\u5B58\u5728\uFF0C\u518D\u5199 \`public/\u9875\u9762.html\`
-- HTML \u4E2D\u53EA\u80FD\u4F7F\u7528\u540C\u76EE\u5F55\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u6587\u6863\uFF0C\u4F8B\u5982 \`\u4E0B\u8F7D Word \u6587\u6863\`
-- **\u7981\u6B62**\u53EA\u5199\u4E0B\u8F7D\u6309\u94AE\u6216 \`.docx\` \u94FE\u63A5\uFF0C\u5374\u6CA1\u6709\u5148\u628A\u76EE\u6807\u6587\u6863\u843D\u76D8
-
-## \u56DE\u590D\u683C\u5F0F\uFF08\u5FC5\u987B\uFF09
-
-\u5411\u7528\u6237\u4EA4\u4ED8\u9875\u9762\u65F6\uFF0C**\u5FC5\u987B\u4F7F\u7528 Markdown \u53EF\u70B9\u51FB\u94FE\u63A5**\uFF0C\u8BA9\u7528\u6237\u5728\u804A\u5929\u91CC\u76F4\u63A5\u70B9\u5F00\u9884\u89C8\uFF1A
-
-\`\`\`markdown
-[\u666E\u62C9\u63D0 618 \u6D3B\u52A8\u9875](${exampleUrl})
-\`\`\`
-
-\u8981\u6C42\uFF1A
-- **\u5FC5\u987B**\u4F7F\u7528 \`[\u6807\u9898](URL)\` \u683C\u5F0F\uFF0C\u4E0D\u8981\u53EA\u7ED9\u88F8 URL
-- \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
-
-\u6BCF\u4E2A HTML \u9875\u9762\u90FD\u5FC5\u987B\u5728 \`\` \u5305\u542B\u4E0E**\u9875\u9762\u4E3B\u9898\u4E00\u81F4**\u7684\u9884\u89C8\u5143\u6570\u636E\u3002\u7CFB\u7EDF\u636E\u6B64\u751F\u6210 **\u7CBE\u7F8E\u7684 3:4 \u4FE1\u606F\u6D41\u5C01\u9762**\uFF08\u300C\u6211\u7684\u7A7A\u95F4\u300D\u5361\u7247 + \u5DE5\u4F5C\u533A \`.thumbnail.svg\` + \u4FDD\u5B58\u5F39\u7A97\u9884\u89C8\uFF09\uFF1A
-
-\`\`\`html
-
-
-\`\`\`
-
-| \u5B57\u6BB5 | \u8981\u6C42 |
-|------|------|
-| \`tag\` | \u4E0E\u9875\u9762\u4E3B\u9898\u4E00\u81F4\uFF1A\u65C5\u884C / \u7F8E\u98DF / \u62A5\u544A / **\u8FD0\u52A8** / **\u6D3B\u52A8** \u7B49\uFF1B\u51B3\u5B9A\u9ED8\u8BA4\u914D\u8272 |
-| \`accent\` / \`accent2\` | \u4ECE\u9875\u9762\u4E3B\u8272\u63D0\u53D6\uFF0C\u4E0E hero/\u80CC\u666F\u4E00\u81F4 |
-| \`subtitle\` | \u4E00\u53E5\u8BDD\u5356\u70B9\uFF1B\u672A\u5199\u65F6\u7528 description |
-| \`cover\` / \`image\` | **\u5FC5\u987B**\u6307\u5411\u9AD8\u8D28\u91CF\u4E3B\u56FE\uFF08\u76F8\u5BF9 HTML \u6216 https\uFF09\uFF1B\u89C1\u4E0B\u6587 |
-| \`emoji\` | \u53EF\u9009\uFF1B\u4E5F\u53EF\u5199\u5728 title \u4E2D |
-
-### \u7CBE\u7F8E\u9884\u89C8\u56FE\uFF08\u5FC5\u987B\u8FBE\u6807\uFF09
-
-\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\u3002\u8981\u4EA7\u51FA**\u53EF\u5728\u4FE1\u606F\u6D41\u4E2D\u76F4\u63A5\u5C55\u793A\u7684\u7CBE\u7F8E\u5C01\u9762**\uFF0C\u5FC5\u987B\uFF1A
-
-1. **\u89C6\u89C9\u7C7B\u9875\u9762**\uFF08\u65C5\u884C\u3001\u7F8E\u98DF\u3001\u6D3B\u52A8\u3001\u8FD0\u52A8\u3001\u54C1\u724C\u3001\u4EA7\u54C1\u3001\u4FC3\u9500\u7B49\uFF09**\u5FC5\u987B**\u5728 \`assets/\` \u653E\u7F6E\u9AD8\u8D28\u91CF hero \u4E3B\u56FE\uFF08\u5EFA\u8BAE\u5BBD\u5EA6 \u22651200px\uFF09\uFF0C\u5E76\u5728 \`cover\` \u5B57\u6BB5\u5F15\u7528
-2. **\u7EAF\u6587\u5B57\u62A5\u544A**\u53EF\u4EC5\u7528\u914D\u8272 + tag\uFF0C\u4F46\u4ECD\u987B\u4FDD\u8BC1 \`accent\` / \`subtitle\` \u4E0E\u9875\u9762\u98CE\u683C\u4E00\u81F4
-3. \`tag\`\u3001\`accent\`\u3001\`accent2\`\u3001\`subtitle\` \u5FC5\u987B\u4E0E\u9875\u9762\u5B9E\u9645\u89C6\u89C9\u4E00\u81F4\uFF1B**\u7981\u6B62**\u7701\u7565 mindspace-cover \u6216\u586B\u65E0\u5173\u9ED8\u8BA4\u503C
-4. \u82E5\u7F3A\u5C11 hero \u4E3B\u56FE\uFF0C\u5C01\u9762\u4F1A\u9000\u5316\u4E3A\u7B80\u964B\u9ED8\u8BA4\u56FE\uFF0C**\u89C6\u4E3A\u672A\u8FBE\u6807**
-
-**\u7981\u6B62**\u4F7F\u7528\u4E0E\u9875\u9762\u65E0\u5173\u7684\u901A\u7528\u98CE\u666F\u56FE\u903B\u8F91\uFF1B\u4FC3\u9500/\u8FD0\u52A8/\u54C1\u724C\u9875\u5FC5\u987B\u5199\u660E \`tag\`\u3001\`accent\` \u548C \`cover\`\u3002
-
-## HTML \u6A21\u677F\u5EFA\u8BAE
-
-- \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\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
-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
-
-## \u67E5\u627E\u6587\u4EF6\uFF08CSV\u3001\u6587\u6863\u7B49\uFF09
-
-- \u53EA\u5728**\u5F53\u524D\u5DE5\u4F5C\u533A**\u5185\u4ECE \`.\` \u641C\u7D22\uFF08\u5982 \`list_dir\`\u3001\`list_dir oa\`\uFF0C\u6216 shell \u53EF\u7528\u65F6 \`find . -name '*.csv'\`\uFF09
-- \u7528\u6237\u8BF4\u7684\u300COA \u533A\u300D\u7B49\u89C6\u4E3A\u5DE5\u4F5C\u533A**\u5B50\u76EE\u5F55**\uFF08\u5982 \`oa/\`\uFF09\uFF0C\u7528\u76F8\u5BF9\u8DEF\u5F84\u67E5\u627E
-- **\u7981\u6B62**\u641C\u7D22\u4E0A\u7EA7\u76EE\u5F55\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\`${PUBLISH_ROOT_DIR}\` \u6839\u76EE\u5F55\u3001\`/Users\`\u3001\u9879\u76EE\u6839\u76EE\u5F55
-- \u627E\u4E0D\u5230\u65F6\u544A\u77E5\u7528\u6237\u4E0A\u4F20\u6216\u63D0\u4F9B\u76F8\u5BF9\u8DEF\u5F84\uFF0C**\u4E0D\u8981**\u6269\u5927\u5230\u5DE5\u4F5C\u533A\u5916
-
-## \u7981\u6B62\u4E8B\u9879
-
-- \u4E0D\u8981\u5199\u5165\u5F53\u524D\u5DE5\u4F5C\u533A\u4EE5\u5916\u7684\u4EFB\u4F55\u76EE\u5F55
-- 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
-
-\u7528\u6237\uFF1A\u300C\u5199\u4E00\u4EFD\u8D8A\u5357\u65C5\u6E38\u6307\u5357\u7F51\u9875\u300D
-
-\u6B63\u786E\u505A\u6CD5\uFF1A
-- \`write_file\` \u2192 \`public/vietnam-guide.html\`\uFF08\u5185\u5BB9\u5B8C\u6574\uFF09
-- \u56DE\u590D\u94FE\u63A5\uFF1A\`${buildPublicZonePageUrl(publicBaseUrl, slug, "vietnam-guide.html")}\`
-`;
-}
-function renderWorkspaceHints({ slug, username, publishDir, displayName }) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- return `# TKMind \u7528\u6237\u5DE5\u4F5C\u533A\u8FB9\u754C
-
-\u4F60\u662F\u7528\u6237 **${addressName}** \u7684\u4E13\u5C5E TKMind \u52A9\u624B\u3002\u5F53\u524D\u4F1A\u8BDD**\u552F\u4E00**\u6587\u4EF6\u6839\u76EE\u5F55\uFF1A
-\`${publishDir}\`
-
-${renderBrandingBlock(addressName)}
-## \u67E5\u627E / \u8BFB\u53D6\u6587\u4EF6\uFF08CSV\u3001\u6587\u6863\u3001OA \u533A\u7B49\uFF09
-
-1. **\u53EA\u80FD**\u5728\u672C\u5DE5\u4F5C\u533A\u5185\u641C\u7D22\uFF0C\u4F8B\u5982 \`list_dir\`\u3001\`list_dir oa\`\uFF0C\u6216 shell \u53EF\u7528\u65F6 \`find . -name '*.csv'\`\u3001\`rg keyword .\`
-2. \u7528\u6237\u8BF4\u7684\u300COA \u533A\u300D\u300C\u6587\u6863\u533A\u300D\u7B49\uFF0C**\u5148\u5F53\u4F5C\u5DE5\u4F5C\u533A\u5185\u7684\u5B50\u76EE\u5F55**\uFF08\u5982 \`oa/\`\u3001\`docs/\`\uFF09\uFF0C\u53EA\u7528\u76F8\u5BF9\u8DEF\u5F84\u67E5\u627E
-3. **\u7EDD\u5BF9\u7981\u6B62**\uFF1A
- - \u53BB\u4E0A\u7EA7\u76EE\u5F55\u3001\`${PUBLISH_ROOT_DIR}\` \u6839\u76EE\u5F55\u3001\u5176\u5B83\u7528\u6237\u540D\u76EE\u5F55\u3001\u9879\u76EE\u6839\u76EE\u5F55\u641C\u7D22
- - \u4F7F\u7528 \`/Users\`\u3001\`/home\`\u3001Desktop\u3001Documents\u3001Downloads \u7B49\u4E3B\u673A\u8DEF\u5F84
- - \u5728\u56DE\u590D\u4E2D\u5EFA\u8BAE\u300C\u5728 MindSpace \u76EE\u5F55\u6216\u9644\u8FD1\u76EE\u5F55\u641C\u7D22\u300D\u2014\u2014\u4F60\u6CA1\u6709\u8FD9\u4E2A\u6743\u9650
-4. \u627E\u4E0D\u5230\u6587\u4EF6\u65F6\uFF1A\u8BF4\u660E\u5728\u5176\u5DE5\u4F5C\u533A\u5185\u672A\u627E\u5230\uFF0C\u8BF7\u7528\u6237\u4E0A\u4F20\u6216\u63D0\u4F9B\u76F8\u5BF9\u8DEF\u5F84\uFF1B**\u4E0D\u5F97**\u6269\u5927\u5230\u5DE5\u4F5C\u533A\u5916
-
-## \u516C\u7F51\u94FE\u63A5 vs \u672C\u5730\u6587\u4EF6
-
-- \u516C\u7F51 URL\uFF08\u5982 \`https://\u2026/MindSpace/<\u7528\u6237>/\`\uFF09**\u4EC5\u4F9B\u7528\u6237\u6D4F\u89C8\u5668\u6253\u5F00\u5DF2\u53D1\u5E03\u7684 HTML \u9875\u9762**
-- **\u7981\u6B62**\u7528\u516C\u7F51 URL\u3001curl\u3001wget \u6216 read_image \u53BB\u300C\u5217\u76EE\u5F55\u300D\u300C\u8BFB CSV\u300D\u2014\u2014\u9759\u6001\u7AD9\u70B9\u4E0D\u63D0\u4F9B\u76EE\u5F55\u7D22\u5F15
-- \u6709 \`shell\` \u65F6\u7528 \`ls oa/\`\u3001\`find .\`\u3001\`cat oa/file.csv\`\uFF1B\u6709 \`tree\` \u65F6\u7528 \`tree oa/\`
-- \u4E0D\u8981\u8BF4\u300C\u6211\u6CA1\u6709 shell\u300D\u2014\u2014\u82E5\u7CFB\u7EDF\u63D0\u793A\u5DF2\u5217\u51FA shell\uFF0C\u5C31\u5FC5\u987B\u7528 shell \u5728\u5DE5\u4F5C\u533A\u5185\u64CD\u4F5C
-
-## Word \u4E0B\u8F7D\u9875
-
-- \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 = [] }) {
- const tools = developerTools.length > 0 ? developerTools : ["write_file", "edit_file"];
- const hasSandboxMcp = tools.includes("write_file") || tools.includes("read_file");
- const hasShell = tools.includes("shell");
- const hasListDir = tools.includes("list_dir") || tools.includes("tree");
- const lines = [
- baseConstraints,
- "",
- "## \u5F53\u524D\u4F1A\u8BDD\u53EF\u7528\u6587\u4EF6\u5DE5\u5177",
- `- ${tools.join(", ")}`,
- hasSandboxMcp ? "- \u6587\u4EF6\u5DE5\u5177\u7531**\u6C99\u7BB1 MCP** \u63D0\u4F9B\uFF1A\u6240\u6709\u8DEF\u5F84\u5728 OS \u5C42\u5F3A\u5236\u9650\u5236\u5728\u5DE5\u4F5C\u533A\u5185\uFF0C\u8D8A\u754C\u8BBF\u95EE\u4F1A\u7ACB\u5373\u62A5\u9519" : ""
- ].filter((l) => l !== "");
- if (hasShell) {
- lines.push(
- '- **shell \u5DF2\u5F00\u542F**\uFF1A\u53EA\u80FD\u5728\u672C\u5DE5\u4F5C\u533A\u5185\u6267\u884C\uFF08\u5982 `ls oa/`\u3001`find . -name "*.csv"`\u3001`cat oa/\u6587\u4EF6.csv`\uFF09',
- "- **\u7981\u6B62** curl/wget \u6216\u8BBF\u95EE\u516C\u7F51 URL \u6765\u8BFB\u53D6\u5DE5\u4F5C\u533A\u6587\u4EF6"
- );
- } else {
- lines.push(
- "- **shell \u672A\u5F00\u542F**\uFF1A\u4E0D\u80FD\u6267\u884C shell\uFF1B\u4E5F\u4E0D\u8981\u7528\u516C\u7F51 URL \u4EE3\u66FF\u672C\u5730\u8BFB\u6587\u4EF6"
- );
- }
- if (hasListDir) {
- lines.push("- **list_dir \u53EF\u7528**\uFF1A`list_dir` \u6216 `list_dir oa` \u67E5\u770B\u76EE\u5F55\u7ED3\u6784");
- } else {
- lines.push("- \u9700\u8981\u5217\u76EE\u5F55\u6216\u8BFB CSV \u65F6\uFF0C\u8BF7\u7528\u6237\u4E0A\u4F20\u6587\u4EF6\u5185\u5BB9\u6216\u5F00\u542F list_dir \u80FD\u529B");
- }
- lines.push(
- "",
- "## \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",
- "- \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",
- "- \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");
-}
-function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, displayName }) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- return [
- "## TKMind \u7528\u6237\u7A7A\u95F4\u6C99\u7BB1\uFF08\u786C\u6027\u7EA6\u675F\uFF09",
- "",
- "- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\u7528 **" + addressName + "** \u79F0\u547C\u7528\u6237\uFF0C\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind",
- "- \u7981\u6B62\u79F0 goose / Goose / goosed \u6216\u300CRust goose \u9879\u76EE\u300D",
- `- \u5F53\u524D\u7528\u6237 **${addressName}** \u7684 Agent \u5DE5\u4F5C\u533A\uFF08\u552F\u4E00\u6587\u4EF6\u6839\u76EE\u5F55\uFF09\uFF1A\`${publishDir}\``,
- "- \u7528\u6237\u4E0A\u4F20\u843D\u5728\u5206\u533A\u5B50\u76EE\u5F55\uFF1A`oa/`\u3001`private/`\u3001`public/`\uFF08\u5747\u5728\u4E0A\u8FF0\u5DE5\u4F5C\u533A\u5185\uFF09",
- `- \u516C\u7F51 HTML \u524D\u7F00\uFF08\u516C\u5F00\u533A\u9875\u9762\uFF09\uFF1A\`${buildPublicUrl(publicBaseUrl, slug, `${PUBLIC_ZONE_DIR}/`)}\``,
- "- **\u67E5\u627E\u6587\u4EF6**\uFF1A\u5728\u5DE5\u4F5C\u533A\u5185\u5BF9\u5E94\u5206\u533A\u641C\u7D22\uFF08\u5982 `oa/2025-12-06T13-34_export.csv`\uFF09\uFF0C\u4E0D\u8981\u641C\u5176\u5B83\u7528\u6237\u76EE\u5F55\u6216\u516C\u7F51 URL",
- "- **\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`",
- "- **\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`",
- "- \u82E5\u5148\u7528 `apps__create_app` \u8BBE\u8BA1/\u9884\u89C8\uFF0C\u6700\u540E\u4ECD\u8981\u628A\u5185\u5BB9 write_file \u843D\u5230 `public/\u9875\u9762.html` \u624D\u6709\u516C\u7F51\u94FE\u63A5",
- "- **\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",
- "- \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 = path5.join(publishDir, WORKSPACE_HINTS_FILENAME);
- const legacyPath = path5.join(publishDir, LEGACY_WORKSPACE_HINTS_FILENAME);
- const content = renderWorkspaceHints(context);
- fs3.writeFileSync(hintsPath, content, "utf8");
- if (fs3.existsSync(legacyPath)) {
- fs3.unlinkSync(legacyPath);
- }
- return hintsPath;
-}
-function ensureDocxSkillInstalled(publishDir) {
- 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);
- 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 = fs3.existsSync(skillPath) ? fs3.readFileSync(skillPath, "utf8") : "";
- if (existing !== content) {
- fs3.writeFileSync(skillPath, content, "utf8");
- }
- return skillPath;
-}
-function ensureUserPublishLayout({
- h5Root,
- publicBaseUrl,
- user,
- legacyUsersRoot = null,
- installWorkspaceHints = false
-}) {
- migrateUserPublishDir(h5Root, user, legacyUsersRoot);
- const slug = resolvePublishKey(user);
- const username = user.username ? resolveUsernameSlug(user) : slug;
- const displayName = user.displayName ?? user.display_name ?? null;
- const publishDir = resolvePublishDir(h5Root, user);
- fs3.mkdirSync(path5.join(publishDir, "assets"), { recursive: true });
- const context = { slug, username, displayName, publicBaseUrl, publishDir };
- if (installWorkspaceHints) {
- ensureWorkspaceHintsInstalled(publishDir, context);
- ensurePublishSkillInstalled(publishDir, context);
- }
- return {
- slug,
- username,
- displayName,
- publishDir,
- publicBaseUrl,
- publicUrl: buildPublicUrl(publicBaseUrl, slug),
- skillName: PUBLISH_SKILL_NAME,
- constraints: buildPublishConstraints(context)
- };
-}
-
-// user-memory-profile.mjs
-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";
-function buildInitialUserMemoryProfile({
- userId,
- displayName,
- username,
- slug,
- now = Date.now()
-}) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- return {
- version: USER_MEMORY_PROFILE_VERSION,
- userId,
- displayName: addressName,
- language: "zh-CN",
- responseStyle: "balanced",
- preferences: [],
- createdAt: now,
- updatedAt: now
- };
-}
-function resolveUserMemoryProfilePath(workspaceRoot) {
- return path6.join(workspaceRoot, USER_MEMORY_PROFILE_FILENAME);
-}
-function loadUserMemoryProfile(workspaceRoot) {
- const profilePath = resolveUserMemoryProfilePath(workspaceRoot);
- if (!fs4.existsSync(profilePath)) return null;
- try {
- const raw = fs4.readFileSync(profilePath, "utf8");
- const parsed = JSON.parse(raw);
- if (!parsed || typeof parsed !== "object") return null;
- return parsed;
- } catch {
- return null;
- }
-}
-function ensureUserMemoryProfile(workspaceRoot, context) {
- const profilePath = resolveUserMemoryProfilePath(workspaceRoot);
- const existing = loadUserMemoryProfile(workspaceRoot);
- if (existing) {
- const addressName = resolveUserAddressName(context);
- const nextDisplayName = addressName || existing.displayName;
- if (nextDisplayName && nextDisplayName !== existing.displayName) {
- const updated = {
- ...existing,
- displayName: nextDisplayName,
- updatedAt: Date.now()
- };
- fs4.writeFileSync(profilePath, `${JSON.stringify(updated, null, 2)}
-`, "utf8");
- return updated;
- }
- return existing;
- }
- const profile = buildInitialUserMemoryProfile({
- userId: context.userId,
- displayName: context.displayName,
- username: context.username,
- slug: context.slug
- });
- fs4.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}
-`, "utf8");
- return profile;
-}
-function renderMemoryStoreGuidance({ addressName }) {
- const name = addressName || "\u7528\u6237";
- return [
- "## TKMind \u957F\u671F\u8BB0\u5FC6\uFF08L3 / memory \u6269\u5C55\uFF09",
- "",
- `- \u5F53\u524D\u670D\u52A1\u5BF9\u8C61\uFF1A**${name}**\uFF08\u4EC5\u6B64\u7528\u6237\uFF0C\u4E0D\u5F97\u4E0E\u5176\u4ED6\u7528\u6237\u6DF7\u6DC6\uFF09`,
- "- \u4F7F\u7528 memory \u5DE5\u5177\u8BB0\u5F55\u7528\u6237**\u660E\u786E\u8868\u8FBE**\u7684\u504F\u597D\u3001\u4E60\u60EF\u4E0E\u7A33\u5B9A\u4E8B\u5B9E",
- "- \u8BB0\u5FC6\u5206\u7C7B\u5EFA\u8BAE\uFF1A",
- " - `preference`\uFF1A\u56DE\u590D\u98CE\u683C\u3001\u683C\u5F0F\u3001\u8BED\u8A00\u3001\u79F0\u547C\u4E60\u60EF",
- " - `project`\uFF1A\u5F53\u524D\u9879\u76EE\u76EE\u6807\u3001\u7EA6\u5B9A\u3001\u6280\u672F\u9009\u578B\uFF08\u4E0E L2 \u9879\u76EE\u8BB0\u5FC6\u4E92\u8865\uFF09",
- " - `fact`\uFF1A\u957F\u671F\u7A33\u5B9A\u3001\u53EF\u590D\u7528\u7684\u4E8B\u5B9E\uFF08\u5982\u90E8\u95E8\u3001\u5E38\u7528\u5DE5\u5177\uFF09",
- "- \u7528\u6237\u8BF4\u300C\u5FD8\u8BB0\u2026\u300D\u300C\u4E0D\u8981\u518D\u2026\u300D\u300C\u53D6\u6D88\u504F\u597D\u2026\u300D\u65F6\uFF0C\u5FC5\u987B\u66F4\u65B0\u6216\u5220\u9664\u5BF9\u5E94\u8BB0\u5FC6",
- "- **\u7981\u6B62**\u8BB0\u5F55\u5176\u4ED6\u7528\u6237\u7684\u4FE1\u606F\uFF1B**\u7981\u6B62**\u731C\u6D4B\u672A\u660E\u786E\u8868\u8FBE\u7684\u504F\u597D",
- "- \u4E0E L2 \u9879\u76EE\u8BB0\u5FC6\uFF08harness\uFF09\u5206\u5DE5\uFF1AL2 \u8BB0\u8FD1\u671F\u5DE5\u4F5C\u4E0E\u51B3\u7B56\u6458\u8981\uFF1BL3 \u8BB0\u53EF\u8DE8\u4F1A\u8BDD\u590D\u7528\u7684\u4E2A\u4EBA\u504F\u597D"
- ].join("\n");
-}
-function renderPreferenceLines(profile) {
- const items = Array.isArray(profile?.preferences) ? profile.preferences : [];
- if (items.length === 0) {
- return ["- \uFF08\u6682\u65E0\u7ED3\u6784\u5316\u504F\u597D \u2014 \u53EF\u5728\u5BF9\u8BDD\u4E2D\u7528 memory \u5DE5\u5177\u8865\u5145\uFF09"];
- }
- return items.map((item) => {
- if (typeof item === "string") return `- ${item}`;
- const category = item?.category ? `[${item.category}] ` : "";
- const label = item?.label ?? item?.key ?? "preference";
- const value = item?.value ?? item?.content ?? "";
- return `- ${category}${label}\uFF1A${value}`;
- });
-}
-function renderUserMemoryProfileForHarness(profile, context = {}) {
- const addressName = resolveUserAddressName({
- displayName: profile?.displayName ?? context.displayName,
- username: context.username,
- slug: context.slug
- });
- const language = profile?.language === "zh-CN" ? "\u7B80\u4F53\u4E2D\u6587" : profile?.language ?? "\u7B80\u4F53\u4E2D\u6587";
- const styleMap = {
- concise: "\u7B80\u6D01\u76F4\u63A5\uFF0C\u5C11\u5E9F\u8BDD",
- detailed: "\u8BE6\u7EC6\u5B8C\u6574\uFF0C\u5FC5\u8981\u65F6\u5C55\u5F00",
- balanced: "\u7B80\u6D01\u52A1\u5B9E\uFF0C\u5FC5\u8981\u65F6\u8865\u5145\u7EC6\u8282"
- };
- const responseStyle = styleMap[profile?.responseStyle] ?? styleMap.balanced;
- return [
- "## TKMind \u7528\u6237\u504F\u597D\u753B\u50CF\uFF08L3\uFF09",
- "",
- `- \u7528\u6237\u79F0\u547C\uFF1A**${addressName}**`,
- `- \u754C\u9762\u8BED\u8A00\uFF1A${language}`,
- `- \u9ED8\u8BA4\u56DE\u590D\u98CE\u683C\uFF1A${responseStyle}`,
- "",
- "### \u5DF2\u8BB0\u5F55\u7684\u7ED3\u6784\u5316\u504F\u597D",
- ...renderPreferenceLines(profile)
- ].join("\n");
-}
-function renderStoredUserMemoryLines(memories) {
- const items = Array.isArray(memories) ? memories : [];
- if (items.length === 0) return ["- \uFF08\u6682\u65E0\u5DF2\u6C89\u6DC0\u7684\u5BF9\u8BDD\u8BB0\u5FC6\uFF09"];
- return items.slice(0, 50).map((item) => {
- const label = item?.label ? `[${item.label}] ` : "";
- const text = String(item?.text ?? item?.memory_text ?? item?.memoryText ?? "").trim();
- return `- ${label}${text}`;
- }).filter((line) => line.trim() !== "-");
-}
-function renderStoredUserMemoriesForHarness(memories) {
- return [
- "## TKMind \u5DF2\u6C89\u6DC0\u7528\u6237\u8BB0\u5FC6",
- "",
- "\u4EE5\u4E0B\u5185\u5BB9\u6765\u81EA\u7528\u6237\u5386\u53F2\u5BF9\u8BDD\u7684\u957F\u671F\u8BB0\u5FC6\u62BD\u53D6\uFF0C\u4EC5\u7528\u4E8E\u6539\u5584\u5F53\u524D\u7528\u6237\u4F53\u9A8C\u3002",
- "\u4E0D\u8981\u628A\u8FD9\u4E9B\u5185\u5BB9\u900F\u9732\u7ED9\u5176\u4ED6\u7528\u6237\uFF1B\u5982\u679C\u7528\u6237\u8981\u6C42\u5FD8\u8BB0\u6216\u5220\u9664\uFF0C\u5E94\u9075\u4ECE\u5E76\u66F4\u65B0\u5BF9\u5E94\u8BB0\u5FC6\u3002",
- "",
- ...renderStoredUserMemoryLines(memories)
- ].join("\n");
-}
-function formatDateParts(now, timezone) {
- const formatter = new Intl.DateTimeFormat("en-CA", {
- timeZone: timezone,
- year: "numeric",
- month: "2-digit",
- day: "2-digit"
- });
- const parts = Object.fromEntries(
- formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value])
- );
- return {
- year: parts.year ?? "0000",
- month: parts.month ?? "01",
- day: parts.day ?? "01"
- };
-}
-function formatLocalClockParts(now, timezone) {
- const formatter = new Intl.DateTimeFormat("en-GB", {
- timeZone: timezone,
- hour: "2-digit",
- minute: "2-digit",
- hour12: false
- });
- const parts = Object.fromEntries(
- formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value])
- );
- return {
- hour: Number(parts.hour ?? 0),
- minute: Number(parts.minute ?? 0)
- };
-}
-function resolveTimeOfDayPeriod(hour) {
- const h = Number(hour);
- if (h < 5) return { period: "\u51CC\u6668", greeting: "\u4F60\u597D" };
- if (h < 9) return { period: "\u65E9\u4E0A", greeting: "\u65E9\u4E0A\u597D" };
- if (h < 12) return { period: "\u4E0A\u5348", greeting: "\u4E0A\u5348\u597D" };
- if (h < 14) return { period: "\u4E2D\u5348", greeting: "\u4E2D\u5348\u597D" };
- if (h < 18) return { period: "\u4E0B\u5348", greeting: "\u4E0B\u5348\u597D" };
- return { period: "\u665A\u4E0A", greeting: "\u665A\u4E0A\u597D" };
-}
-function buildCurrentTimeAgentPrefix({ now = Date.now(), timezone = DEFAULT_TIMEZONE } = {}) {
- return [
- "\u3010TKMind \u65F6\u95F4\u57FA\u51C6 - \u4EC5\u4F9B\u56DE\u7B54\u65F6\u95F4/\u65E5\u671F/\u65F6\u6BB5\u95EE\u5019\uFF0C\u4E0D\u8981\u5411\u7528\u6237\u590D\u8FF0\u3011",
- renderCurrentTimeAnchor({ now, timezone }),
- ""
- ].join("\n");
-}
-function renderCurrentTimeAnchor({ now = Date.now(), timezone = DEFAULT_TIMEZONE } = {}) {
- const { year, month, day } = formatDateParts(now, timezone);
- const { hour, minute } = formatLocalClockParts(now, timezone);
- const { period, greeting } = resolveTimeOfDayPeriod(hour);
- const clock = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
- const weekday = new Intl.DateTimeFormat("zh-CN", {
- timeZone: timezone,
- weekday: "long"
- }).format(new Date(now));
- return [
- "## TKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6",
- "",
- `- \u5F53\u524D\u65F6\u533A\uFF1A${timezone}`,
- `- \u5F53\u524D\u65E5\u671F\uFF1A${year}-${month}-${day}\uFF08${weekday}\uFF09`,
- `- \u5F53\u524D\u65F6\u523B\uFF1A${clock}\uFF08${period}\uFF09`,
- `- \u82E5\u9700\u65F6\u6BB5\u95EE\u5019\u53EF\u53C2\u8003\uFF1A${greeting}\uFF08\u4EC5\u65B0\u4F1A\u8BDD\u5F00\u573A\u6216\u7528\u6237\u4E3B\u52A8\u6253\u62DB\u547C\u65F6\u4F7F\u7528\uFF0C\u666E\u901A\u4EFB\u52A1\u56DE\u590D\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\uFF09`,
- "- \u56DE\u7B54\u4E2D\u6D89\u53CA\u201C\u4ECA\u5929 / \u660E\u5929 / \u540E\u5929 / \u5468\u51E0 / \u65E9\u4E0A / \u4E0B\u5348 / \u665A\u4E0A\u201D\u7B49\u65F6\u95F4\u8868\u8FF0\u65F6\uFF0C\u5FC5\u987B\u4EE5\u4E0A\u8FF0\u65E5\u671F\u4E0E\u65F6\u523B\u4E3A\u51C6\uFF0C\u7981\u6B62\u81EA\u884C\u5047\u8BBE\u5F53\u524D\u65F6\u95F4\u6216\u4F7F\u7528 UTC \u7B49\u5176\u5B83\u65F6\u533A\u3002"
- ].join("\n");
-}
-function buildSessionMemoryEntries({
- workingDir,
- sessionPolicy,
- sandboxConstraints = null,
- userContext = null,
- userMemories = null,
- now = Date.now()
-}) {
- const entries = [];
- const timezone = String(
- userContext?.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? DEFAULT_TIMEZONE
- ).trim() || DEFAULT_TIMEZONE;
- if (sandboxConstraints?.trim()) {
- entries.push({
- title: "TKMind \u7528\u6237\u7A7A\u95F4\u6C99\u7BB1",
- content: sandboxConstraints.trim()
- });
- }
- const executorGuidance = renderCodeExecutorGuidance(sessionPolicy);
- if (executorGuidance) {
- entries.push({
- title: "TKMind \u4EE3\u7801\u59D4\u6258\u7B56\u7565",
- content: executorGuidance
- });
- }
- entries.push({
- title: "TKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6",
- content: renderCurrentTimeAnchor({ now, timezone })
- });
- const scheduleTools = (sessionPolicy?.extensionOverrides ?? []).find((ext) => ext.name === "sandbox-fs");
- if ((scheduleTools?.available_tools ?? []).includes("schedule_create_item")) {
- entries.push({
- title: "TKMind \u65E5\u7A0B\u5199\u5165\u89C4\u5219",
- content: [
- "\u521B\u5EFA\u6216\u63D0\u9192\u5F85\u529E/\u65E5\u7A0B\u65F6\uFF1A",
- "- \u5FC5\u987B\u4F7F\u7528 startLocal / endLocal / remindLocal\uFF0C\u683C\u5F0F YYYY-MM-DD HH:mm\uFF08\u7528\u6237\u65F6\u533A\u5899\u4E0A\u65F6\u949F\uFF09\u3002",
- "- \u7981\u6B62\u81EA\u884C\u4F30\u7B97 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF1B\u4F20\u9519\u4F1A\u88AB\u5DE5\u5177\u62D2\u7EDD\u3002",
- "- \u5199\u5165\u540E\u8C03\u7528 schedule_list_items \u6838\u5BF9\u65E5\u671F\u662F\u5426\u4E0E\u7528\u6237\u8868\u8FF0\u4E00\u81F4\u3002"
- ].join("\n")
- });
- }
- if (!hasMemoryStore(sessionPolicy)) {
- return entries;
- }
- const profile = userContext?.userId ? ensureUserMemoryProfile(workingDir, userContext) : loadUserMemoryProfile(workingDir);
- const addressName = resolveUserAddressName({
- displayName: profile?.displayName ?? userContext?.displayName,
- username: userContext?.username,
- slug: userContext?.slug
- });
- entries.push({
- title: "TKMind \u957F\u671F\u8BB0\u5FC6\u89C4\u5219",
- content: renderMemoryStoreGuidance({ addressName })
- });
- if (profile) {
- entries.push({
- title: "TKMind \u7528\u6237\u504F\u597D\u753B\u50CF",
- content: renderUserMemoryProfileForHarness(profile, userContext ?? {})
- });
- }
- if (Array.isArray(userMemories) && userMemories.length > 0) {
- entries.push({
- title: "TKMind \u5DF2\u6C89\u6DC0\u7528\u6237\u8BB0\u5FC6",
- content: renderStoredUserMemoriesForHarness(userMemories)
- });
- }
- return entries;
-}
-function hasMemoryStore(sessionPolicy) {
- return (sessionPolicy?.extensionOverrides ?? []).some((ext) => ext.name === "memory");
-}
-function availableDelegateExecutors(sessionPolicy) {
- const names = new Set((sessionPolicy?.extensionOverrides ?? []).map((ext) => ext.name));
- return ["aider", "openhands"].filter((name) => names.has(name));
-}
-function resolveCodeExecutorRouting(sessionPolicy) {
- const available = availableDelegateExecutors(sessionPolicy);
- const configuredPreferred = String(
- sessionPolicy?.policies?.code_delegate_executor ?? "auto"
- ).trim();
- const routing = String(sessionPolicy?.policies?.code_task_routing ?? "balanced").trim();
- const preferred = (configuredPreferred === "aider" || configuredPreferred === "openhands") && available.includes(configuredPreferred) ? configuredPreferred : "auto";
- const rules = [];
- if (routing === "split") {
- rules.push("\u5C0F\u8303\u56F4\u8865\u4E01\u3001\u5C40\u90E8\u4FEE\u590D\u3001\u5C11\u6587\u4EF6\u4FEE\u6539\u4F18\u5148 `aider`\u3002");
- rules.push("\u590D\u6742\u591A\u6587\u4EF6\u6539\u9020\u3001\u4ED3\u5E93\u63A2\u7D22\u3001\u8F83\u91CD\u7684\u547D\u4EE4\u6267\u884C\u4F18\u5148 `openhands`\u3002");
- } else if (routing === "force_aider") {
- rules.push("\u53EA\u8981\u4EFB\u52A1\u80FD\u7531 `aider` \u80DC\u4EFB\uFF0C\u5C31\u4F18\u5148\u7EDF\u4E00\u8D70 `aider`\u3002");
- rules.push("\u53EA\u6709 `aider` \u660E\u663E\u65E0\u6CD5\u80DC\u4EFB\u65F6\u624D\u56DE\u9000\u5230 `openhands`\u3002");
- } else if (routing === "force_openhands") {
- rules.push("\u53EA\u8981\u4EFB\u52A1\u9700\u8981\u4EE3\u7801\u59D4\u6258\uFF0C\u5C31\u4F18\u5148\u7EDF\u4E00\u8D70 `openhands`\u3002");
- rules.push("\u53EA\u6709\u4EFB\u52A1\u660E\u663E\u66F4\u9002\u5408\u8F7B\u91CF\u8865\u4E01\u65F6\u624D\u56DE\u9000\u5230 `aider`\u3002");
- } else {
- rules.push("\u7531 Goose \u6839\u636E\u4EFB\u52A1\u590D\u6742\u5EA6\u3001\u6D89\u53CA\u6587\u4EF6\u6570\u3001\u662F\u5426\u9700\u8981\u4ED3\u5E93\u63A2\u7D22\u548C\u547D\u4EE4\u6267\u884C\uFF0C\u5728 `aider` \u4E0E `openhands` \u4E4B\u95F4\u5E73\u8861\u9009\u62E9\u3002");
- }
- if (preferred !== "auto") {
- rules.unshift(`\u540E\u53F0\u4F18\u5148\u6267\u884C\u5668\uFF1A**${preferred}**\u3002`);
- } else {
- rules.unshift("\u540E\u53F0\u4F18\u5148\u6267\u884C\u5668\uFF1A`auto`\uFF0C\u7531 Goose \u7ED3\u5408\u4EFB\u52A1\u7279\u5F81\u51B3\u5B9A\u3002");
- }
- return {
- available,
- preferred,
- routing,
- rules
- };
-}
-function containsAny(text, patterns) {
- return patterns.some((pattern) => pattern.test(text));
-}
-function suggestCodeExecutorForTask(taskText, sessionPolicy) {
- const text = String(taskText ?? "").trim().toLowerCase();
- if (!text) return null;
- const routing = resolveCodeExecutorRouting(sessionPolicy);
- if (routing.available.length === 0) return null;
- const codingSignals = [
- /bug|fix|debug|refactor|feature|repo|repository|code|patch|test|compile|build/,
- /修复|改代码|重构|功能|仓库|代码|补丁|测试|编译|构建|多文件|命令|脚本/
- ];
- if (!containsAny(text, codingSignals)) {
- return null;
- }
- let aiderScore = 0;
- let openhandsScore = 0;
- if (containsAny(text, [/small|minor|tiny|simple|one file|single file|quick patch/, /小改|微调|简单修复|单文件|一个文件|快速修复/])) {
- aiderScore += 2;
- }
- if (containsAny(text, [/refactor|multi-?file|repo|repository|end-to-end|investigate|explore/, /重构|多文件|仓库级|全链路|排查|探索代码库/])) {
- openhandsScore += 2;
- }
- if (containsAny(text, [/run|command|terminal|shell|build|compile|test suite/, /执行命令|终端|shell|构建|编译|整套测试/])) {
- openhandsScore += 1;
- }
- if (containsAny(text, [/rename|edit|patch|tweak/, /修改一下|补丁|小范围调整|局部编辑/])) {
- aiderScore += 1;
- }
- if (routing.routing === "split") {
- aiderScore += 1;
- openhandsScore += 1;
- } else if (routing.routing === "force_aider") {
- aiderScore += 3;
- } else if (routing.routing === "force_openhands") {
- openhandsScore += 3;
- }
- if (routing.preferred === "aider") aiderScore += 2;
- if (routing.preferred === "openhands") openhandsScore += 2;
- let suggested = null;
- if (openhandsScore > aiderScore && routing.available.includes("openhands")) {
- suggested = "openhands";
- } else if (aiderScore > openhandsScore && routing.available.includes("aider")) {
- suggested = "aider";
- } else if (routing.preferred !== "auto" && routing.available.includes(routing.preferred)) {
- suggested = routing.preferred;
- } else if (routing.routing === "split" && routing.available.includes("aider") && routing.available.includes("openhands")) {
- suggested = openhandsScore >= aiderScore ? "openhands" : "aider";
- } else {
- suggested = routing.available[0] ?? null;
- }
- if (!suggested) return null;
- const reason = suggested === "openhands" ? "\u4EFB\u52A1\u770B\u8D77\u6765\u66F4\u50CF\u590D\u6742\u591A\u6587\u4EF6\u6539\u9020\u3001\u4ED3\u5E93\u63A2\u7D22\u6216\u9700\u8981\u66F4\u591A\u547D\u4EE4\u6267\u884C\u3002" : "\u4EFB\u52A1\u770B\u8D77\u6765\u66F4\u50CF\u5C40\u90E8\u8865\u4E01\u3001\u5C0F\u8303\u56F4\u4FEE\u590D\u6216\u8F83\u8F7B\u91CF\u7684\u4EE3\u7801\u4FEE\u6539\u3002";
- return {
- suggestedExecutor: suggested,
- reason,
- routing
- };
-}
-function buildTaskRoutingAgentText(taskText, sessionPolicy) {
- const suggestion = suggestCodeExecutorForTask(taskText, sessionPolicy);
- if (!suggestion) return String(taskText ?? "").trim();
- return [
- "\u3010TKMind \u8DEF\u7531\u63D0\u793A\u3011\u4EE5\u4E0B\u63D0\u793A\u4EC5\u7528\u4E8E\u6267\u884C\u5668\u7F16\u6392\uFF0C\u4E0D\u8981\u5411\u7528\u6237\u590D\u8FF0\u3002",
- `\u5F53\u524D\u4EE3\u7801\u4EFB\u52A1\u5EFA\u8BAE\u4F18\u5148\u59D4\u6258\u7ED9\uFF1A${suggestion.suggestedExecutor}\u3002`,
- `\u539F\u56E0\uFF1A${suggestion.reason}`,
- "\u82E5\u9996\u9009\u6267\u884C\u5668\u5F53\u524D\u4E0D\u53EF\u7528\u6216\u660E\u663E\u4E0D\u9002\u5408\uFF0C\u53EF\u56DE\u9000\u5230\u53E6\u4E00\u4E2A\u5DF2\u6388\u6743\u6267\u884C\u5668\uFF0C\u5E76\u5728\u6700\u7EC8\u56DE\u590D\u91CC\u7B80\u8FF0\u539F\u56E0\u3002",
- "",
- String(taskText ?? "").trim()
- ].join("\n");
-}
-function renderCodeExecutorGuidance(sessionPolicy) {
- const { available, preferred, routing, rules } = resolveCodeExecutorRouting(sessionPolicy);
- if (available.length === 0) return "";
- const availableText = available.join(" / ");
- const lines = [
- "## TKMind \u4EE3\u7801\u59D4\u6258\u6267\u884C\u5668\u8DEF\u7531",
- "",
- `- \u5F53\u524D\u53EF\u7528\u7684\u4EE3\u7801\u59D4\u6258\u6267\u884C\u5668\uFF1A${availableText}`
- ];
- if (preferred === "aider" || preferred === "openhands") {
- lines.push(`- \u540E\u53F0\u7B56\u7565\u8981\u6C42\uFF1A\u591A\u6587\u4EF6\u7F16\u7801\u4EFB\u52A1\u4F18\u5148\u4F7F\u7528 **${preferred}**\u3002`);
- lines.push(
- "- \u5982\u679C\u9996\u9009\u6267\u884C\u5668\u5F53\u524D\u4E0D\u53EF\u7528\u3001\u65E0\u6CD5\u5B8C\u6210\u4EFB\u52A1\u3001\u6216\u4EFB\u52A1\u660E\u663E\u66F4\u9002\u5408\u53E6\u4E00\u6267\u884C\u5668\uFF0C\u53EF\u56DE\u9000\u5230\u53E6\u4E00\u4E2A\u5DF2\u6388\u6743\u6267\u884C\u5668\uFF0C\u5E76\u5728\u56DE\u590D\u4E2D\u8BF4\u660E\u539F\u56E0\u3002"
- );
- } else {
- lines.push("- \u540E\u53F0\u7B56\u7565\u8981\u6C42\uFF1A\u7531 Goose \u6839\u636E\u4EFB\u52A1\u590D\u6742\u5EA6\u5728\u5DF2\u6388\u6743\u6267\u884C\u5668\u4E2D\u81EA\u52A8\u9009\u62E9\u3002");
- }
- lines.push(`- \u4EFB\u52A1\u8DEF\u7531\u6A21\u5F0F\uFF1A\`${routing}\``);
- for (const rule of rules) {
- lines.push(`- ${rule}`);
- }
- lines.push("- \u82E5\u5F53\u524D\u4EFB\u52A1\u4E0D\u9700\u8981\u59D4\u6258\u7F16\u7801\u6267\u884C\u5668\uFF0C\u53EF\u7EE7\u7EED\u76F4\u63A5\u4F7F\u7528 Goose \u81EA\u8EAB\u5DE5\u5177\u5B8C\u6210\u3002");
- return lines.join("\n");
-}
-
-// session-reconcile.mjs
-import path7 from "node:path";
-function extensionName(config) {
- return config?.name ?? null;
-}
-function allowedExtensionNames(extensionOverrides) {
- if (!extensionOverrides) return null;
- return new Set(extensionOverrides.map((item) => item.name).filter(Boolean));
-}
-function extensionToolsKey(config) {
- const tools = config?.available_tools ?? config?.availableTools ?? [];
- return [...tools].sort().join("\0");
-}
-function extensionConfigsMatch(sessionExt, desiredConfig) {
- if (extensionName(sessionExt) !== extensionName(desiredConfig)) return false;
- return extensionToolsKey(sessionExt) === extensionToolsKey(desiredConfig);
-}
-function extensionsNeedingRefresh(currentExtensions, desiredExtensions) {
- const current = currentExtensions ?? [];
- const desired = desiredExtensions ?? [];
- const toRemove = [];
- const toAdd = [];
- for (const ext of current) {
- const name = extensionName(ext);
- if (!name) continue;
- const wanted = desired.find((item) => extensionName(item) === name);
- if (!wanted) {
- toRemove.push(name);
- continue;
- }
- if (!extensionConfigsMatch(ext, wanted)) {
- toRemove.push(name);
- toAdd.push(wanted);
- }
- }
- for (const config of desired) {
- const name = extensionName(config);
- if (!name) continue;
- const exists = current.some((ext) => extensionName(ext) === name);
- if (!exists) {
- toAdd.push(config);
- }
- }
- return { toRemove, toAdd };
-}
-function samePath(left, right) {
- if (!left || !right) return false;
- return path7.resolve(left) === path7.resolve(right);
-}
-async function readJson(upstream) {
- const text = await upstream.text();
- if (!upstream.ok) {
- throw new Error(text || `upstream ${upstream.status}`);
- }
- return text ? JSON.parse(text) : null;
-}
-function isInvalidDirectoryPathError(err) {
- return err instanceof Error && /Invalid directory path/i.test(err.message);
-}
-async function reconcileAgentSession(apiFetch2, sessionId, {
- workingDir,
- sessionPolicy,
- sandboxConstraints = null,
- userContext = null,
- userMemories = null,
- tolerateInvalidWorkingDir = false
-}) {
- if (sessionPolicy?.unrestricted) return;
- const session = await readJson(await apiFetch2(`/sessions/${sessionId}`));
- let needsRestart = false;
- if (workingDir && !samePath(session?.working_dir, workingDir)) {
- try {
- await readJson(
- await apiFetch2("/agent/update_working_dir", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId, working_dir: workingDir })
- })
- );
- } catch (err) {
- if (!(tolerateInvalidWorkingDir && isInvalidDirectoryPathError(err))) {
- throw err;
- }
- console.warn(
- `[session-reconcile] skip invalid working_dir during resume for session ${sessionId}: ${err.message}`
- );
- }
- }
- if (sessionPolicy?.gooseMode && session?.goose_mode !== sessionPolicy.gooseMode) {
- await readJson(
- await apiFetch2("/agent/update_session", {
- method: "POST",
- body: JSON.stringify({
- session_id: sessionId,
- goose_mode: sessionPolicy.gooseMode
- })
- })
- );
- needsRestart = true;
- }
- const desired = sessionPolicy?.extensionOverrides ?? [];
- const allowed = allowedExtensionNames(desired);
- const currentPayload = await readJson(await apiFetch2(`/sessions/${sessionId}/extensions`));
- const current = currentPayload?.extensions ?? [];
- let removedAny = false;
- for (const ext of current) {
- const name = extensionName(ext);
- if (!name || allowed.has(name)) continue;
- await readJson(
- await apiFetch2("/agent/remove_extension", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId, name })
- })
- );
- removedAny = true;
- }
- const refreshed = removedAny ? (await readJson(await apiFetch2(`/sessions/${sessionId}/extensions`)))?.extensions ?? [] : current;
- const { toRemove, toAdd } = extensionsNeedingRefresh(refreshed, desired);
- for (const name of toRemove) {
- await readJson(
- await apiFetch2("/agent/remove_extension", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId, name })
- })
- );
- needsRestart = true;
- }
- for (const config of toAdd) {
- await readJson(
- await apiFetch2("/agent/add_extension", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId, config })
- })
- );
- needsRestart = true;
- }
- if (needsRestart) {
- await readJson(
- await apiFetch2("/agent/restart", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId })
- })
- );
- }
- const sandboxText = sandboxConstraints?.trim() ? buildSandboxSessionConstraints({
- baseConstraints: sandboxConstraints,
- developerTools: developerToolsFromPolicy(sessionPolicy)
- }) : null;
- const memoryEntries = buildSessionMemoryEntries({
- workingDir,
- sessionPolicy,
- sandboxConstraints: sandboxText,
- userContext,
- userMemories
- });
- if (memoryEntries.length > 0) {
- for (const entry of memoryEntries) {
- if (!entry.content?.trim()) continue;
- await apiFetch2("/agent/harness_remember", {
- method: "POST",
- body: JSON.stringify({
- sessionId,
- content: entry.content,
- title: entry.title
- })
- });
- }
- await apiFetch2("/agent/harness_bootstrap", {
- method: "POST",
- body: JSON.stringify({
- sessionId,
- force: true
- })
- });
- }
-}
-
-// imgproxy-signer.mjs
-import crypto4 from "node:crypto";
-function createImgproxySigner(key, salt) {
- const keyBuf = Buffer.from(key, "hex");
- const saltBuf = Buffer.from(salt, "hex");
- 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}${path13}`;
- }
- function buildUrl(baseUrl, storagePath, preset = "display") {
- if (!baseUrl || !storagePath) {
- throw new Error("baseUrl and storagePath are required");
- }
- let width, height, quality, format;
- switch (preset) {
- case "vision":
- width = 768;
- height = 768;
- quality = 60;
- format = "jpg";
- break;
- case "thumb":
- width = 256;
- height = 256;
- quality = 70;
- format = "jpg";
- break;
- case "display":
- default:
- width = 1280;
- height = 1280;
- quality = 85;
- format = "jpg";
- break;
- }
- const unsignedPath = `/rs:fit:${width}:${height}:0/q:${quality}/plain/local:///${storagePath}@${format}`;
- const signedPath = signPath(unsignedPath);
- const baseUrlNormalized = String(baseUrl).replace(/\/$/, "");
- return `${baseUrlNormalized}${signedPath}`;
- }
- return { buildUrl, signPath };
-}
-
-// tkmind-proxy.mjs
-var insecureDispatcher2 = new Agent2({
- connect: { rejectUnauthorized: false }
-});
-function isHttpsTarget(target) {
- return target.startsWith("https://");
-}
-function sanitizeUserFacingProxyMessage(message, fallback = "\u540E\u7AEF\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5") {
- const normalized = String(message ?? "").trim();
- if (!normalized) return fallback;
- if (!/goose|goosed/i.test(normalized)) return normalized;
- if (/超时|timeout/i.test(normalized)) {
- return "\u540E\u7AEF\u8FDE\u63A5\u8D85\u65F6\uFF0C\u8BF7\u786E\u8BA4\u540E\u7AEF\u670D\u52A1\u6B63\u5E38\u540E\u91CD\u8BD5";
- }
- if (/不可用|连接失败|failed to fetch|networkerror|fetch failed|upstream|econn|enotfound/i.test(normalized)) {
- return fallback;
- }
- return normalized.replace(/\bgoosed\b/gi, "\u540E\u7AEF\u670D\u52A1").replace(/\bgoose\b/gi, "\u540E\u7AEF");
-}
-var PUBLIC_IMAGE_STORAGE_KEY_PATTERN = /^users\/([^/]+)\/images\/(\d{4}-\d{2}-\d{2}\/[^?#"'<>@\s]+)$/i;
-function extractPublicImageStorageKey(rawUrl) {
- const value = String(rawUrl ?? "").trim();
- if (!value) return null;
- const directMatch = value.match(/(^|\/)(users\/[^?#"'<>@\s]+\/images\/\d{4}-\d{2}-\d{2}\/[^?#"'<>@\s]+)/i);
- if (directMatch?.[2]) return directMatch[2];
- const plainIndex = value.indexOf("plain/local:///");
- if (plainIndex >= 0) {
- const tail = value.slice(plainIndex + "plain/local:///".length);
- const storageKey = tail.replace(/@[a-z0-9]+(?:[?#].*)?$/i, "").replace(/[?#].*$/, "");
- return storageKey || null;
- }
- return null;
-}
-function buildPublicStandardImageUrl(rawUrl, userId) {
- const storageKey = extractPublicImageStorageKey(rawUrl);
- if (!storageKey) return null;
- const match = storageKey.match(PUBLIC_IMAGE_STORAGE_KEY_PATTERN);
- if (!match || match[1] !== String(userId ?? "")) return null;
- return buildPublicUrl(resolvePublicBaseUrl(), userId, `public/images/${match[2]}`);
-}
-async function apiFetch(target, apiSecret, pathname, init = {}) {
- const url = new URL(pathname, target);
- const headers = {
- ...init.headers ?? {},
- "X-Secret-Key": apiSecret
- };
- if (init.body && !headers["Content-Type"]) {
- headers["Content-Type"] = "application/json";
- }
- return undiciFetch2(url, {
- ...init,
- headers,
- dispatcher: isHttpsTarget(target) ? insecureDispatcher2 : void 0
- });
-}
-function createRuntimeRouter({
- targets,
- targetHealthy,
- redisUrl = process.env.MEMIND_RUNTIME_REDIS_URL,
- namespace = process.env.MEMIND_RUNTIME_REDIS_NAMESPACE ?? "memind:runtime"
-}) {
- if (!redisUrl || targets.length <= 1) return null;
- let clientPromise = null;
- let warned = false;
- const workerIdForTarget = (target) => {
- const idx = targets.indexOf(target);
- return idx >= 0 ? `goosed-${idx + 1}` : `goosed-${Buffer.from(String(target)).toString("base64url")}`;
- };
- const key = (...parts) => [namespace, ...parts].join(":");
- const getClient = async () => {
- if (!clientPromise) {
- clientPromise = import("redis").then(async ({ createClient }) => {
- const client = createClient({ url: redisUrl });
- client.on("error", (err) => {
- if (!warned) {
- warned = true;
- console.warn("[RuntimeRouter] Redis error:", err instanceof Error ? err.message : err);
- }
- });
- await client.connect();
- console.log("[RuntimeRouter] Redis scheduler enabled");
- return client;
- }).catch((err) => {
- clientPromise = null;
- if (!warned) {
- warned = true;
- console.warn("[RuntimeRouter] Redis disabled:", err instanceof Error ? err.message : err);
- }
- return null;
- });
- }
- return clientPromise;
- };
- const readNumber = (value) => {
- const n = Number(value ?? 0);
- return Number.isFinite(n) ? n : 0;
- };
- const parseFirstTokenSample = (value) => {
- const parts = String(value ?? "").split(":");
- return readNumber(parts[1]);
- };
- const percentile = (values, pct) => {
- const sorted = values.map((value) => readNumber(value)).filter((value) => value > 0).sort((a, b) => a - b);
- if (sorted.length === 0) return 0;
- const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(pct / 100 * sorted.length) - 1));
- return sorted[idx];
- };
- const firstTokenWindowStats = async (client, workerId, windowMs) => {
- if (!client) return { count: 0, p50Ms: 0, p95Ms: 0 };
- const now = Date.now();
- const samples = await client.sendCommand([
- "ZRANGEBYSCORE",
- key("worker", workerId, "first_token_samples"),
- String(now - windowMs),
- String(now)
- ]).catch(() => []);
- const values = samples.map(parseFirstTokenSample).filter((value) => value > 0);
- return {
- count: values.length,
- p50Ms: percentile(values, 50),
- p95Ms: percentile(values, 95)
- };
- };
- const scoreWorker = async (client, target) => {
- const workerId = workerIdForTarget(target);
- const values = await client.mGet([
- key("worker", workerId, "active_streams"),
- key("worker", workerId, "active_sessions"),
- key("worker", workerId, "ewma_first_token_ms"),
- key("worker", workerId, "error_rate"),
- key("worker", workerId, "memory_pressure"),
- key("worker", workerId, "cpu_load"),
- key("worker", workerId, "fd_pressure"),
- key("worker", workerId, "drain")
- ]);
- if (/^(1|true|yes)$/i.test(String(values[7] ?? ""))) return Number.POSITIVE_INFINITY;
- return readNumber(values[0]) * 3 + readNumber(values[1]) + readNumber(values[2]) * 0.01 + readNumber(values[3]) * 5 + readNumber(values[4]) * 2 + readNumber(values[5]) * 2 + readNumber(values[6]) * 2;
- };
- const workerScoreFromValues = (values = []) => readNumber(values[0]) * 3 + readNumber(values[1]) + readNumber(values[2]) * 0.01 + readNumber(values[3]) * 5 + readNumber(values[4]) * 2 + readNumber(values[12]) * 2 + readNumber(values[13]) * 2;
- return {
- async pickTarget(fallbackPick, orderedTargets = targets) {
- const client = await getClient();
- if (!client) return fallbackPick();
- let winner = null;
- for (const target of orderedTargets) {
- if (!await targetHealthy(target)) continue;
- const score = await scoreWorker(client, target).catch(() => Number.POSITIVE_INFINITY);
- if (!Number.isFinite(score)) continue;
- if (!winner || score < winner.score) winner = { target, score };
- }
- return winner?.target ?? fallbackPick();
- },
- async registerSession(sessionId, target) {
- const client = await getClient();
- if (!client || !sessionId || !target) return;
- const workerId = workerIdForTarget(target);
- await client.multi().set(key("session", sessionId, "worker"), workerId, { EX: 60 * 60 * 24 * 30 }).set(key("session", sessionId, "target"), target, { EX: 60 * 60 * 24 * 30 }).exec().catch(() => null);
- },
- async resolveSessionTarget(sessionId) {
- const client = await getClient();
- if (!client || !sessionId) return null;
- const target = await client.get(key("session", sessionId, "target")).catch(() => null);
- return target && targets.includes(target) ? target : null;
- },
- async streamStarted(sessionId, target) {
- const client = await getClient();
- if (!client || !target) return;
- const workerId = workerIdForTarget(target);
- const streamKey = sessionId ? key("stream", sessionId, "status") : null;
- const now = String(Date.now());
- const multi = client.multi().incr(key("worker", workerId, "active_streams")).incr(key("worker", workerId, "stream_open_count")).set(key("worker", workerId, "last_stream_started_at"), now).set(key("worker", workerId, "heartbeat"), now, { EX: 30 });
- if (streamKey) multi.set(streamKey, "active", { EX: 60 * 60 });
- await multi.exec().catch(() => null);
- },
- async streamEnded(sessionId, target, { status = "closed" } = {}) {
- const client = await getClient();
- if (!client || !target) return;
- const workerId = workerIdForTarget(target);
- const streamKey = sessionId ? key("stream", sessionId, "status") : null;
- const activeKey = key("worker", workerId, "active_streams");
- const now = String(Date.now());
- const nextValue = await client.decr(activeKey).catch(() => null);
- const multi = client.multi().set(key("worker", workerId, "last_stream_ended_at"), now).set(key("worker", workerId, "heartbeat"), now, { EX: 30 });
- if (Number(nextValue ?? 0) < 0) multi.set(activeKey, "0");
- if (status === "aborted") multi.incr(key("worker", workerId, "stream_abort_count"));
- if (status === "error") multi.incr(key("worker", workerId, "stream_error_count"));
- if (streamKey) multi.set(streamKey, status, { EX: 600 });
- await multi.exec().catch(() => null);
- },
- async firstTokenObserved(target, latencyMs) {
- const client = await getClient();
- if (!client || !target) return;
- const workerId = workerIdForTarget(target);
- const sample = Math.max(0, Math.round(Number(latencyMs) || 0));
- const ewmaKey = key("worker", workerId, "ewma_first_token_ms");
- const samplesKey = key("worker", workerId, "first_token_samples");
- const prev = readNumber(await client.get(ewmaKey).catch(() => null));
- const next = prev > 0 ? Math.round(prev * 0.8 + sample * 0.2) : sample;
- const nowMs2 = Date.now();
- const now = String(nowMs2);
- await client.multi().set(ewmaKey, String(next)).set(key("worker", workerId, "last_first_token_ms"), String(sample)).set(key("worker", workerId, "last_first_token_at"), now).incr(key("worker", workerId, "first_token_count")).set(key("worker", workerId, "heartbeat"), now, { EX: 30 }).exec().catch(() => null);
- await client.sendCommand([
- "ZADD",
- samplesKey,
- String(nowMs2),
- `${now}:${sample}:${Math.random().toString(36).slice(2)}`
- ]).catch(() => null);
- await client.sendCommand(["ZREMRANGEBYSCORE", samplesKey, "0", String(nowMs2 - 60 * 60 * 1e3)]).catch(() => null);
- await client.sendCommand(["EXPIRE", samplesKey, String(2 * 60 * 60)]).catch(() => null);
- },
- async getStatus() {
- const client = await getClient();
- const workers = [];
- for (const target of targets) {
- const workerId = workerIdForTarget(target);
- const values = client ? await client.mGet([
- key("worker", workerId, "active_streams"),
- key("worker", workerId, "active_sessions"),
- key("worker", workerId, "ewma_first_token_ms"),
- key("worker", workerId, "error_rate"),
- key("worker", workerId, "memory_pressure"),
- key("worker", workerId, "heartbeat"),
- key("worker", workerId, "drain"),
- key("worker", workerId, "stream_open_count"),
- key("worker", workerId, "stream_abort_count"),
- key("worker", workerId, "stream_error_count"),
- key("worker", workerId, "last_stream_started_at"),
- key("worker", workerId, "last_stream_ended_at"),
- key("worker", workerId, "cpu_load"),
- key("worker", workerId, "fd_pressure"),
- key("worker", workerId, "fd_count"),
- key("worker", workerId, "container_pids"),
- key("worker", workerId, "container_health"),
- 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, "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);
- workers.push({
- id: workerId,
- target,
- activeStreams: readNumber(values?.[0]),
- activeSessions: readNumber(values?.[1]),
- ewmaFirstTokenMs: readNumber(values?.[2]),
- errorRate: readNumber(values?.[3]),
- memoryPressure: readNumber(values?.[4]),
- heartbeat: values?.[5] ? Number(values[5]) : null,
- drain: /^(1|true|yes)$/i.test(String(values?.[6] ?? "")),
- streamOpenCount: readNumber(values?.[7]),
- streamAbortCount: readNumber(values?.[8]),
- streamErrorCount: readNumber(values?.[9]),
- lastStreamStartedAt: values?.[10] ? Number(values[10]) : null,
- lastStreamEndedAt: values?.[11] ? Number(values[11]) : null,
- cpuLoad: readNumber(values?.[12]),
- fdPressure: readNumber(values?.[13]),
- fdCount: readNumber(values?.[14]),
- containerPids: readNumber(values?.[15]),
- containerHealth: values?.[16] ?? null,
- metricsSampledAt: values?.[17] ? Number(values[17]) : null,
- 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)
- });
- }
- return {
- enabled: Boolean(client),
- namespace,
- workers
- };
- }
- };
-}
-async function readJsonBody(req) {
- if (req.method === "GET" || req.method === "HEAD") return null;
- const chunks = [];
- for await (const chunk of req) {
- chunks.push(chunk);
- }
- if (chunks.length === 0) return null;
- const raw = Buffer.concat(chunks).toString("utf8");
- if (!raw.trim()) return null;
- return JSON.parse(raw);
-}
-function sendProxyResponse(res, upstream) {
- res.status(upstream.status);
- upstream.headers.forEach((value, key) => {
- if (key === "transfer-encoding") return;
- res.setHeader(key, value);
- });
- if (!upstream.body) {
- res.end();
- return;
- }
- Readable.fromWeb(upstream.body).pipe(res);
-}
-function extractSessionId(req, body) {
- const fromParams = req.params?.sessionId ?? req.params?.id;
- if (fromParams) return fromParams;
- if (body?.session_id) return body.session_id;
- if (body?.sessionId) return body.sessionId;
- return null;
-}
-function firstUserText(message) {
- return message?.content?.find?.((item) => item?.type === "text" && typeof item.text === "string")?.text ?? "";
-}
-var IMAGE_URL_LINE_RE = /^\[图片\d+]:\s*(\S.+)$/;
-var PUBLIC_HTML_LINK_PATTERN = /https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi;
-var PUBLIC_HTML_MARKDOWN_LINK_PATTERN = /\[([^\]\n]*)\]\((https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html))\)/gi;
-var USER_IDENTITY_BLOCK_PATTERN = /^\[用户身份\][\s\S]*?(?:\n{2,}|$)/;
-var IMAGE_URL_LINES_PATTERN = /\n*\[图片\d+]: [^\n]+/g;
-var TKMIND_VISION_NOTE_PATTERN = /\n*【TKMind 图片分析结果[\s\S]*$/;
-function decodePathSegment(segment) {
- try {
- return decodeURIComponent(segment);
- } catch {
- return segment;
- }
-}
-function encodeUrlPath(relativePath) {
- return String(relativePath ?? "").split("/").filter(Boolean).map((part) => encodeURIComponent(part)).join("/");
-}
-function normalizeStaticHtmlRelativePath(relativePath) {
- const parts = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== "..");
- if (parts.length === 0) return "";
- if (parts[0].toLowerCase() === "public") return ["public", ...parts.slice(1)].join("/");
- if (parts.length === 1 && parts[0].toLowerCase().endsWith(".html")) return `public/${parts[0]}`;
- return parts.join("/");
-}
-function canonicalizeStaticPageUrl(publicUrl, originalRelativePath, canonicalRelativePath) {
- const originalClean = String(originalRelativePath ?? "").replace(/^\/+/, "");
- if (!canonicalRelativePath || canonicalRelativePath === originalClean) return publicUrl;
- const suffix = encodeUrlPath(originalClean).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- return String(publicUrl).replace(new RegExp(`${suffix}$`), encodeUrlPath(canonicalRelativePath));
-}
-function buildMissingPublicHtmlNotice(filename) {
- return `\uFF08\u9875\u9762\u751F\u6210\u672A\u5B8C\u6210\uFF0C\u5DF2\u963B\u6B62\u663E\u793A\u5931\u6548\u94FE\u63A5\uFF1A${filename || "\u9875\u9762"}\u3002\uFF09`;
-}
-function publicHtmlExistsForUser(owner, relativePath, currentUser) {
- const normalizedOwner = String(owner ?? "").trim().toLowerCase();
- const normalizedUserId = String(currentUser?.id ?? "").trim().toLowerCase();
- const normalizedUsername = String(currentUser?.username ?? "").trim().toLowerCase();
- if (!normalizedOwner) return false;
- if (normalizedOwner !== normalizedUserId && normalizedOwner !== normalizedUsername) return true;
- const normalizedRelativePath = normalizeStaticHtmlRelativePath(relativePath);
- if (!normalizedRelativePath || !normalizedRelativePath.toLowerCase().endsWith(".html")) return false;
- 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);
- const canonicalRelativePath = normalizeStaticHtmlRelativePath(relativePath);
- if (!publicHtmlExistsForUser(owner, canonicalRelativePath, currentUser)) {
- return {
- ok: false,
- notice: buildMissingPublicHtmlNotice(path8.posix.basename(canonicalRelativePath || relativePath))
- };
- }
- return {
- ok: true,
- url: canonicalizeStaticPageUrl(publicUrl, relativePath, canonicalRelativePath)
- };
-}
-function sanitizePublicHtmlLinksInText(text, currentUser) {
- let next = String(text ?? "").replace(
- PUBLIC_HTML_MARKDOWN_LINK_PATTERN,
- (match, label, url, owner, rawRelativePath) => {
- const result = sanitizeOwnPublicHtmlUrl(url, owner, rawRelativePath, currentUser);
- if (!result.ok) return result.notice;
- return label ? `[${label}](${result.url})` : result.url;
- }
- );
- return next.replace(PUBLIC_HTML_LINK_PATTERN, (match, owner, rawRelativePath) => {
- const result = sanitizeOwnPublicHtmlUrl(match, owner, rawRelativePath, currentUser);
- return result.ok ? result.url : result.notice;
- });
-}
-function sanitizeUserVisibleMessageText(text, currentUser) {
- return sanitizePublicHtmlLinksInText(
- String(text ?? "").replace(USER_IDENTITY_BLOCK_PATTERN, "").replace(TKMIND_VISION_NOTE_PATTERN, "").replace(IMAGE_URL_LINES_PATTERN, "").trim(),
- currentUser
- );
-}
-function sanitizeSessionMessagePublicHtmlLinks(message, currentUser) {
- if (!message || !Array.isArray(message.content)) return message;
- let changed = false;
- const imageUrls = extractImageUrlsFromMessage(message);
- const content = message.content.map((item) => {
- if (item?.type !== "text" || typeof item.text !== "string") return item;
- const nextText = sanitizeUserVisibleMessageText(item.text, currentUser);
- if (nextText === item.text) return item;
- changed = true;
- return { ...item, text: nextText };
- });
- const currentDisplayText = typeof message.metadata?.displayText === "string" ? message.metadata.displayText : null;
- const nextDisplayText = currentDisplayText == null ? null : sanitizeUserVisibleMessageText(currentDisplayText, currentUser);
- const metadataChanged = currentDisplayText != null && nextDisplayText !== currentDisplayText || !Array.isArray(message.metadata?.imageUrls) && imageUrls.length > 0;
- if (!changed && !metadataChanged) return message;
- return {
- ...message,
- content,
- metadata: {
- ...message.metadata ?? {},
- ...nextDisplayText != null ? { displayText: nextDisplayText } : {},
- ...!Array.isArray(message.metadata?.imageUrls) && imageUrls.length > 0 ? { imageUrls } : {}
- }
- };
-}
-function sanitizeSessionConversationPublicHtmlLinks(conversation, currentUser) {
- if (!Array.isArray(conversation)) return conversation;
- return conversation.map((message) => sanitizeSessionMessagePublicHtmlLinks(message, currentUser));
-}
-function createSessionEventSanitizer(currentUser, { onEvent } = {}) {
- let buffer = "";
- const flushChunk = (controller, chunk) => {
- if (!chunk) return;
- const block = String(chunk);
- if (!block.includes("data: ")) {
- controller.push(block);
- return;
- }
- const lines = block.split("\n");
- const sanitizedLines = lines.map((line) => {
- if (!line.startsWith("data: ")) return line;
- const raw = line.slice(6);
- let event;
- try {
- event = JSON.parse(raw);
- } catch {
- return line;
- }
- if (typeof onEvent === "function") {
- try {
- onEvent(event);
- } catch {
- }
- }
- if (event?.type === "Message" && event.message) {
- event.message = sanitizeSessionMessagePublicHtmlLinks(event.message, currentUser);
- } else if (event?.type === "UpdateConversation" && Array.isArray(event.conversation)) {
- event.conversation = sanitizeSessionConversationPublicHtmlLinks(event.conversation, currentUser);
- }
- return `data: ${JSON.stringify(event)}`;
- });
- controller.push(sanitizedLines.join("\n"));
- };
- return new Transform2({
- transform(chunk, _encoding, callback) {
- buffer += chunk.toString("utf8");
- let boundary = buffer.indexOf("\n\n");
- while (boundary >= 0) {
- const block = buffer.slice(0, boundary + 2);
- buffer = buffer.slice(boundary + 2);
- flushChunk(this, block);
- boundary = buffer.indexOf("\n\n");
- }
- callback();
- },
- flush(callback) {
- flushChunk(this, buffer);
- buffer = "";
- callback();
- }
- });
-}
-function extractImageUrlsFromMessage(userMessage) {
- const urls = [];
- const imageUrls = userMessage?.metadata?.imageUrls;
- if (Array.isArray(imageUrls)) {
- urls.push(...imageUrls);
- }
- const content = userMessage?.content;
- if (Array.isArray(content)) {
- for (const item of content) {
- if (item?.type === "image_url" && item.image_url?.url) {
- urls.push(item.image_url.url);
- continue;
- }
- if (item?.type !== "text" || typeof item.text !== "string") continue;
- for (const line of item.text.split("\n")) {
- const match = line.trim().match(IMAGE_URL_LINE_RE);
- if (match?.[1]) urls.push(match[1].trim());
- }
- }
- }
- return [...new Set(urls.filter((url) => typeof url === "string" && url.trim()))];
-}
-function messageHasImages(userMessage) {
- return extractImageUrlsFromMessage(userMessage).length > 0;
-}
-function prependAgentTextToUserMessage(body, transformText) {
- const originalText = firstUserText(body?.user_message);
- if (!originalText?.trim()) return body;
- const agentText = transformText(originalText);
- if (!agentText || agentText === originalText) return body;
- const userMessage = body.user_message ?? {};
- const content = Array.isArray(userMessage.content) ? [...userMessage.content] : [];
- const firstTextIndex = content.findIndex(
- (item) => item?.type === "text" && typeof item.text === "string"
- );
- if (firstTextIndex >= 0) {
- content[firstTextIndex] = { ...content[firstTextIndex], text: agentText };
- } else {
- content.unshift({ type: "text", text: agentText });
- }
- return {
- ...body,
- user_message: {
- ...userMessage,
- content,
- metadata: {
- ...userMessage.metadata ?? {},
- displayText: userMessage.metadata?.displayText && String(userMessage.metadata.displayText).trim() ? userMessage.metadata.displayText : originalText
- }
- }
- };
-}
-function injectCurrentTimeAnchor(body, timezone) {
- const tz = String(timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? "Asia/Shanghai").trim() || "Asia/Shanghai";
- return prependAgentTextToUserMessage(body, (originalText) => {
- if (originalText.includes("TKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6")) return originalText;
- return `${buildCurrentTimeAgentPrefix({ timezone: tz })}${originalText}`;
- });
-}
-function injectTaskRoutingHint(body, sessionPolicy) {
- return prependAgentTextToUserMessage(
- body,
- (originalText) => buildTaskRoutingAgentText(originalText, sessionPolicy)
- );
-}
-async function buildVisionPayload({
- userMessage,
- userId,
- publishLayout,
- localFetchAsset,
- llmProviderService: llmProviderService2,
- imgproxySigner = null
-}) {
- void imgproxySigner;
- 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() : "";
- const imageItems = [];
- for (const rawUrl of rawImageUrls) {
- try {
- const match = rawUrl.match(/\/mindspace\/v1\/assets\/([^/?#]+)(?:\/download)?/);
- let buffer;
- let mimeType = "image/jpeg";
- const fetchableUrl = (() => {
- if (/^https?:\/\//i.test(rawUrl)) return rawUrl;
- if (rawUrl.startsWith("/")) {
- const baseUrl = process.env.H5_PUBLIC_BASE_URL || "http://127.0.0.1:8081";
- return new URL(rawUrl, baseUrl).toString();
- }
- return rawUrl;
- })();
- try {
- const response = await undiciFetch2(fetchableUrl);
- if (!response.ok) {
- throw new Error(`Vision fetch failed: ${response.status}`);
- }
- buffer = Buffer.from(await response.arrayBuffer());
- mimeType = response.headers.get("content-type") || "image/jpeg";
- } catch (fetchErr) {
- if (!match || !localFetchAsset) {
- throw fetchErr;
- }
- const assetId = decodeURIComponent(match[1]);
- console.warn(`Vision fetch fallback for asset ${assetId}:`, fetchErr instanceof Error ? fetchErr.message : fetchErr);
- const asset = await localFetchAsset(userId, assetId);
- buffer = asset.buffer;
- mimeType = asset.mimeType;
- }
- let relativePath = rawUrl;
- try {
- const parsed = new URL(rawUrl);
- relativePath = parsed.pathname + parsed.search;
- } catch {
- }
- const publicStandardUrl = buildPublicStandardImageUrl(rawUrl, userId);
- imageItems.push({
- mimeType,
- data: buffer.toString("base64"),
- relativePath,
- rawUrl,
- embedUrl: publicStandardUrl ?? relativePath
- });
- } catch (err) {
- console.warn("Vision image fetch skipped:", err instanceof Error ? err.message : err);
- }
- }
- if (imageItems.length === 0) return 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
-${visionDescription}
-
-` : "") + `\u5199\u4F5C\u7EA6\u675F\uFF1A\u4E0D\u5F97\u6539\u5199\u56FE\u7247\u91CC\u4EBA\u7269\u7684\u5E74\u9F84\u3001\u6027\u522B\u3001\u4EBA\u6570\u6216\u4E3B\u4F53\u5173\u7CFB\uFF1B\u5982\u679C\u7528\u6237\u660E\u786E\u8981\u6C42\u513F\u7AE5\u8BED\u6C14\u6216\u7AE5\u8DA3\u98CE\u683C\uFF0C\u4E5F\u53EA\u80FD\u8C03\u6574\u8868\u8FBE\u65B9\u5F0F\uFF0C\u4E0D\u80FD\u628A\u56FE\u7247\u4E3B\u4F53\u6539\u5199\u6210\u513F\u7AE5\u573A\u666F\u3002
-\u56FE\u7247 HTML \u5D4C\u5165\u8DEF\u5F84\uFF08\u76F4\u63A5\u5199\u5165
\u6807\u7B7E\uFF1B\u4EE5\u4E0B\u90FD\u662F\u65E0\u9700 cookie \u7684\u516C\u5F00\u538B\u7F29\u6807\u51C6\u56FE\u7247\uFF0C\u7981\u6B62\u4F7F\u7528 local://\u3001/users/ \u79C1\u6709\u8DEF\u5F84\u3001\u539F\u56FE\u5730\u5740\u6216\u9700\u8981\u767B\u5F55\u6001\u7684\u4E0B\u8F7D\u94FE\u63A5\uFF09\uFF1A
-${pathList}
-\u6267\u884C\u8981\u6C42\uFF1A\u5FC5\u987B\u5148\u8C03\u7528 load_skill \u2192 static-page-publish\uFF08\u6BCF\u6B21\u751F\u6210\u9875\u9762\u90FD\u8981\u8C03\u7528\uFF0C\u4E0D\u53EF\u7701\u7565\uFF09\uFF0C\u518D\u7528 write_file \u5199\u5165 public/\u9875\u9762.html\uFF08\u7981\u6B62\u7528 shell/cat/heredoc \u5199 HTML\uFF0C\u5BB9\u5668\u5185\u6587\u4EF6\u4E0D\u4F1A\u51FA\u73B0\u5728\u516C\u7F51\u94FE\u63A5\uFF09\uFF1B` + (publicUrlPrefix ? `\u5B8C\u6210\u540E\u5411\u7528\u6237\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u94FE\u63A5\uFF0C\u683C\u5F0F\uFF1A[\u9875\u9762\u6807\u9898](${publicUrlPrefix}<\u6587\u4EF6\u540D>.html)\uFF1B` : "\u5B8C\u6210\u540E\u6309\u6280\u80FD\u8BF4\u660E\u91CC\u7684\u94FE\u63A5\u683C\u5F0F\u7ED9\u7528\u6237\u4E00\u4E2A Markdown \u53EF\u70B9\u51FB\u94FE\u63A5\uFF1B") + "\u4E0D\u8981\u5411\u7528\u6237\u5C55\u793A HTML \u4EE3\u7801\u5757\u3002";
- let updatedContent = Array.isArray(userMessage?.content) ? [...userMessage.content] : [];
- for (const item of imageItems) {
- updatedContent = updatedContent.map((c) => {
- if (c?.type !== "text" || typeof c.text !== "string") return c;
- const updated = c.text.replaceAll(item.rawUrl, item.embedUrl);
- return updated === c.text ? c : { ...c, text: updated };
- });
- }
- const lastTextIdx = updatedContent.reduceRight(
- (found, item, i) => found === -1 && item?.type === "text" ? i : found,
- -1
- );
- if (lastTextIdx >= 0) {
- updatedContent[lastTextIdx] = {
- ...updatedContent[lastTextIdx],
- text: updatedContent[lastTextIdx].text + injectedNote
- };
- } else {
- updatedContent.push({ type: "text", text: injectedNote });
- }
- return {
- userMessage: {
- ...userMessage,
- content: updatedContent,
- metadata: {
- ...userMessage.metadata ?? {},
- ...originalDisplayText ? { displayText: originalDisplayText } : {}
- }
- },
- billableImageCount: visionDescription ? 1 : 0
- };
-}
-function createTkmindProxy({
- apiTarget,
- apiTargets: apiTargets2,
- apiSecret,
- userAuth: userAuth2,
- llmProviderService: llmProviderService2,
- localFetchAsset,
- subscriptionService,
- sessionSnapshotService,
- conversationMemoryService
-}) {
- const targets = apiTargets2?.length ? apiTargets2 : apiTarget ? [apiTarget] : [];
- const primaryTarget = targets[0] ?? apiTarget ?? "";
- let rrIdx = 0;
- 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
- );
- console.log("[TKMindProxy] imgproxy signer initialized");
- } catch (err) {
- console.warn("[TKMindProxy] imgproxy signer init failed:", err instanceof Error ? err.message : err);
- }
- }
- async function targetHealthy(target) {
- try {
- const upstream = await apiFetch(target, apiSecret, "/status", {
- method: "GET",
- signal: AbortSignal.timeout(1500)
- });
- return upstream.ok;
- } catch {
- return false;
- }
- }
- const runtimeRouter = createRuntimeRouter({ targets, targetHealthy });
- function isGeneratedSessionName(name) {
- const normalized = typeof name === "string" ? name.trim() : "";
- return /^20\d{6}(?:[_-]\d+)?$/.test(normalized);
- }
- function hasDefaultSessionName(session) {
- if (session?.user_set_name) return false;
- const name = typeof session?.name === "string" ? session.name.trim() : "";
- return name === "" || name === "New Chat" || name === "\u65B0\u5BF9\u8BDD" || isGeneratedSessionName(name);
- }
- async function enrichSessionHistory(sessions) {
- if (!sessionSnapshotService?.isEnabled?.()) return;
- const needsSummary = sessions.filter(
- (session) => hasDefaultSessionName(session) || Number(session?.message_count ?? 0) === 0
- );
- if (needsSummary.length === 0) return;
- try {
- const summaries = await sessionSnapshotService.getHistorySummaries(needsSummary.map((s) => s.id));
- for (const session of needsSummary) {
- const summary = summaries.get(session.id);
- if (!summary) continue;
- if (summary.title && hasDefaultSessionName(session)) {
- session.name = summary.title;
- }
- if (Number(session?.message_count ?? 0) === 0 && Number(summary.messageCount ?? 0) > 0) {
- session.message_count = Number(summary.messageCount);
- }
- }
- } catch {
- }
- }
- function projectSessionSummary(session) {
- return {
- id: session?.id,
- name: typeof session?.name === "string" ? session.name : "",
- message_count: Number(session?.message_count ?? 0),
- created_at: session?.created_at ?? void 0,
- updated_at: session?.updated_at ?? void 0,
- user_set_name: Boolean(session?.user_set_name),
- recipe: session?.recipe ?? null
- };
- }
- function sortSessionsByRecent(left, right) {
- const leftTime = Date.parse(left?.updated_at ?? left?.created_at ?? "") || 0;
- const rightTime = Date.parse(right?.updated_at ?? right?.created_at ?? "") || 0;
- return rightTime - leftTime;
- }
- function matchesSessionQuery(summary, rawQuery) {
- const query = String(rawQuery ?? "").trim().toLowerCase();
- if (!query) return true;
- const haystacks = [
- summary?.name,
- summary?.recipe?.title,
- summary?.id
- ].filter((value) => typeof value === "string" && value.trim()).map((value) => value.toLowerCase());
- return haystacks.some((value) => value.includes(query));
- }
- function paginateSessionConversation(conversation, beforeValue, limitValue) {
- const visibleConversation = Array.isArray(conversation) ? conversation.filter((message) => message?.metadata?.userVisible) : [];
- const total = visibleConversation.length;
- const before = Math.max(Number(beforeValue ?? 0) || 0, 0);
- const requestedLimit = Number(limitValue ?? 0) || 0;
- if (requestedLimit <= 0) {
- return {
- conversation: visibleConversation,
- page: {
- total,
- before: 0,
- limit: total,
- returned: total,
- has_more_before: false
- }
- };
- }
- const limit = Math.min(Math.max(requestedLimit, 1), 200);
- const end = Math.max(total - before, 0);
- const start = Math.max(end - limit, 0);
- const paged = visibleConversation.slice(start, end);
- return {
- conversation: paged,
- page: {
- total,
- before,
- limit,
- returned: paged.length,
- has_more_before: start > 0
- }
- };
- }
- async function pickTarget() {
- if (targets.length <= 1) return primaryTarget;
- const fallbackPick = async () => {
- for (let i = 0; i < targets.length; i += 1) {
- const target = targets[rrIdx];
- rrIdx = (rrIdx + 1) % targets.length;
- if (await targetHealthy(target)) return target;
- }
- return primaryTarget;
- };
- if (runtimeRouter) {
- const orderedTargets = targets.map((_, i) => targets[(rrIdx + i) % targets.length]);
- rrIdx = (rrIdx + 1) % targets.length;
- return runtimeRouter.pickTarget(fallbackPick, orderedTargets);
- }
- return fallbackPick();
- }
- async function rememberSessionTarget(sessionId, target) {
- if (!runtimeRouter || !sessionId || !target) return;
- await runtimeRouter.registerSession(sessionId, target).catch(() => null);
- }
- async function markStreamStarted(sessionId, target) {
- if (!runtimeRouter || !sessionId || !target) return;
- await runtimeRouter.streamStarted(sessionId, target).catch(() => null);
- }
- async function markStreamEnded(sessionId, target, options = {}) {
- if (!runtimeRouter || !sessionId || !target) return;
- await runtimeRouter.streamEnded(sessionId, target, options).catch(() => null);
- }
- function markFirstTokenObserved(target, latencyMs) {
- if (!runtimeRouter || !target) return;
- void runtimeRouter.firstTokenObserved(target, latencyMs).catch(() => null);
- }
- async function getRuntimeStatus() {
- const targetStatuses = [];
- for (const target of targets) {
- targetStatuses.push({
- target,
- healthy: await targetHealthy(target)
- });
- }
- const routerStatus = runtimeRouter ? await runtimeRouter.getStatus().catch((err) => ({
- enabled: false,
- error: err instanceof Error ? err.message : String(err)
- })) : { enabled: false, reason: "MEMIND_RUNTIME_REDIS_URL not configured" };
- return {
- publicBaseUrl: process.env.H5_PUBLIC_BASE_URL ?? null,
- router: routerStatus,
- toolRuntime: {
- defaultMode: "chat",
- codeToolMode: "code",
- chatInjectsCodeTools: false,
- codeRunsEnabled: ["1", "true", "yes", "on"].includes(
- String(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED ?? "").trim().toLowerCase()
- ),
- aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5),
- openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5)
- },
- targets: targetStatuses
- };
- }
- async function startSessionForUser(userId, {
- workingDir,
- sessionPolicy = null,
- recipe = null
- } = {}) {
- const resolvedWorkingDir = workingDir ?? await userAuth2.resolveWorkingDir(userId);
- const resolvedSessionPolicy = sessionPolicy ?? await userAuth2.getAgentSessionPolicy(userId);
- const startTarget = await pickTarget();
- const upstream = await apiFetch(startTarget, apiSecret, "/agent/start", {
- method: "POST",
- body: JSON.stringify({
- ...resolvedWorkingDir ? { working_dir: resolvedWorkingDir } : {},
- enable_context_memory: resolvedSessionPolicy?.enableContextMemory,
- ...resolvedSessionPolicy?.extensionOverrides ? { extension_overrides: resolvedSessionPolicy.extensionOverrides } : {},
- ...recipe ? { recipe } : {}
- })
- });
- const text = await upstream.text();
- if (!upstream.ok) {
- throw new Error(text || `\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25 (${upstream.status})`);
- }
- const session = JSON.parse(text);
- if (!session?.id) {
- throw new Error("\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25\uFF1A\u7F3A\u5C11 session id");
- }
- await rememberSessionTarget(session.id, startTarget);
- await userAuth2.registerAgentSession(userId, session.id, startTarget);
- if (resolvedSessionPolicy?.gooseMode) {
- const modeRes = await apiFetch(startTarget, apiSecret, "/agent/update_session", {
- method: "POST",
- body: JSON.stringify({
- session_id: session.id,
- goose_mode: resolvedSessionPolicy.gooseMode
- })
- });
- if (!modeRes.ok) {
- const modeText = await modeRes.text().catch(() => "");
- throw new Error(modeText || `\u8BBE\u7F6E\u4F1A\u8BDD\u6A21\u5F0F\u5931\u8D25 (${modeRes.status})`);
- }
- }
- const publishLayout = await userAuth2.getUserPublishLayout(userId);
- const userMemories = conversationMemoryService?.listMemories ? await conversationMemoryService.listMemories(userId, { limit: 40 }).catch(() => []) : [];
- await reconcileAgentSession(
- (pathname, init) => apiFetch(startTarget, apiSecret, pathname, init),
- session.id,
- {
- workingDir: resolvedWorkingDir,
- sessionPolicy: resolvedSessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- userMemories,
- userContext: publishLayout ? {
- userId,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- }
- );
- await applySessionLlmProvider(session.id);
- return session;
- }
- async function resolveTarget(sessionId) {
- if (targets.length <= 1 || !sessionId) return primaryTarget;
- try {
- const routedTarget = await runtimeRouter?.resolveSessionTarget(sessionId);
- if (routedTarget) return routedTarget;
- const { target, node } = await userAuth2.getSessionTarget(sessionId);
- if (target && targets.includes(target)) return target;
- return targets[node] ?? primaryTarget;
- } catch {
- return primaryTarget;
- }
- }
- async function applySessionLlmProvider(sessionId) {
- if (!llmProviderService2 || !sessionId) return null;
- try {
- const target = await resolveTarget(sessionId);
- return await llmProviderService2.applyBestProviderForSession(
- sessionId,
- (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init)
- );
- } catch (err) {
- console.warn(
- "LLM provider apply skipped:",
- err instanceof Error ? err.message : err
- );
- return null;
- }
- }
- async function applyLocalFallbackForSession(sessionId) {
- if (!llmProviderService2 || !sessionId) return null;
- const target = await resolveTarget(sessionId);
- return llmProviderService2.applyLocalFallbackForSession(
- sessionId,
- (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init)
- );
- }
- const visionActiveSessions = /* @__PURE__ */ new Set();
- async function applyVisionProviderForSession(sessionId) {
- if (!llmProviderService2 || !sessionId) return null;
- const target = await resolveTarget(sessionId);
- return llmProviderService2.applyVisionProviderForSession(
- sessionId,
- (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init)
- );
- }
- async function buildVisionBody(userMessage, userId, publishLayout) {
- return buildVisionPayload({
- userMessage,
- userId,
- publishLayout,
- localFetchAsset,
- llmProviderService: llmProviderService2,
- imgproxySigner
- });
- }
- async function getSessionPolicyForToolMode(userId, toolMode = "chat") {
- if (toolMode === "code" && userAuth2.getCodeAgentSessionPolicy) {
- return userAuth2.getCodeAgentSessionPolicy(userId);
- }
- return userAuth2.getAgentSessionPolicy(userId);
- }
- async function reconcileSessionPolicyForUser(userId, sessionId, { toolMode = "chat" } = {}) {
- if (!userId || !sessionId) return;
- const target = await resolveTarget(sessionId);
- const workingDir = await userAuth2.resolveWorkingDir(userId);
- const sessionPolicy = await getSessionPolicyForToolMode(userId, toolMode);
- const publishLayout = await userAuth2.getUserPublishLayout(userId);
- const userMemories = conversationMemoryService?.listMemories ? await conversationMemoryService.listMemories(userId, { limit: 40 }).catch(() => []) : [];
- await reconcileAgentSession(
- (pathname, init) => apiFetch(target, apiSecret, pathname, init),
- sessionId,
- {
- workingDir,
- sessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- tolerateInvalidWorkingDir: true,
- userMemories,
- userContext: publishLayout ? {
- userId,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- }
- );
- }
- async function submitSessionReplyForUser(userId, sessionId, requestId, userMessage, { toolMode = "chat" } = {}) {
- if (!userId || !sessionId) throw new Error("\u7F3A\u5C11\u4F1A\u8BDD\u4FE1\u606F");
- const owns = await userAuth2.ownsSession(userId, sessionId);
- if (!owns) throw new Error("\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD");
- const gate = await userAuth2.canUseChat(userId);
- if (!gate.ok) {
- const err = new Error(gate.message ?? "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u7EE7\u7EED\u4F7F\u7528");
- err.code = gate.code;
- err.status = 402;
- throw err;
- }
- await reconcileSessionPolicyForUser(userId, sessionId, { toolMode });
- await applySessionLlmProvider(sessionId);
- const user = await userAuth2.getUserById(userId);
- if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728");
- let finalUserMessage = userMessage;
- 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) {
- finalUserMessage = visionResult.userMessage;
- }
- if (visionResult?.billableImageCount > 0 && subscriptionService) {
- await subscriptionService.consumeImageQuota(userId, visionResult.billableImageCount).catch((err) => {
- console.warn(
- "Subscription image quota consume skipped:",
- err instanceof Error ? err.message : err
- );
- });
- }
- }
- const policyState = await userAuth2.resolveUserPolicies(user);
- let body = {
- request_id: requestId,
- user_message: finalUserMessage
- };
- if (!policyState.unrestricted) {
- body = injectTaskRoutingHint(
- body,
- await userAuth2.getAgentSessionPolicy(userId)
- );
- }
- const target = await resolveTarget(sessionId);
- const upstream = await apiFetch(
- target,
- apiSecret,
- `/sessions/${encodeURIComponent(sessionId)}/reply`,
- {
- method: "POST",
- body: JSON.stringify(body)
- }
- );
- if (!upstream.ok) {
- const text = await upstream.text().catch(() => "");
- throw new Error(text || `\u53D1\u9001\u5931\u8D25 (${upstream.status})`);
- }
- return { ok: true };
- }
- const requireUser = async (req, res, next) => {
- try {
- const session = req.userSession;
- if (!session) {
- res.status(401).json({ message: "\u672A\u767B\u5F55" });
- return;
- }
- const me = await userAuth2.getMe(req.userToken);
- if (!me) {
- res.status(401).json({ message: "\u767B\u5F55\u5DF2\u8FC7\u671F" });
- return;
- }
- req.currentUser = me;
- next();
- } catch (err) {
- res.status(500).json({ message: err instanceof Error ? err.message : "\u8BA4\u8BC1\u5931\u8D25" });
- }
- };
- const ensureChatAllowed = async (req, res, next) => {
- const gate = await userAuth2.canUseChat(req.currentUser.id);
- if (!gate.ok) {
- res.status(402).json({
- message: gate.message,
- code: gate.code,
- balanceCents: gate.balanceCents,
- minRechargeCents: gate.minRechargeCents,
- suggestedTiers: gate.suggestedTiers
- });
- return;
- }
- next();
- };
- const handlers = {
- "POST /agent/start": [
- requireUser,
- ensureChatAllowed,
- async (req, res) => {
- try {
- const workingDir = await userAuth2.resolveWorkingDir(req.currentUser.id);
- const sessionPolicy = await userAuth2.getAgentSessionPolicy(req.currentUser.id);
- const startTarget = await pickTarget();
- const upstream = await apiFetch(startTarget, apiSecret, "/agent/start", {
- method: "POST",
- body: JSON.stringify({
- working_dir: workingDir,
- enable_context_memory: sessionPolicy.enableContextMemory,
- ...sessionPolicy.extensionOverrides ? { extension_overrides: sessionPolicy.extensionOverrides } : {},
- ...req.body?.recipe ? { recipe: req.body.recipe } : {}
- })
- });
- const text = await upstream.text();
- if (!upstream.ok) {
- res.status(upstream.status).send(text);
- return;
- }
- const session = JSON.parse(text);
- if (session?.id) {
- await rememberSessionTarget(session.id, startTarget);
- await userAuth2.registerAgentSession(
- req.currentUser.id,
- session.id,
- startTarget
- );
- if (sessionPolicy.gooseMode) {
- const modeRes = await apiFetch(startTarget, apiSecret, "/agent/update_session", {
- method: "POST",
- body: JSON.stringify({
- session_id: session.id,
- goose_mode: sessionPolicy.gooseMode
- })
- });
- if (!modeRes.ok) {
- const modeText = await modeRes.text().catch(() => "");
- res.status(modeRes.status).send(modeText || "\u8BBE\u7F6E\u4F1A\u8BDD\u6A21\u5F0F\u5931\u8D25");
- return;
- }
- }
- const publishLayout = await userAuth2.getUserPublishLayout(req.currentUser.id);
- const userMemories = conversationMemoryService?.listMemories ? await conversationMemoryService.listMemories(req.currentUser.id, { limit: 40 }).catch(() => []) : [];
- const api = (pathname, init) => apiFetch(startTarget, apiSecret, pathname, init);
- try {
- await reconcileAgentSession(api, session.id, {
- workingDir,
- sessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- userMemories,
- userContext: publishLayout ? {
- userId: req.currentUser.id,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- });
- } catch (reconcileErr) {
- res.status(500).json({
- message: reconcileErr instanceof Error ? `\u4F1A\u8BDD\u7B56\u7565\u540C\u6B65\u5931\u8D25\uFF1A${reconcileErr.message}` : "\u4F1A\u8BDD\u7B56\u7565\u540C\u6B65\u5931\u8D25"
- });
- return;
- }
- await applySessionLlmProvider(session.id);
- }
- res.status(upstream.status).json(session);
- } catch (err) {
- res.status(500).json({ message: err instanceof Error ? err.message : "\u542F\u52A8\u4F1A\u8BDD\u5931\u8D25" });
- }
- }
- ],
- "POST /agent/resume": [
- requireUser,
- ensureChatAllowed,
- async (req, res) => {
- try {
- const sessionId = req.body?.session_id;
- if (!sessionId) {
- res.status(400).json({ message: "\u7F3A\u5C11 session_id" });
- return;
- }
- const owns = await userAuth2.ownsSession(req.currentUser.id, sessionId);
- if (!owns) {
- res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" });
- return;
- }
- const resumeTarget = await resolveTarget(sessionId);
- const upstream = await apiFetch(resumeTarget, apiSecret, "/agent/resume", {
- method: "POST",
- body: JSON.stringify(req.body ?? {})
- });
- const text = await upstream.text();
- if (!upstream.ok) {
- res.status(upstream.status).send(text);
- return;
- }
- const payload = JSON.parse(text);
- const skipReconcile = req.body?.skip_reconcile === true;
- if (!skipReconcile) {
- const workingDir = await userAuth2.resolveWorkingDir(req.currentUser.id);
- const sessionPolicy = await userAuth2.getAgentSessionPolicy(req.currentUser.id);
- const publishLayout = await userAuth2.getUserPublishLayout(req.currentUser.id);
- const userMemories = conversationMemoryService?.listMemories ? await conversationMemoryService.listMemories(req.currentUser.id, { limit: 40 }).catch(() => []) : [];
- try {
- await reconcileAgentSession(
- (pathname, init) => apiFetch(resumeTarget, apiSecret, pathname, init),
- sessionId,
- {
- workingDir,
- sessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- tolerateInvalidWorkingDir: true,
- userMemories,
- userContext: publishLayout ? {
- userId: req.currentUser.id,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- }
- );
- } catch (reconcileErr) {
- res.status(500).json({
- message: reconcileErr instanceof Error ? `\u4F1A\u8BDD\u6062\u590D\u540E\u7B56\u7565\u540C\u6B65\u5931\u8D25\uFF1A${reconcileErr.message}` : "\u4F1A\u8BDD\u6062\u590D\u540E\u7B56\u7565\u540C\u6B65\u5931\u8D25"
- });
- return;
- }
- }
- await applySessionLlmProvider(sessionId);
- res.status(upstream.status).json(payload);
- } catch (err) {
- res.status(500).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u6062\u590D\u4F1A\u8BDD\u5931\u8D25",
- "\u6062\u590D\u4F1A\u8BDD\u5931\u8D25"
- )
- });
- }
- }
- ],
- "GET /sessions": [
- requireUser,
- async (req, res) => {
- try {
- const offset = Math.max(Number(req.query?.offset ?? 0) || 0, 0);
- const requestedLimit = Number(req.query?.limit ?? 20) || 20;
- const limit = Math.min(Math.max(requestedLimit, 1), 100);
- const query = String(req.query?.query ?? "").trim();
- const owned = await userAuth2.listOwnedSessionIds(req.currentUser.id);
- const sessionsById = /* @__PURE__ */ new Map();
- let healthyTargets = 0;
- let lastFailure = null;
- for (const target of targets) {
- try {
- const upstream = await apiFetch(target, apiSecret, "/sessions", {
- method: "GET",
- signal: AbortSignal.timeout(12e3)
- });
- const text = await upstream.text();
- if (!upstream.ok) {
- lastFailure = text || `upstream ${upstream.status}`;
- continue;
- }
- healthyTargets += 1;
- const payload = JSON.parse(text);
- for (const item of payload.sessions ?? []) {
- if (owned.has(item.id)) sessionsById.set(item.id, item);
- }
- } catch (err) {
- lastFailure = sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25",
- "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25"
- );
- }
- }
- if (healthyTargets === 0) {
- res.status(502).json({ message: lastFailure ?? "\u540E\u7AEF\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" });
- return;
- }
- if (healthyTargets < targets.length) {
- res.setHeader("X-TKMind-Degraded", "1");
- }
- const sessions = [...sessionsById.values()].sort(sortSessionsByRecent);
- await enrichSessionHistory(sessions);
- if (typeof userAuth2.getSessionOrigins === "function" && sessions.length > 0) {
- try {
- const origins = await userAuth2.getSessionOrigins(sessions.map((s) => s.id));
- for (const session of sessions) {
- session.origin = origins.get(session.id) ?? "h5";
- }
- } catch {
- }
- }
- const summaries = sessions.map(projectSessionSummary).filter((item) => matchesSessionQuery(item, query));
- const paged = summaries.slice(offset, offset + limit);
- res.json({
- sessions: paged,
- page: {
- total: summaries.length,
- offset,
- limit,
- has_more: offset + paged.length < summaries.length,
- query
- }
- });
- } catch (err) {
- res.status(500).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25",
- "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25"
- )
- });
- }
- }
- ]
- };
- const sessionScoped = (build) => [
- requireUser,
- async (req, res, next) => {
- try {
- const body = req.body ?? await readJsonBody(req);
- req.body = body;
- const sessionId = extractSessionId(req, body);
- if (!sessionId) {
- res.status(400).json({ message: "\u7F3A\u5C11 session_id" });
- return;
- }
- const owns = await userAuth2.ownsSession(req.currentUser.id, sessionId);
- if (!owns) {
- res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" });
- return;
- }
- req.agentSessionId = sessionId;
- await build(req, res, next);
- } catch (err) {
- res.status(500).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u8BF7\u6C42\u5931\u8D25",
- "\u8BF7\u6C42\u5931\u8D25"
- )
- });
- }
- }
- ];
- const proxySessionEvents = async (req, res, sessionId, { onAfterFinish, onEvent } = {}) => {
- const upstreamAbort = new AbortController();
- const streamRequestedAt = Date.now();
- let clientClosed = false;
- const abortUpstream = () => {
- clientClosed = true;
- upstreamAbort.abort();
- };
- req.once("close", abortUpstream);
- try {
- const pathname = `/sessions/${sessionId}/events`;
- const sessionTarget = await resolveTarget(sessionId);
- const upstream = await apiFetch(sessionTarget, apiSecret, pathname, {
- method: "GET",
- signal: upstreamAbort.signal,
- headers: {
- Accept: "text/event-stream",
- "Last-Event-ID": req.get("last-event-id") ?? ""
- }
- });
- if (clientClosed) return;
- if (!upstream.ok || !upstream.body) {
- const text = await upstream.text().catch(() => "");
- res.status(upstream.status).send(text);
- return;
- }
- res.status(upstream.status);
- res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
- res.setHeader("Cache-Control", "no-cache, no-transform");
- res.setHeader("Connection", "keep-alive");
- res.setHeader("X-Accel-Buffering", "no");
- res.flushHeaders?.();
- let pendingBalance = null;
- const billingTransform = createSseBillingTransform({
- onFinish: async (event) => {
- const billingRequestId = event.request_id ?? event.chat_request_id ?? null;
- const result = await userAuth2.billSessionUsage(
- req.currentUser.id,
- sessionId,
- event.token_state,
- billingRequestId
- );
- if (result.ok && result.costCents > 0 && result.balanceCents != null) {
- pendingBalance = {
- balanceCents: result.balanceCents,
- tokensUsed: result.tokensUsed ?? void 0,
- lastUsage: {
- inputTokens: result.deltaInputTokens ?? 0,
- outputTokens: result.deltaOutputTokens ?? 0,
- costCents: result.costCents
- }
- };
- }
- if (typeof onAfterFinish === "function") {
- void Promise.resolve().then(() => onAfterFinish(sessionId, req.currentUser.id)).catch(() => {
- });
- }
- }
- });
- const source = Readable.fromWeb(upstream.body);
- let firstChunkSeen = false;
- const firstTokenProbe = new Transform2({
- transform(chunk, _encoding, callback) {
- if (!firstChunkSeen && chunk?.length > 0) {
- firstChunkSeen = true;
- markFirstTokenObserved(sessionTarget, Date.now() - streamRequestedAt);
- }
- callback(null, chunk);
- }
- });
- const linkSanitizer = createSessionEventSanitizer(req.currentUser, { onEvent });
- const waitForDrain = () => new Promise((resolve) => res.once("drain", resolve));
- const writeClientChunk = async (chunk) => {
- if (res.writableEnded || clientClosed) return;
- let needsDrain = !res.write(chunk);
- if (pendingBalance != null && !res.writableEnded && !clientClosed) {
- needsDrain = !res.write(appendBalanceEvent(pendingBalance)) || needsDrain;
- pendingBalance = null;
- }
- if (needsDrain && !res.writableEnded && !clientClosed) {
- await waitForDrain();
- }
- };
- const clientSink = new Writable({
- write(chunk, _encoding, callback) {
- writeClientChunk(chunk).then(() => callback(), callback);
- }
- });
- const keepalive = setInterval(() => {
- if (!res.writableEnded && !clientClosed) {
- const ok = res.write(": keepalive\n\n");
- if (!ok) source.pause();
- }
- }, 2e4);
- res.on("drain", () => source.resume());
- await markStreamStarted(sessionId, sessionTarget);
- let streamCloseStatus = "closed";
- try {
- try {
- await pipeline(source, firstTokenProbe, linkSanitizer, billingTransform, clientSink);
- } catch (err) {
- streamCloseStatus = clientClosed || upstreamAbort.signal.aborted ? "aborted" : "error";
- throw err;
- }
- } finally {
- clearInterval(keepalive);
- req.off("close", abortUpstream);
- await markStreamEnded(sessionId, sessionTarget, { status: streamCloseStatus });
- if (!res.writableEnded) res.end();
- }
- } catch (err) {
- req.off("close", abortUpstream);
- if (clientClosed || upstreamAbort.signal.aborted) {
- if (!res.writableEnded) res.end();
- return;
- }
- res.status(502).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "SSE \u4EE3\u7406\u5931\u8D25",
- "SSE \u4EE3\u7406\u5931\u8D25"
- )
- });
- }
- };
- const proxyFallback = async (req, res) => {
- try {
- const pathname = req.originalUrl.replace(/^\/api/, "") || "/";
- if (isNativeH5ApiPath(pathname)) {
- res.status(404).json({
- message: "H5 \u672C\u5730\u63A5\u53E3\u672A\u627E\u5230\uFF0C\u8BF7\u786E\u8BA4\u670D\u52A1\u7AEF\u5DF2\u66F4\u65B0\u5E76\u91CD\u542F",
- code: "not_found"
- });
- return;
- }
- const isAgentRunPath = req.method === "POST" && pathname === "/agent/runs" || req.method === "GET" && /^\/agent\/runs\/[^/]+$/.test(pathname) || req.method === "GET" && /^\/agent\/runs\/[^/]+\/events$/.test(pathname);
- if (isAgentRunPath) {
- res.status(503).json({
- message: "Agent Run \u63A5\u53E3\u9700\u5728 Portal \u672C\u5730\u5904\u7406\uFF0C\u8BF7\u786E\u8BA4 server.mjs \u5DF2\u66F4\u65B0\u5E76\u91CD\u542F\u540E\u7AEF",
- code: "AGENT_RUNS_NATIVE_REQUIRED"
- });
- return;
- }
- const policyState = await userAuth2.resolveUserPolicies(req.currentUser);
- const capabilityState = await userAuth2.resolveUserCapabilities(req.currentUser);
- const gate = evaluateProxyRequest(req.method, pathname, policyState.policies, {
- unrestricted: policyState.unrestricted
- });
- if (!gate.allowed) {
- res.status(403).json({ message: gate.reason ?? "\u8BE5 API \u5DF2\u88AB\u7B56\u7565\u7981\u6B62" });
- return;
- }
- if (/^\/agent\/harness_(bootstrap|remember)$/.test(pathname) && !capabilityState.unrestricted && !capabilityState.capabilities.context_memory) {
- res.status(403).json({ message: "\u5F53\u524D\u8D26\u6237\u672A\u5F00\u901A\u9879\u76EE\u8BB0\u5FC6\uFF0C\u65E0\u6CD5\u8BBF\u95EE\u8BE5 API" });
- return;
- }
- const isReplyPath = pathname.match(/^\/sessions\/[^/]+\/reply$/) && req.method === "POST";
- const sessionMatch = pathname.match(/^\/sessions\/([^/]+)/);
- if (isReplyPath) {
- res.status(410).json({
- message: "\u804A\u5929\u63D0\u4EA4\u5165\u53E3\u5DF2\u7EDF\u4E00\u4E3A POST /agent/runs",
- code: "AGENT_RUNS_REQUIRED"
- });
- return;
- }
- let baseBody = req.body;
- if (isReplyPath && !policyState.unrestricted) {
- const publishLayout = await userAuth2.getUserPublishLayout(req.currentUser.id).catch(() => null);
- baseBody = injectCurrentTimeAnchor(req.body, publishLayout?.timezone);
- baseBody = injectTaskRoutingHint(
- baseBody,
- await userAuth2.getAgentSessionPolicy(req.currentUser.id)
- );
- }
- if (isReplyPath && llmProviderService2) {
- const sessionId = sessionMatch?.[1];
- if (sessionId) {
- const hasImages = messageHasImages(req.body?.user_message);
- 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,
- req.currentUser?.id,
- publishLayout
- ).catch(() => null);
- if (visionResult?.userMessage) {
- baseBody = { ...baseBody ?? req.body, user_message: visionResult.userMessage };
- }
- if (visionResult?.billableImageCount > 0 && subscriptionService) {
- await subscriptionService.consumeImageQuota(
- req.currentUser.id,
- visionResult.billableImageCount
- ).catch((err) => {
- console.warn(
- "Subscription image quota consume skipped:",
- err instanceof Error ? err.message : err
- );
- });
- }
- }
- }
- }
- const body = req.method === "GET" || req.method === "HEAD" ? void 0 : baseBody ? JSON.stringify(baseBody) : void 0;
- const fallbackTarget = req.goosedTarget ?? (sessionMatch ? await resolveTarget(sessionMatch[1]) : primaryTarget);
- const upstream = await apiFetch(fallbackTarget, apiSecret, pathname, {
- method: req.method,
- body,
- headers: {
- Accept: req.get("accept") ?? "*/*",
- "Last-Event-ID": req.get("last-event-id") ?? ""
- }
- });
- if (req.method === "GET" && /^\/sessions\/[^/]+$/.test(pathname) && upstream.ok) {
- const payload = await upstream.json().catch(() => null);
- if (payload && Array.isArray(payload.conversation)) {
- const sanitizedConversation = sanitizeSessionConversationPublicHtmlLinks(
- payload.conversation,
- req.currentUser
- );
- const { conversation, page } = paginateSessionConversation(
- sanitizedConversation,
- req.query?.history_before,
- req.query?.history_limit
- );
- payload.conversation = conversation;
- payload.conversation_page = page;
- }
- res.status(upstream.status).json(payload ?? {});
- return;
- }
- sendProxyResponse(res, upstream);
- } catch (err) {
- res.status(502).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u4EE3\u7406\u5931\u8D25",
- "\u4EE3\u7406\u5931\u8D25"
- )
- });
- }
- };
- return {
- requireUser,
- ensureChatAllowed,
- applySessionLlmProvider,
- applyLocalFallbackForSession,
- applyVisionProviderForSession,
- reconcileSessionPolicyForUser,
- handlers,
- sessionScoped,
- proxyFallback,
- proxySessionEvents,
- resolveTarget,
- startSessionForUser,
- getRuntimeStatus,
- submitSessionReplyForUser,
- apiFetch: async (pathname, init) => apiFetch(await pickTarget(), apiSecret, pathname, init),
- apiFetchTo: (target, pathname, init) => apiFetch(target, apiSecret, pathname, init)
- };
-}
-
-// 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 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
-function loadBillingConfig() {
- const useBackendCost = process.env.H5_USE_BACKEND_COST === "1";
- const marginMultiplier = Number(process.env.H5_MARGIN_MULTIPLIER ?? 1);
- return {
- useBackendCost,
- usdCnyRate: Number(process.env.H5_USD_CNY_RATE ?? 7.2),
- marginMultiplier: Number.isFinite(marginMultiplier) && marginMultiplier > 0 ? marginMultiplier : 1,
- inputCentsPer1k: Number(process.env.H5_BILL_INPUT_CENTS_PER_1K ?? 2),
- outputCentsPer1k: Number(process.env.H5_BILL_OUTPUT_CENTS_PER_1K ?? 6),
- minBillCents: Number(process.env.H5_MIN_BILL_CENTS ?? 1)
- };
-}
-function normalizeTokenState(raw) {
- if (!raw || typeof raw !== "object") {
- return {
- inputTokens: 0,
- outputTokens: 0,
- totalTokens: 0,
- accumulatedInputTokens: 0,
- accumulatedOutputTokens: 0,
- accumulatedTotalTokens: 0,
- accumulatedCost: null
- };
- }
- return {
- inputTokens: Number(raw.inputTokens ?? raw.input_tokens ?? 0),
- outputTokens: Number(raw.outputTokens ?? raw.output_tokens ?? 0),
- totalTokens: Number(raw.totalTokens ?? raw.total_tokens ?? 0),
- accumulatedInputTokens: Number(
- raw.accumulatedInputTokens ?? raw.accumulated_input_tokens ?? raw.inputTokens ?? 0
- ),
- accumulatedOutputTokens: Number(
- raw.accumulatedOutputTokens ?? raw.accumulated_output_tokens ?? raw.outputTokens ?? 0
- ),
- accumulatedTotalTokens: Number(
- raw.accumulatedTotalTokens ?? raw.accumulated_total_tokens ?? raw.totalTokens ?? 0
- ),
- accumulatedCost: raw.accumulatedCost ?? raw.accumulated_cost ?? null
- };
-}
-function computeDeltaCostCents(previous, current, config = loadBillingConfig()) {
- const prevCost = previous?.lastAccumulatedCost == null ? null : Number(previous.lastAccumulatedCost);
- const currCost = current.accumulatedCost == null ? null : Number(current.accumulatedCost);
- const margin = config.marginMultiplier ?? 1;
- if (config.useBackendCost && prevCost != null && currCost != null && currCost >= prevCost) {
- const deltaUsd = currCost - prevCost;
- if (deltaUsd <= 0) return 0;
- return Math.max(config.minBillCents, Math.ceil(deltaUsd * config.usdCnyRate * 100 * margin));
- }
- if (config.useBackendCost && prevCost == null && currCost != null && currCost > 0) {
- return Math.max(config.minBillCents, Math.ceil(currCost * config.usdCnyRate * 100 * margin));
- }
- const prevIn = Number(previous?.lastInputTokens ?? 0);
- const prevOut = Number(previous?.lastOutputTokens ?? 0);
- const deltaIn = Math.max(0, current.accumulatedInputTokens - prevIn);
- const deltaOut = Math.max(0, current.accumulatedOutputTokens - prevOut);
- if (deltaIn === 0 && deltaOut === 0) return 0;
- const raw = deltaIn / 1e3 * config.inputCentsPer1k + deltaOut / 1e3 * config.outputCentsPer1k;
- if (raw <= 0) return 0;
- return Math.max(config.minBillCents, Math.ceil(raw));
-}
-
-// billing-recharge.mjs
-function loadRechargeConfig() {
- const tiers = (process.env.H5_RECHARGE_TIERS_CENTS ?? "500,1000,3000,5000,10000,20000").split(",").map((value) => Number(value.trim())).filter((value) => Number.isFinite(value) && value > 0);
- return {
- tiersCents: tiers.length ? tiers : [500, 1e3, 3e3, 5e3, 1e4, 2e4],
- minRechargeCents: Number(process.env.H5_MIN_RECHARGE_CENTS ?? 500),
- orderTtlMs: Number(process.env.H5_RECHARGE_ORDER_TTL_MS ?? 15 * 60 * 1e3),
- maxPendingOrders: Number(process.env.H5_RECHARGE_MAX_PENDING ?? 3),
- dailyLimitCents: Number(process.env.H5_RECHARGE_DAILY_LIMIT_CENTS ?? 2e5)
- };
-}
-function buildInsufficientBalancePayload(balanceCents, config = loadRechargeConfig()) {
- return {
- code: "INSUFFICIENT_BALANCE",
- message: "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u7EE7\u7EED\u4F7F\u7528",
- balanceCents: Number(balanceCents ?? 0),
- minRechargeCents: config.minRechargeCents,
- suggestedTiers: config.tiersCents
- };
-}
-
-// user-space.mjs
-import fs6 from "node:fs";
-import path9 from "node:path";
-var UPLOAD_ZONE_CODES = ["oa", "public"];
-function resolveMindspaceStorageRoot(h5Root, env = process.env) {
- return path9.resolve(env.MINDSPACE_STORAGE_ROOT ?? path9.join(h5Root, "data", "mindspace"));
-}
-var WORKSPACE_HINTS_FILENAME2 = ".tkmindhints";
-var LEGACY_WORKSPACE_HINTS_FILENAME2 = ".goosehints";
-function renderUserSpaceBrandingBlock(userAddressName) {
- const name = userAddressName || "\u7528\u6237";
- return `## \u54C1\u724C\u4E0E\u79F0\u547C\uFF08\u786C\u6027\uFF09
-
-- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4ECB\u7ECD\u4EA7\u54C1\u65F6\u7528 TKMind\uFF0C\u4E0D\u8981\u79F0 goose\u3001Goose\u3001goosed
-- \u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\uFF0C\u7528 **${name}** \u79F0\u547C\u7528\u6237\uFF08\u53EF\u8F85\u4EE5\u300C\u4F60/\u60A8\u300D\uFF09\uFF0C**\u7981\u6B62**\u628A\u7528\u6237\u53EB\u4F5C TKMind
-- \u4EC5\u5728\u5F00\u573A\u6216\u7528\u6237\u6253\u62DB\u547C\u65F6\u4F7F\u7528\u65F6\u6BB5\u95EE\u5019\uFF0C\u4E14\u987B\u4E0E\u300CTKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6\u300D\u4E00\u81F4\uFF08\u5982\u300C${name}\uFF0C\u65E9\u4E0A\u597D\u300D\uFF09\uFF1B\u666E\u901A\u56DE\u590D\u76F4\u63A5\u4F5C\u7B54\uFF0C\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\u95EE\u5019\uFF1B\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind
-- \u4E0D\u8981\u63CF\u8FF0\u672C\u5DE5\u4F5C\u533A\u4E3A\u300CRust goose \u9879\u76EE\u300D\u6216\u300Cgoose AI \u6846\u67B6\u300D
-- \u672C\u5DE5\u4F5C\u533A\u662F TKMind **MindSpace \u7528\u6237\u7A7A\u95F4**\uFF0C\u7528\u4E8E OA/\u516C\u5F00\u6587\u4EF6\u7BA1\u7406\u4E0E\u9759\u6001\u9875\u9762\u751F\u6210
-`;
-}
-function resolveZoneDir(workspaceRoot, categoryCode) {
- return path9.join(workspaceRoot, categoryCode);
-}
-function resolveZoneFilePath(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) {
- fs6.mkdirSync(resolveZoneDir(workspaceRoot, code), { recursive: true });
- }
-}
-function mirrorAssetToZone({ workspaceRoot, categoryCode, filename, sourcePath }) {
- if (!UPLOAD_ZONE_CODES.includes(categoryCode)) return null;
- if (!sourcePath || !fs6.existsSync(sourcePath)) return null;
- ensureUserZoneDirs(workspaceRoot);
- const dest = resolveZoneFilePath(workspaceRoot, categoryCode, filename);
- fs6.mkdirSync(path9.dirname(dest), { recursive: true });
- fs6.copyFileSync(sourcePath, dest);
- return dest;
-}
-function isPathInsideUserWorkspace(workspaceRoot, requestedPath) {
- 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 });
- const zoneLines = UPLOAD_ZONE_CODES.map(
- (code) => `- \`${code}/\` \u2014 ${zoneLabel(code)}\uFF08\u7528\u6237\u4E0A\u4F20\u843D\u5728\u6B64\u5206\u533A\uFF09`
- );
- return `# TKMind \u7528\u6237\u7A7A\u95F4\u5206\u533A
-
-\u4F60\u662F\u7528\u6237 **${addressName}** \u7684\u4E13\u5C5E TKMind \u52A9\u624B\u3002\u5F53\u524D\u4F1A\u8BDD\u5DE5\u4F5C\u533A\uFF08\u552F\u4E00\u6587\u4EF6\u6839\u76EE\u5F55\uFF09\uFF1A
-\`${workspaceRoot}\`
-
-${renderUserSpaceBrandingBlock(addressName)}
-## \u5206\u533A\u89C4\u5219\uFF08\u786C\u6027\uFF09
-
-\u7528\u6237\u4E0A\u4F20\u7684\u6587\u4EF6**\u53EA\u4F1A**\u51FA\u73B0\u5728\u4E0B\u5217\u5B50\u76EE\u5F55\u4E4B\u4E00\uFF1A
-
-${zoneLines.join("\n")}
-
-## \u67E5\u627E / \u8BFB\u53D6\u6587\u4EF6
-
-1. \u5728\u5DE5\u4F5C\u533A\u5185\u641C\u7D22\uFF0C\u4F8B\u5982 \`tree oa/\`\u3001\`find oa -name '*.csv'\`\u3001\`cat oa/\u6587\u4EF6\u540D.csv\`
-2. \u7528\u6237\u8BF4\u300COA \u533A\u300D\u2192 \`oa/\`\uFF1B\u300C\u79C1\u4EBA\u533A\u300D\u2192 \`private/\`\uFF1B\u300C\u516C\u5F00\u533A\u300D\u2192 \`public/\`
-3. **\u7981\u6B62**\u8BBF\u95EE\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\`${PUBLISH_ROOT_DIR}/\` \u6839\u76EE\u5F55\uFF08\u975E\u672C\u7528\u6237\uFF09\u3001\`data/mindspace/\`\u3001\u4E3B\u673A\u7EDD\u5BF9\u8DEF\u5F84
-4. **\u7981\u6B62**\u7528\u516C\u7F51 URL \u5217\u76EE\u5F55\u6216\u8BFB CSV\uFF1B\u516C\u7F51\u94FE\u63A5\u4EC5\u7528\u4E8E\u5206\u4EAB\u5DF2\u53D1\u5E03 HTML
-5. \u627E\u4E0D\u5230\u65F6\u8BF4\u660E\u8BE5\u5206\u533A\u5185\u6CA1\u6709\u8BE5\u6587\u4EF6\uFF0C\u8BF7\u7528\u6237\u786E\u8BA4\u4E0A\u4F20\u5230\u4E86\u54EA\u4E2A\u5206\u533A
-
-## \u751F\u6210\u9875\u9762
-
-- \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
-- \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\`
-- \u7ED9\u94FE\u63A5\u65F6\u6309 \`static-page-publish\` skill \u91CC\u7684\u516C\u7F51\u524D\u7F00\u6A21\u677F\u62FC\u63A5\u771F\u5B9E\u5730\u5740\uFF0C\u4E0D\u786E\u5B9A\u57DF\u540D\u65F6\u7528\u76F8\u5BF9\u8DEF\u5F84 \`public/xxx.html\` \u8BF4\u660E\uFF0C\u4E0D\u8981\u731C\u4E00\u4E2A\u57DF\u540D\u5145\u6570
-- **\u7981\u6B62**\u56DE\u590D\u300C\u8BF7\u624B\u52A8\u4FDD\u5B58\u5230 public \u76EE\u5F55\u300D\u300C\u6211\u65E0\u6CD5\u751F\u6210\u9875\u9762\u300D\u2014\u2014\u9664\u975E \`write_file\` / \`edit_file\` \u8C03\u7528\u5DF2\u5931\u8D25\u5E76\u8BF4\u660E\u5177\u4F53\u9519\u8BEF
-
-## \u5DE5\u4F5C\u533A\u6587\u4EF6\u4E0E OA \u754C\u9762
-
-- \u5199\u5165 \`oa/\`\u3001\`private/\`\u3001\`public/\` \u4E0B\u4EFB\u610F\u5C42\u7EA7\u7684\u652F\u6301\u7C7B\u578B\u6587\u4EF6\uFF08docx\u3001md\u3001txt\u3001csv\u3001pdf\u3001\u56FE\u7247\u7B49\uFF09\u4F1A**\u81EA\u52A8\u540C\u6B65**\u5230 MindSpace \u8D44\u4EA7\u5E93\uFF08\u542B\u5B50\u76EE\u5F55\uFF0C\u5982 \`oa/\u8BD7\u6B4C\u6563\u6587/\u590F\u65E5\u7684\u8BD7\u7BC7.md\`\uFF09
-- \u6253\u5F00\u5BF9\u5E94\u5206\u533A\u6216\u4FDD\u5B58\u6587\u4EF6\u540E\u4F1A\u51FA\u73B0\u5728\u754C\u9762\u4E2D\uFF0C\u53EF\u76F4\u63A5\u9884\u89C8\u6216\u4E0B\u8F7D
-- \u751F\u6210 docx/pdf \u7B49 OA \u8D44\u6599\u65F6\uFF0C\u53EF\u76F4\u63A5 \`write_file\` \u5230 \`oa/<\u5B50\u76EE\u5F55>/<\u6587\u4EF6\u540D>\`\uFF0C\u65E0\u9700\u624B\u52A8\u590D\u5236\u5230\u6839\u76EE\u5F55
-- \u7528\u6237\u4E0A\u4F20\u7684\u6587\u4EF6\u4ECD\u4EE5\u754C\u9762\u5165\u5E93\u4E3A\u51C6\uFF0C\u5E76\u955C\u50CF\u5230\u4E0A\u8FF0\u5206\u533A
-`;
-}
-function buildUserSpaceConstraints({ username, workspaceRoot, publicBaseUrl, slug, displayName }) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- const zoneList = UPLOAD_ZONE_CODES.map((code) => `\`${code}/\`\uFF08${zoneLabel(code)}\uFF09`).join("\u3001");
- return [
- "## TKMind \u7528\u6237\u7A7A\u95F4\u5206\u533A\uFF08\u786C\u6027\u7EA6\u675F\uFF09",
- "",
- `- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\u7528 **${addressName}** \u79F0\u547C\u7528\u6237\uFF0C\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind`,
- "- \u7981\u6B62\u79F0 goose / Goose / goosed \u6216\u300CRust goose \u9879\u76EE\u300D",
- `- \u7528\u6237 **${addressName}** \u7684 Agent \u5DE5\u4F5C\u533A\uFF1A\`${workspaceRoot}\``,
- `- \u7528\u6237\u4E0A\u4F20\u843D\u5728\u5206\u533A\u5B50\u76EE\u5F55\uFF1A${zoneList}\uFF08\u4F8B\u5982 OA \u6587\u4EF6\u5728 \`oa/\`\uFF09`,
- "- **\u67E5\u627E\u6587\u4EF6**\uFF1A\u53EA\u5728\u4E0A\u8FF0\u5DE5\u4F5C\u533A\u5185\u641C\u7D22\uFF08\u5982 `oa/2025-12-06T13-34_export.csv`\uFF09",
- "- **\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",
- "- **\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\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 = path9.join(workspaceRoot, WORKSPACE_HINTS_FILENAME2);
- const legacyPath = path9.join(workspaceRoot, LEGACY_WORKSPACE_HINTS_FILENAME2);
- const content = renderUserSpaceHints({ ...context, workspaceRoot });
- fs6.writeFileSync(hintsPath, content, "utf8");
- if (fs6.existsSync(legacyPath)) {
- fs6.unlinkSync(legacyPath);
- }
- return hintsPath;
-}
-async function syncUserZonesFromAssets(pool2, storageRoot, userId, workspaceRoot) {
- ensureUserZoneDirs(workspaceRoot);
- const placeholders = UPLOAD_ZONE_CODES.map(() => "?").join(", ");
- const [rows] = await pool2.query(
- `SELECT a.original_filename, c.category_code, v.storage_key
- FROM h5_assets a
- JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id
- JOIN h5_asset_versions v ON v.id = a.current_version_id
- WHERE a.user_id = ? AND a.status <> 'deleted' AND c.category_code IN (${placeholders})`,
- [userId, ...UPLOAD_ZONE_CODES]
- );
- for (const row of rows) {
- const sourcePath = path9.join(storageRoot, row.storage_key);
- mirrorAssetToZone({
- workspaceRoot,
- categoryCode: row.category_code,
- filename: row.original_filename,
- sourcePath
- });
- }
- return { workspaceRoot, mirrored: rows.length };
-}
-async function ensureUserSpaceLayout({
- pool: pool2,
- storageRoot,
- userId,
- username,
- displayName,
- publicBaseUrl,
- slug,
- workspaceRoot
-}) {
- ensureUserZoneDirs(workspaceRoot);
- if (pool2 && userId) {
- await syncUserZonesFromAssets(pool2, storageRoot, userId, workspaceRoot);
- }
- const context = {
- username: username ?? slug,
- displayName,
- workspaceRoot,
- slug,
- publicBaseUrl
- };
- ensureUserSpaceHints(workspaceRoot, context);
- return {
- workspaceRoot,
- zonesRoot: workspaceRoot,
- zonesPublicDir: resolveZoneDir(workspaceRoot, "public"),
- constraints: buildUserSpaceConstraints(context),
- ...context
- };
-}
-
-// skills-registry.mjs
-import fs7 from "node:fs";
-import path10 from "node:path";
-import { fileURLToPath as fileURLToPath4 } from "node:url";
-var __dirname3 = path10.dirname(fileURLToPath4(import.meta.url));
-var DEFAULT_USER_SKILLS = {
- web: true,
- search: true,
- "schedule-assistant": true,
- "form-builder": true,
- "table-viewer": true,
- "product-campaign-page": true,
- "docx-generate": true,
- "long-image-download": true,
- [PUBLISH_SKILL_NAME]: false
-};
-var USER_ROLE_SKILL_PRESETS = {
- user: DEFAULT_USER_SKILLS,
- creator: {
- ...DEFAULT_USER_SKILLS,
- [PUBLISH_SKILL_NAME]: true,
- kanban: true,
- timeline: true
- },
- developer: {
- ...DEFAULT_USER_SKILLS,
- git: true,
- "diff-viewer": true,
- "code-playground": true,
- "test-runner": true
- }
-};
-function parseSkillFrontmatter(content) {
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
- if (!match) return { name: null, description: "" };
- const block = match[1];
- const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? null;
- const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
- return { name, description };
-}
-function listPlatformSkillCatalog(h5Root = __dirname3) {
- const skillsRoot = path10.join(h5Root, "skills");
- if (!fs7.existsSync(skillsRoot)) return [];
- const catalog = [];
- for (const entry of fs7.readdirSync(skillsRoot, { withFileTypes: true })) {
- if (!entry.isDirectory()) continue;
- 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({
- name,
- dirName: entry.name,
- label: name,
- description: meta.description || "\u5E73\u53F0\u901A\u7528\u6280\u80FD",
- category: "platform",
- requiresPublish: name === PUBLISH_SKILL_NAME
- });
- }
- return catalog.sort((a, b) => a.name.localeCompare(b.name));
-}
-function isValidSkillName(catalog, name) {
- return catalog.some((item) => item.name === name);
-}
-function normalizeSkillPatch(catalog, patch) {
- const normalized = {};
- for (const [key, value] of Object.entries(patch ?? {})) {
- if (!isValidSkillName(catalog, key)) continue;
- normalized[key] = Boolean(value);
- }
- return normalized;
-}
-function resolveSkillMap(roleGrants, userOverrides, catalog) {
- const resolved = {};
- for (const item of catalog) {
- const key = item.name;
- if (key in userOverrides) {
- resolved[key] = userOverrides[key];
- } else if (key in roleGrants) {
- resolved[key] = roleGrants[key];
- } else {
- resolved[key] = DEFAULT_USER_SKILLS[key] ?? false;
- }
- }
- return resolved;
-}
-function grantedSkillNames(skillMap) {
- return Object.entries(skillMap).filter(([, enabled]) => enabled).map(([name]) => name);
-}
-function applySkillGrantsToCapabilities(capabilities, skillMap) {
- const effective = { ...capabilities };
- const enabled = grantedSkillNames(skillMap);
- if (enabled.length > 0) {
- effective.skills = true;
- }
- if (enabled.includes(PUBLISH_SKILL_NAME)) {
- effective.static_publish = true;
- }
- return effective;
-}
-function copySkillTree(srcDir, destDir) {
- 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 {
- fs7.copyFileSync(from, to);
- }
- }
-}
-function syncSkillsToWorkspace({
- h5Root,
- publishDir,
- skillMap,
- catalog,
- user,
- publicBaseUrl
-}) {
- const enabled = new Set(grantedSkillNames(skillMap));
- const platformNames = new Set(catalog.map((item) => item.name));
- 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)) {
- fs7.rmSync(path10.join(agentsSkills, entry.name), { recursive: true, force: true });
- }
- }
- }
- for (const item of catalog) {
- if (!enabled.has(item.name)) continue;
- 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(),
- username: user.username ? String(user.username).trim().toLowerCase() : void 0,
- publicBaseUrl,
- publishDir
- });
- continue;
- }
- if (fs7.existsSync(srcDir)) {
- if (fs7.existsSync(destDir)) fs7.rmSync(destDir, { recursive: true, force: true });
- copySkillTree(srcDir, destDir);
- }
- }
-}
-
-// user-auth.mjs
-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 && crypto5.timingSafeEqual(a, b);
-}
-var PASSWORD_ALGORITHM_PBKDF2 = "pbkdf2-sha512";
-var PASSWORD_ALGORITHM_ARGON2ID = "argon2id";
-var ARGON2_MEMORY = 64 * 1024;
-var ARGON2_PASSES = 3;
-var ARGON2_PARALLELISM = 1;
-var ARGON2_TAG_LENGTH = 32;
-function hashPasswordPbkdf2(password, salt) {
- return crypto5.pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
-}
-function hashPasswordArgon2id(password, salt) {
- return argon2HashRawSync(password, {
- salt: Buffer.from(salt, "hex"),
- parallelism: ARGON2_PARALLELISM,
- outputLen: ARGON2_TAG_LENGTH,
- memoryCost: ARGON2_MEMORY,
- timeCost: ARGON2_PASSES,
- algorithm: Argon2Algorithm.Argon2id
- }).toString("hex");
-}
-function createPasswordRecord(password, algorithm = PASSWORD_ALGORITHM_ARGON2ID) {
- const salt = crypto5.randomBytes(16).toString("hex");
- if (algorithm === PASSWORD_ALGORITHM_ARGON2ID) {
- return {
- salt,
- passwordHash: hashPasswordArgon2id(password, salt),
- passwordAlgorithm: PASSWORD_ALGORITHM_ARGON2ID
- };
- }
- return {
- salt,
- passwordHash: hashPasswordPbkdf2(password, salt),
- passwordAlgorithm: PASSWORD_ALGORITHM_PBKDF2
- };
-}
-function verifyPassword(password, row) {
- const algorithm = row.password_algorithm || PASSWORD_ALGORITHM_PBKDF2;
- if (algorithm === PASSWORD_ALGORITHM_ARGON2ID) {
- return safeEqual(hashPasswordArgon2id(password, row.salt), row.password_hash);
- }
- return safeEqual(hashPasswordPbkdf2(password, row.salt), row.password_hash);
-}
-function normalizeUsername(username) {
- return username.trim().toLowerCase();
-}
-function isValidUsername(username) {
- return /^[a-z0-9_]{2,32}$/.test(username);
-}
-function isValidEmail(email) {
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
-}
-function hashSessionToken(token) {
- return crypto5.createHash("sha256").update(token).digest("hex");
-}
-function createUserAuth(pool2, options = {}) {
- 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);
- const skillCatalog = listPlatformSkillCatalog(h5Root);
- const defaultSignupBalanceCents = Number(options.defaultSignupBalanceCents ?? 500);
- const lowBalanceGiftThresholdCents = Number(options.lowBalanceGiftThresholdCents ?? 100);
- const lowBalanceGiftAmountCents = Number(options.lowBalanceGiftAmountCents ?? 1e3);
- const sessionTtlMs = Number(options.sessionTtlMs ?? 7 * 24 * 60 * 60 * 1e3);
- const loginMaxFailures = Number(options.loginMaxFailures ?? 5);
- const loginFailureWindowMs = Number(options.loginFailureWindowMs ?? 5 * 60 * 1e3);
- const persistSessions = options.persistSessions !== false && Boolean(pool2);
- let rechargeNotifier = typeof options.onRechargeNotification === "function" ? options.onRechargeNotification : null;
- const subscriptionService = options.subscriptionService ?? null;
- const sessions = /* @__PURE__ */ new Map();
- const loginFailures = /* @__PURE__ */ new Map();
- const pruneLoginFailures = (now = Date.now()) => {
- for (const [key, state] of loginFailures) {
- if (state.resetAt <= now) loginFailures.delete(key);
- }
- };
- const pruneSessions = (now = Date.now()) => {
- for (const [token, session] of sessions) {
- if (session.expiresAt <= now) sessions.delete(token);
- }
- };
- const storeSession = async (userId, role, token, now = Date.now()) => {
- const expiresAt = now + sessionTtlMs;
- sessions.set(token, { userId, role, expiresAt });
- if (!persistSessions) return expiresAt;
- await pool2.query(
- `INSERT INTO h5_login_sessions (id, user_id, token_hash, expires_at, created_at)
- VALUES (?, ?, ?, ?, ?)`,
- [crypto5.randomUUID(), userId, hashSessionToken(token), expiresAt, now]
- );
- return expiresAt;
- };
- const revokeAllSessionsForUser = async (userId, now = Date.now()) => {
- for (const [token, session] of sessions) {
- if (session.userId === userId) sessions.delete(token);
- }
- if (!persistSessions) return;
- await pool2.query(
- `UPDATE h5_login_sessions SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL`,
- [now, userId]
- );
- };
- const ensureWorkspace = (workspaceRoot) => {
- fs8.mkdirSync(workspaceRoot, { recursive: true });
- };
- const isAdminRole = (user) => user?.role === "admin";
- const getUserById = async (userId) => {
- const [rows] = await pool2.query(
- `SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
- u.plan_type, u.workspace_root,
- s.quota_bytes, s.used_bytes, s.reserved_bytes,
- w.balance_cents, w.tokens_used,
- (SELECT COALESCE(SUM(ABS(amount_cents)), 0)
- FROM h5_billing_ledger l
- WHERE l.user_id = u.id AND l.type = 'deduct') AS spent_cents
- FROM h5_users u
- LEFT JOIN h5_user_spaces s ON s.user_id = u.id
- LEFT JOIN h5_user_wallets w ON w.user_id = u.id
- WHERE u.id = ?`,
- [userId]
- );
- return rows[0] ?? null;
- };
- const publicUser = (row) => {
- const balanceCents = Number(row.balance_cents ?? 0);
- const spentCents = Number(row.spent_cents ?? 0);
- const base = {
- id: row.id,
- username: row.username,
- slug: row.slug ?? row.username,
- email: row.email ?? null,
- displayName: row.display_name,
- role: row.role,
- status: row.status,
- planType: row.plan_type ?? "free",
- workspaceRoot: row.workspace_root,
- balanceCents,
- totalCreditCents: balanceCents + spentCents,
- tokensUsed: Number(row.tokens_used ?? 0),
- spaceQuotaBytes: Number(row.quota_bytes ?? 0),
- spaceUsedBytes: Number(row.used_bytes ?? 0),
- spaceReservedBytes: Number(row.reserved_bytes ?? 0),
- spaceAvailableBytes: Math.max(
- 0,
- Number(row.quota_bytes ?? 0) - Number(row.used_bytes ?? 0) - Number(row.reserved_bytes ?? 0)
- )
- };
- const publishKey = row.id;
- return {
- ...base,
- publishSlug: publishKey,
- publishUrl: buildPublicUrl(publicBaseUrl, publishKey),
- publishSkillName: PUBLISH_SKILL_NAME
- };
- };
- const publishLayoutFor = async (user, { migrateLegacy = true } = {}) => {
- const web = ensureUserPublishLayout({
- h5Root,
- publicBaseUrl,
- user,
- legacyUsersRoot: migrateLegacy ? usersRoot : null
- });
- const space = await ensureUserSpaceLayout({
- pool: pool2,
- storageRoot,
- userId: user.id,
- username: user.username ?? web.slug,
- displayName: user.displayName,
- publicBaseUrl,
- slug: web.slug,
- workspaceRoot: web.publishDir
- });
- const hintsContext = {
- slug: web.slug,
- username: user.username ?? web.username,
- displayName: user.displayName,
- publicBaseUrl,
- publishDir: web.publishDir
- };
- ensurePublishSkillInstalled(web.publishDir, hintsContext);
- ensureWorkspaceHintsInstalled(web.publishDir, hintsContext);
- const legacyPublishDir = resolveLegacyPublishDir(h5Root, user);
- if (legacyPublishDir && legacyPublishDir !== web.publishDir && fs8.existsSync(legacyPublishDir)) {
- ensureWorkspaceHintsInstalled(legacyPublishDir, { ...hintsContext, publishDir: legacyPublishDir });
- }
- ensureUserMemoryProfile(web.publishDir, {
- userId: user.id,
- displayName: user.displayName ?? user.display_name,
- username: user.username ?? web.username,
- slug: web.slug
- });
- return {
- ...web,
- ...space,
- publishDir: web.publishDir,
- constraints: web.constraints
- };
- };
- const listSkillGrants = async (subjectType, subjectId) => {
- const [rows] = await pool2.query(
- `SELECT skill_name, enabled
- FROM h5_user_skill_grants
- WHERE subject_type = ? AND subject_id = ?`,
- [subjectType, subjectId]
- );
- return Object.fromEntries(rows.map((row) => [row.skill_name, Boolean(row.enabled)]));
- };
- const resolveUserSkillMap = async (user) => {
- if (!user || user.role === "admin") {
- return Object.fromEntries(skillCatalog.map((item) => [item.name, true]));
- }
- const roleDefaults = await listSkillGrants("role", "user");
- const userOverrides = await listSkillGrants("user", user.id);
- return resolveSkillMap(roleDefaults, userOverrides, skillCatalog);
- };
- const syncUserSkillsForUser = async (user) => {
- if (!user || user.role === "admin") return;
- const layout = await syncUserPublishWorkspace(user);
- const skillMap = await resolveUserSkillMap(user);
- syncSkillsToWorkspace({
- h5Root,
- publishDir: layout?.publishDir,
- skillMap,
- catalog: skillCatalog,
- user,
- publicBaseUrl
- });
- };
- const syncUserPublishWorkspace = async (user) => {
- if (!user) return null;
- const layout = await publishLayoutFor(user);
- 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 = ?`, [
- layout.publishDir,
- now,
- user.id
- ]);
- await pool2.query(`DELETE FROM h5_user_path_grants WHERE user_id = ?`, [user.id]);
- await pool2.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [user.id, layout.publishDir]
- );
- }
- return layout;
- };
- const recordSignupBonus = async (conn, userId, amountCents, now) => {
- const amount = Number(amountCents);
- if (!Number.isFinite(amount) || amount <= 0) return;
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, note, operator_id, created_at)
- VALUES (?, 'adjust', ?, 0, '\u65B0\u7528\u6237\u8D60\u9001', NULL, ?)`,
- [userId, amount, now]
- );
- };
- const shouldEnableLowBalanceGift = ({ isAdmin = false, initialBalanceCents }) => {
- return !isAdmin && Number(initialBalanceCents) === defaultSignupBalanceCents && defaultSignupBalanceCents > 0;
- };
- const grantLowBalanceGiftIfNeeded = async (conn, { userId, currentBalance, nextBalance, now }) => {
- if (lowBalanceGiftAmountCents <= 0 || lowBalanceGiftThresholdCents < 0 || nextBalance > lowBalanceGiftThresholdCents) {
- return { gifted: false, balanceAfter: nextBalance };
- }
- const [rows] = await conn.query(
- `SELECT low_balance_gift_eligible, low_balance_gift_granted_at
- FROM h5_users
- WHERE id = ?
- FOR UPDATE`,
- [userId]
- );
- const user = rows[0];
- if (!user || !Boolean(user.low_balance_gift_eligible) || user.low_balance_gift_granted_at != null) {
- return { gifted: false, balanceAfter: nextBalance };
- }
- const giftedBalance = nextBalance + lowBalanceGiftAmountCents;
- await conn.query(
- `UPDATE h5_user_wallets
- SET balance_cents = ?, updated_at = ?
- WHERE user_id = ?`,
- [giftedBalance, now, userId]
- );
- await conn.query(
- `UPDATE h5_users
- SET low_balance_gift_eligible = 0,
- low_balance_gift_granted_at = ?,
- status = CASE WHEN status = 'suspended' THEN 'active' ELSE status END,
- updated_at = ?
- WHERE id = ?`,
- [now, now, userId]
- );
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, note, operator_id, created_at)
- VALUES (?, 'adjust', ?, 0, ?, NULL, ?)`,
- [userId, lowBalanceGiftAmountCents, "\u65B0\u7528\u6237\u4F4E\u4F59\u989D\u81EA\u52A8\u8D60\u9001", now]
- );
- await conn.query(
- `INSERT INTO h5_user_notifications
- (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
- VALUES (?, ?, 'web', 'low_balance_gift', ?, ?, ?, 'unread', NULL, ?, ?)`,
- [
- 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`,
- JSON.stringify({
- triggerBalanceCents: nextBalance,
- previousBalanceCents: currentBalance,
- giftAmountCents: lowBalanceGiftAmountCents,
- thresholdCents: lowBalanceGiftThresholdCents
- }),
- now,
- now
- ]
- );
- return { gifted: true, balanceAfter: giftedBalance };
- };
- const register = async ({ username, password, displayName, email }) => {
- const normalized = normalizeUsername(username);
- if (!isValidUsername(normalized)) {
- return { ok: false, message: "\u7528\u6237\u540D\u4EC5\u652F\u6301 2-32 \u4F4D\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF" };
- }
- if (!password || password.length < 6) {
- return { ok: false, message: "\u5BC6\u7801\u81F3\u5C11 6 \u4F4D" };
- }
- if (!email || !isValidEmail(email.trim())) {
- return { ok: false, message: "\u8BF7\u8F93\u5165\u6709\u6548\u90AE\u7BB1" };
- }
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
- const userId = crypto5.randomUUID();
- const layout = await publishLayoutFor({ id: userId, username: normalized });
- const workspaceRoot = layout.publishDir;
- const now = Date.now();
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- await conn.query(
- `INSERT INTO h5_users
- (id, username, slug, email, display_name, salt, password_hash, password_algorithm,
- role, status, plan_type, workspace_root, low_balance_gift_eligible, low_balance_gift_granted_at,
- created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'user', 'active', 'free', ?, 1, NULL, ?, ?)`,
- [
- userId,
- normalized,
- normalized,
- email?.trim().toLowerCase() || null,
- displayName?.trim() || normalized,
- salt,
- passwordHash,
- passwordAlgorithm,
- workspaceRoot,
- now,
- now
- ]
- );
- await conn.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)`,
- [userId, defaultSignupBalanceCents, now]
- );
- await recordSignupBonus(conn, userId, defaultSignupBalanceCents, now);
- await conn.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [userId, workspaceRoot]
- );
- await initializeDefaultSpace(conn, userId, {
- quotaBytes: Number(process.env.MINDSPACE_FREE_QUOTA_BYTES ?? 5 * 1024 * 1024),
- now
- });
- await conn.commit();
- ensureWorkspace(workspaceRoot);
- ensureUserMemoryProfile(workspaceRoot, {
- userId,
- displayName: displayName?.trim() || normalized,
- username: normalized,
- slug: normalized
- });
- if (subscriptionService) {
- subscriptionService.grantSubscription(userId, "free", null, null, "\u6CE8\u518C\u8D60\u9001\u514D\u8D39\u5957\u9910").catch(() => {
- });
- }
- const user = await getUserById(userId);
- return { ok: true, user: publicUser(user) };
- } catch (err) {
- await conn.rollback();
- if (err?.code === "ER_DUP_ENTRY") {
- return { ok: false, message: "\u7528\u6237\u540D\u3001\u4E3B\u9875\u5730\u5740\u6216\u90AE\u7BB1\u5DF2\u5B58\u5728" };
- }
- throw err;
- } finally {
- conn.release();
- }
- };
- const login = async ({ username, password, ip = "unknown", now = Date.now() }) => {
- pruneLoginFailures(now);
- const normalized = normalizeUsername(username);
- const failureKey = `${ip}:${normalized}`;
- const failure = loginFailures.get(failureKey);
- if (failure && failure.count >= loginMaxFailures && failure.resetAt > now) {
- return {
- ok: false,
- message: "\u5C1D\u8BD5\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
- retryAfterMs: failure.resetAt - now
- };
- }
- const [rows] = await pool2.query(
- `SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
- u.plan_type, u.workspace_root,
- u.salt, u.password_hash, u.password_algorithm, w.balance_cents, w.tokens_used
- FROM h5_users u
- LEFT JOIN h5_user_wallets w ON w.user_id = u.id
- WHERE u.username = ?`,
- [normalized]
- );
- const row = rows[0];
- if (!row) {
- const current = failure && failure.resetAt > now ? failure : { count: 0, resetAt: now + loginFailureWindowMs };
- current.count += 1;
- loginFailures.set(failureKey, current);
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF" };
- }
- if (!verifyPassword(password, row)) {
- const current = failure && failure.resetAt > now ? failure : { count: 0, resetAt: now + loginFailureWindowMs };
- current.count += 1;
- loginFailures.set(failureKey, current);
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF" };
- }
- if (row.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- if ((row.password_algorithm || PASSWORD_ALGORITHM_PBKDF2) !== PASSWORD_ALGORITHM_ARGON2ID) {
- const nextPassword = createPasswordRecord(password);
- await pool2.query(
- `UPDATE h5_users
- SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ?
- WHERE id = ?`,
- [nextPassword.salt, nextPassword.passwordHash, nextPassword.passwordAlgorithm, now, row.id]
- );
- row.salt = nextPassword.salt;
- row.password_hash = nextPassword.passwordHash;
- row.password_algorithm = nextPassword.passwordAlgorithm;
- }
- loginFailures.delete(failureKey);
- const token = crypto5.randomBytes(32).toString("base64url");
- await storeSession(row.id, row.role, token, now);
- return { ok: true, token, user: publicUser(row) };
- };
- const resetPassword = async ({ username, email, password }) => {
- const normalized = normalizeUsername(username);
- if (!isValidUsername(normalized)) {
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u90AE\u7BB1\u4E0D\u6B63\u786E" };
- }
- if (!email || !isValidEmail(email.trim())) {
- return { ok: false, message: "\u8BF7\u8F93\u5165\u6709\u6548\u90AE\u7BB1" };
- }
- if (!password || password.length < 6) {
- return { ok: false, message: "\u65B0\u5BC6\u7801\u81F3\u5C11 6 \u4F4D" };
- }
- const [rows] = await pool2.query(
- `SELECT id, email, status FROM h5_users WHERE username = ? LIMIT 1`,
- [normalized]
- );
- const row = rows[0];
- const normalizedEmail = email.trim().toLowerCase();
- if (!row || (row.email ?? "").toLowerCase() !== normalizedEmail) {
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u90AE\u7BB1\u4E0D\u6B63\u786E" };
- }
- if (row.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
- const now = Date.now();
- await pool2.query(
- `UPDATE h5_users SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ? WHERE id = ?`,
- [salt, passwordHash, passwordAlgorithm, now, row.id]
- );
- await revokeAllSessionsForUser(row.id, now);
- return { ok: true };
- };
- const verify = async (token, now = Date.now()) => {
- if (!token) return null;
- pruneSessions(now);
- const cached = sessions.get(token);
- if (cached) {
- if (cached.expiresAt <= now) {
- sessions.delete(token);
- return null;
- }
- const user = await getUserById(cached.userId);
- if (!user || user.status === "disabled") {
- await revoke(token, now);
- return null;
- }
- cached.expiresAt = now + sessionTtlMs;
- if (persistSessions) {
- await pool2.query(
- `UPDATE h5_login_sessions
- SET expires_at = ?
- WHERE token_hash = ? AND revoked_at IS NULL`,
- [cached.expiresAt, hashSessionToken(token)]
- );
- }
- return cached;
- }
- if (!persistSessions) return null;
- const tokenHash = hashSessionToken(token);
- const [rows] = await pool2.query(
- `SELECT s.user_id, s.expires_at, u.role, u.status
- FROM h5_login_sessions s
- JOIN h5_users u ON u.id = s.user_id
- WHERE s.token_hash = ? AND s.revoked_at IS NULL
- LIMIT 1`,
- [tokenHash]
- );
- const row = rows[0];
- if (!row || Number(row.expires_at ?? 0) <= now || row.status === "disabled") {
- if (row) await revoke(token, now);
- return null;
- }
- const expiresAt = now + sessionTtlMs;
- await pool2.query(
- `UPDATE h5_login_sessions SET expires_at = ? WHERE token_hash = ? AND revoked_at IS NULL`,
- [expiresAt, tokenHash]
- );
- const session = { userId: row.user_id, role: row.role, expiresAt };
- sessions.set(token, session);
- return session;
- };
- const revoke = async (token, now = Date.now()) => {
- if (!token) return;
- sessions.delete(token);
- if (!persistSessions) return;
- await pool2.query(
- `UPDATE h5_login_sessions SET revoked_at = ? WHERE token_hash = ? AND revoked_at IS NULL`,
- [now, hashSessionToken(token)]
- );
- };
- const getMe = async (token) => {
- const session = await verify(token);
- if (!session) return null;
- const user = await getUserById(session.userId);
- if (!user) return null;
- return publicUser(user);
- };
- const listPathGrants = async (userId) => {
- const [rows] = await pool2.query(
- `SELECT path, mode FROM h5_user_path_grants WHERE user_id = ? ORDER BY path`,
- [userId]
- );
- return rows.map((row) => ({ path: row.path, mode: row.mode }));
- };
- const resolveWorkingDir = async (userId) => {
- const user = await getUserById(userId);
- if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728");
- const layout = await syncUserPublishWorkspace(user);
- return layout.publishDir;
- };
- const getUserPublishLayout = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return null;
- return syncUserPublishWorkspace(user);
- };
- const isPathAllowed = async (userId, requestedPath) => {
- const user = await getUserById(userId);
- if (!user) return false;
- if (isAdminRole(user)) return true;
- const layout = await publishLayoutFor(user, { migrateLegacy: false });
- return isPathInsideUserWorkspace(layout.publishDir, requestedPath);
- };
- const repairAllUserPublishDirs = async () => {
- const [rows] = await pool2.query(
- `SELECT id, username, role, workspace_root FROM h5_users WHERE role = 'user'`
- );
- for (const row of rows) {
- await syncUserSkillsForUser(row);
- }
- };
- const seedRoleSkillDefaults = async () => {
- const now = Date.now();
- for (const [name, enabled] of Object.entries(DEFAULT_USER_SKILLS)) {
- await pool2.query(
- `INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
- VALUES ('role', 'user', ?, ?, ?)
- ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), updated_at = VALUES(updated_at)`,
- [name, enabled ? 1 : 0, now]
- );
- }
- };
- const registerAgentSession = async (userId, agentSessionId, goosedTarget = 0) => {
- const isLegacyIndex = typeof goosedTarget === "number" || /^\d+$/.test(String(goosedTarget));
- const goosedNode = isLegacyIndex ? Number(goosedTarget) : 0;
- const targetUrl = isLegacyIndex ? null : String(goosedTarget);
- await pool2.query(
- `INSERT INTO h5_user_sessions (agent_session_id, user_id, goosed_node, goosed_target, created_at)
- VALUES (?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE user_id = VALUES(user_id),
- goosed_node = VALUES(goosed_node), goosed_target = VALUES(goosed_target)`,
- [agentSessionId, userId, goosedNode, targetUrl, Date.now()]
- );
- };
- const getSessionNode = async (agentSessionId) => {
- const [rows] = await pool2.query(
- `SELECT goosed_node FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
- [agentSessionId]
- );
- return rows[0]?.goosed_node ?? 0;
- };
- const getSessionTarget = async (agentSessionId) => {
- const [rows] = await pool2.query(
- `SELECT goosed_node, goosed_target FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
- [agentSessionId]
- );
- return { target: rows[0]?.goosed_target ?? null, node: rows[0]?.goosed_node ?? 0 };
- };
- const ownsSession = async (userId, agentSessionId) => {
- const [rows] = await pool2.query(
- `SELECT 1 FROM h5_user_sessions WHERE agent_session_id = ? AND user_id = ? LIMIT 1`,
- [agentSessionId, userId]
- );
- return rows.length > 0;
- };
- const listOwnedSessionIds = async (userId) => {
- const [rows] = await pool2.query(
- `SELECT agent_session_id FROM h5_user_sessions WHERE user_id = ?`,
- [userId]
- );
- return new Set(rows.map((row) => row.agent_session_id));
- };
- const setSessionOrigin = async (agentSessionId, origin) => {
- if (!agentSessionId || origin !== "h5" && origin !== "wechat") return;
- await pool2.query(
- `UPDATE h5_user_sessions SET origin = ? WHERE agent_session_id = ?`,
- [origin, agentSessionId]
- );
- };
- const getSessionOrigins = async (agentSessionIds = []) => {
- const ids = [...new Set(agentSessionIds)].filter(Boolean);
- if (ids.length === 0) return /* @__PURE__ */ new Map();
- const [rows] = await pool2.query(
- `SELECT agent_session_id, origin FROM h5_user_sessions WHERE agent_session_id IN (?)`,
- [ids]
- );
- return new Map(rows.map((row) => [row.agent_session_id, row.origin]));
- };
- const unregisterAgentSession = async (userId, agentSessionId) => {
- await pool2.query(
- `DELETE FROM h5_user_sessions WHERE agent_session_id = ? AND user_id = ?`,
- [agentSessionId, userId]
- );
- };
- const canUseChat = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528" };
- }
- if (user.status === "suspended" && !isAdminRole(user)) {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u6682\u505C" };
- }
- if (isAdminRole(user)) {
- return { ok: true, balanceCents: Number(user.balance_cents ?? 0) };
- }
- if (subscriptionService) {
- const sub = await subscriptionService.getActiveSubscription(userId);
- if (sub) {
- const unlimited = sub.periodTokensLimit === 0;
- const hasQuota = unlimited || sub.periodTokensUsed < sub.periodTokensLimit;
- const balanceCents2 = Number(user.balance_cents ?? 0);
- if (hasQuota) {
- return { ok: true, balanceCents: balanceCents2, subscription: sub };
- }
- if (balanceCents2 > 0) {
- return { ok: true, balanceCents: balanceCents2, subscription: sub, overQuota: true };
- }
- return {
- ok: false,
- message: "\u672C\u6708\u989D\u5EA6\u5DF2\u7528\u5B8C\uFF0C\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u6216\u5347\u7EA7\u5957\u9910",
- ...buildInsufficientBalancePayload(balanceCents2, loadRechargeConfig())
- };
- }
- }
- const balanceCents = Number(user.balance_cents ?? 0);
- if (balanceCents <= 0) {
- return {
- ok: false,
- message: "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u7EE7\u7EED\u4F7F\u7528",
- ...buildInsufficientBalancePayload(balanceCents, loadRechargeConfig())
- };
- }
- return { ok: true, balanceCents };
- };
- const listUsers = async ({ page = 1, pageSize = 20, search = "", role = "", status = "" } = {}) => {
- const safePageSize = Math.min(Math.max(Number(pageSize) || 20, 1), 100);
- const safePage = Math.max(Number(page) || 1, 1);
- const offset = (safePage - 1) * safePageSize;
- const params = [];
- const clauses = [];
- if (search) {
- clauses.push("(u.username LIKE ? OR u.display_name LIKE ?)");
- params.push(`%${search}%`, `%${search}%`);
- }
- if (role) {
- clauses.push("u.role = ?");
- params.push(role);
- }
- if (status) {
- clauses.push("u.status = ?");
- params.push(status);
- }
- const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
- const [[{ total }]] = await pool2.query(
- `SELECT COUNT(*) AS total FROM h5_users u ${where}`,
- params
- );
- const [rows] = await pool2.query(
- `SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
- u.plan_type, u.workspace_root,
- s.quota_bytes, s.used_bytes, s.reserved_bytes,
- u.created_at, u.updated_at, w.balance_cents, w.tokens_used
- FROM h5_users u
- LEFT JOIN h5_user_spaces s ON s.user_id = u.id
- LEFT JOIN h5_user_wallets w ON w.user_id = u.id
- ${where}
- ORDER BY u.created_at DESC
- LIMIT ${safePageSize} OFFSET ${offset}`,
- params
- );
- return {
- users: rows.map((row) => ({ ...publicUser(row), createdAt: Number(row.created_at), updatedAt: Number(row.updated_at) })),
- total: Number(total),
- page: safePage,
- pageSize: safePageSize
- };
- };
- const getUserPublic = async (userId) => {
- const user = await getUserById(userId);
- return user ? publicUser(user) : null;
- };
- const createUser = async ({
- username,
- password,
- displayName,
- workspaceRoot,
- balanceCents,
- role = "user",
- email
- }) => {
- const normalized = normalizeUsername(username);
- if (!isValidUsername(normalized)) {
- return { ok: false, message: "\u7528\u6237\u540D\u683C\u5F0F\u65E0\u6548" };
- }
- if (!password || password.length < 6) {
- return { ok: false, message: "\u5BC6\u7801\u81F3\u5C11 6 \u4F4D" };
- }
- const isAdmin = role === "admin";
- const userId = crypto5.randomUUID();
- const root2 = (await publishLayoutFor({ id: userId, username: normalized })).publishDir;
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
- const now = Date.now();
- const conn = await pool2.getConnection();
- const initialBalanceCents = Number(balanceCents ?? defaultSignupBalanceCents);
- try {
- await conn.beginTransaction();
- await conn.query(
- `INSERT INTO h5_users
- (id, username, slug, email, display_name, salt, password_hash, password_algorithm,
- role, status, plan_type, workspace_root, low_balance_gift_eligible, low_balance_gift_granted_at,
- created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 'free', ?, ?, NULL, ?, ?)`,
- [
- userId,
- normalized,
- normalized,
- email?.trim().toLowerCase() || null,
- displayName?.trim() || normalized,
- salt,
- passwordHash,
- passwordAlgorithm,
- isAdmin ? "admin" : "user",
- root2,
- shouldEnableLowBalanceGift({ isAdmin, initialBalanceCents }) ? 1 : 0,
- now,
- now
- ]
- );
- await conn.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)`,
- [userId, initialBalanceCents, now]
- );
- await conn.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [userId, root2]
- );
- if (!isAdmin) {
- await initializeDefaultSpace(conn, userId, {
- quotaBytes: Number(process.env.MINDSPACE_FREE_QUOTA_BYTES ?? 5 * 1024 * 1024),
- now
- });
- }
- await conn.commit();
- ensureWorkspace(root2);
- if (subscriptionService && !isAdmin) {
- subscriptionService.grantSubscription(userId, "free", null, null, "\u6CE8\u518C\u8D60\u9001\u514D\u8D39\u5957\u9910").catch(() => {
- });
- }
- const user = await getUserById(userId);
- return { ok: true, user: publicUser(user) };
- } catch (err) {
- await conn.rollback();
- if (err?.code === "ER_DUP_ENTRY") {
- return { ok: false, message: "\u7528\u6237\u540D\u5DF2\u5B58\u5728" };
- }
- throw err;
- } finally {
- conn.release();
- }
- };
- const updateUser = async (userId, patch) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const now = Date.now();
- const fields = [];
- const values = [];
- if (patch.displayName !== void 0) {
- fields.push("display_name = ?");
- values.push(patch.displayName.trim() || user.username);
- }
- if (patch.status !== void 0) {
- fields.push("status = ?");
- values.push(patch.status);
- }
- if (patch.workspaceRoot !== void 0) {
- const root2 = path11.resolve(patch.workspaceRoot);
- fields.push("workspace_root = ?");
- values.push(root2);
- ensureWorkspace(root2);
- await pool2.query(`DELETE FROM h5_user_path_grants WHERE user_id = ?`, [userId]);
- await pool2.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [userId, root2]
- );
- }
- if (patch.role !== void 0) {
- fields.push("role = ?");
- values.push(patch.role === "admin" ? "admin" : "user");
- }
- if (fields.length > 0) {
- fields.push("updated_at = ?");
- values.push(now, userId);
- await pool2.query(`UPDATE h5_users SET ${fields.join(", ")} WHERE id = ?`, values);
- }
- if (patch.status === "disabled" || patch.status === "suspended") {
- await revokeAllSessionsForUser(userId, now);
- }
- if (patch.balanceCents !== void 0) {
- await pool2.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)
- ON DUPLICATE KEY UPDATE balance_cents = VALUES(balance_cents), updated_at = VALUES(updated_at)`,
- [userId, Number(patch.balanceCents), now]
- );
- }
- if (patch.spaceQuotaBytes !== void 0) {
- const quotaBytes = Math.floor(Number(patch.spaceQuotaBytes));
- if (!Number.isFinite(quotaBytes) || quotaBytes <= 0) {
- return { ok: false, message: "\u7A7A\u95F4\u5927\u5C0F\u65E0\u6548" };
- }
- const [spaceRows] = await pool2.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes
- FROM h5_user_spaces
- WHERE user_id = ?
- LIMIT 1`,
- [userId]
- );
- const currentSpace = spaceRows[0];
- const occupiedBytes = Number(currentSpace?.used_bytes ?? 0) + Number(currentSpace?.reserved_bytes ?? 0);
- if (quotaBytes < occupiedBytes) {
- return {
- ok: false,
- message: `\u7A7A\u95F4\u4E0D\u80FD\u5C0F\u4E8E\u5DF2\u4F7F\u7528\u5BB9\u91CF ${Math.ceil(occupiedBytes / 1024 / 1024)} MB`
- };
- }
- if (currentSpace?.id) {
- await pool2.query(
- `UPDATE h5_user_spaces
- SET quota_bytes = ?, updated_at = ?
- WHERE user_id = ?`,
- [quotaBytes, now, userId]
- );
- } else {
- await initializeDefaultSpace(pool2, userId, {
- quotaBytes,
- now
- });
- }
- }
- const updated = await getUserById(userId);
- return { ok: true, user: publicUser(updated) };
- };
- const purchaseSpaceQuota = async (userId, sizeMb) => {
- const purchaseMb = Math.floor(Number(sizeMb));
- if (!Number.isFinite(purchaseMb) || purchaseMb <= 0) {
- return { ok: false, message: "\u6269\u5BB9\u5927\u5C0F\u65E0\u6548" };
- }
- const deltaBytes = purchaseMb * 1024 * 1024;
- const costCents = purchaseMb * 200;
- const now = Date.now();
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [spaceRows] = await conn.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes
- FROM h5_user_spaces
- WHERE user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [userId]
- );
- if (!spaceRows[0]) {
- await initializeDefaultSpace(conn, userId, { now });
- }
- const [walletRows] = await conn.query(
- `SELECT balance_cents
- FROM h5_user_wallets
- WHERE user_id = ?
- FOR UPDATE`,
- [userId]
- );
- const balanceCents = Number(walletRows[0]?.balance_cents ?? 0);
- if (balanceCents < costCents) {
- await conn.rollback();
- return {
- ok: false,
- code: "INSUFFICIENT_BALANCE",
- message: "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5148\u5145\u503C\u540E\u518D\u8D2D\u4E70\u7A7A\u95F4",
- balanceCents,
- minRechargeCents: Math.max(500, costCents - balanceCents),
- suggestedTiers: loadRechargeConfig().tiersCents
- };
- }
- await conn.query(
- `UPDATE h5_user_wallets
- SET balance_cents = balance_cents - ?, updated_at = ?
- WHERE user_id = ?`,
- [costCents, now, userId]
- );
- await conn.query(
- `UPDATE h5_user_spaces
- SET quota_bytes = quota_bytes + ?, updated_at = ?
- WHERE user_id = ?`,
- [deltaBytes, now, userId]
- );
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, note, operator_id, created_at)
- VALUES (?, 'deduct', ?, 0, ?, NULL, ?)`,
- [userId, costCents, `space_purchase:${purchaseMb}MB`, now]
- );
- await conn.query(
- `INSERT INTO h5_user_notifications
- (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
- VALUES (?, ?, 'web', 'space_purchase', ?, ?, ?, 'unread', NULL, ?, ?)`,
- [
- 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`,
- JSON.stringify({ purchaseMb, deltaBytes, costCents }),
- now,
- now
- ]
- );
- await conn.commit();
- const [updatedSpaceRows] = await pool2.query(
- `SELECT quota_bytes, used_bytes, reserved_bytes
- FROM h5_user_spaces
- WHERE user_id = ?
- LIMIT 1`,
- [userId]
- );
- const updatedSpace = updatedSpaceRows[0] ?? {};
- const updatedUser = await getUserById(userId);
- return {
- ok: true,
- balanceCents: Number(updatedUser?.balance_cents ?? Math.max(0, balanceCents - costCents)),
- quota: {
- quotaBytes: Number(updatedSpace.quota_bytes ?? 0),
- usedBytes: Number(updatedSpace.used_bytes ?? 0),
- reservedBytes: Number(updatedSpace.reserved_bytes ?? 0),
- availableBytes: Math.max(
- 0,
- Number(updatedSpace.quota_bytes ?? 0) - Number(updatedSpace.used_bytes ?? 0) - Number(updatedSpace.reserved_bytes ?? 0)
- )
- }
- };
- } catch (err) {
- await conn.rollback();
- throw err;
- } finally {
- conn.release();
- }
- };
- const recharge = async (userId, amountCents, operatorId, note = "", options2 = {}) => {
- const amount = Number(amountCents);
- if (!Number.isFinite(amount) || amount <= 0) {
- return { ok: false, message: "\u5145\u503C\u91D1\u989D\u65E0\u6548" };
- }
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const paymentOrderId = options2.paymentOrderId ?? null;
- const ledgerNote = paymentOrderId ? `order:${paymentOrderId}` : note || (operatorId ? "\u7BA1\u7406\u5458\u5145\u503C" : "\u8D26\u6237\u5145\u503C");
- const rechargeType = paymentOrderId ? "self_recharge" : operatorId ? "admin_recharge" : "recharge";
- const now = Date.now();
- const ownsConnection = !options2.conn;
- const conn = options2.conn ?? await pool2.getConnection();
- try {
- if (ownsConnection) await conn.beginTransaction();
- await conn.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)
- ON DUPLICATE KEY UPDATE
- balance_cents = balance_cents + VALUES(balance_cents),
- updated_at = VALUES(updated_at)`,
- [userId, amount, now]
- );
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, note, operator_id, created_at)
- VALUES (?, 'recharge', ?, 0, ?, ?, ?)`,
- [userId, amount, ledgerNote, operatorId, now]
- );
- const title = paymentOrderId ? "\u5145\u503C\u6210\u529F" : operatorId ? "\u7BA1\u7406\u5458\u5DF2\u5145\u503C" : "\u8D26\u6237\u5145\u503C\u6210\u529F";
- const body = paymentOrderId ? `\u4F60\u5DF2\u6210\u529F\u5145\u503C \xA5${(amount / 100).toFixed(2)}\uFF0C\u4F59\u989D\u5DF2\u66F4\u65B0\u3002` : operatorId ? `\u7BA1\u7406\u5458\u5DF2\u4E3A\u4F60\u5145\u503C \xA5${(amount / 100).toFixed(2)}\uFF0C\u4F59\u989D\u5DF2\u66F4\u65B0\u3002` : `\u4F60\u7684\u8D26\u6237\u5DF2\u5145\u503C \xA5${(amount / 100).toFixed(2)}\uFF0C\u4F59\u989D\u5DF2\u66F4\u65B0\u3002`;
- await conn.query(
- `INSERT INTO h5_user_notifications
- (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
- VALUES (?, ?, 'web', ?, ?, ?, ?, 'unread', NULL, ?, ?)`,
- [
- crypto5.randomUUID(),
- userId,
- rechargeType,
- title,
- body,
- JSON.stringify({
- amountCents: amount,
- operatorId: operatorId ?? null,
- paymentOrderId
- }),
- now,
- now
- ]
- );
- if (user.status === "suspended") {
- await conn.query(`UPDATE h5_users SET status = 'active', updated_at = ? WHERE id = ?`, [
- now,
- userId
- ]);
- }
- if (ownsConnection) await conn.commit();
- const updated = await getUserById(userId);
- if (rechargeNotifier) {
- try {
- await rechargeNotifier({
- userId,
- amountCents: amount,
- operatorId: operatorId ?? null,
- paymentOrderId,
- dedupeKey: paymentOrderId ? `recharge:${paymentOrderId}` : null,
- notificationType: rechargeType,
- title,
- body,
- user: publicUser(updated)
- });
- } catch (err) {
- console.warn(
- "Recharge notifier failed:",
- err instanceof Error ? err.message : String(err)
- );
- }
- }
- return { ok: true, user: publicUser(updated) };
- } catch (err) {
- if (ownsConnection) await conn.rollback();
- throw err;
- } finally {
- if (ownsConnection) conn.release();
- }
- };
- const getBillingState = async (agentSessionId) => {
- const [rows] = await pool2.query(
- `SELECT agent_session_id, user_id, last_accumulated_cost, last_input_tokens,
- last_output_tokens, updated_at
- FROM h5_session_billing_state
- WHERE agent_session_id = ?`,
- [agentSessionId]
- );
- const row = rows[0];
- if (!row) return null;
- return {
- agentSessionId: row.agent_session_id,
- userId: row.user_id,
- lastAccumulatedCost: row.last_accumulated_cost,
- lastInputTokens: Number(row.last_input_tokens ?? 0),
- lastOutputTokens: Number(row.last_output_tokens ?? 0),
- updatedAt: Number(row.updated_at)
- };
- };
- const billSessionUsage = async (userId, agentSessionId, tokenStateRaw, requestId = null) => {
- const user = await getUserById(userId);
- if (isAdminRole(user)) {
- return {
- ok: true,
- costCents: 0,
- balanceCents: Number(user?.balance_cents ?? 0),
- tokensUsed: Number(user?.tokens_used ?? 0),
- deltaInputTokens: 0,
- deltaOutputTokens: 0
- };
- }
- const tokenState = normalizeTokenState(tokenStateRaw);
- const config = loadBillingConfig();
- const normalizedRequestId = requestId ? String(requestId).trim() || null : null;
- const now = Date.now();
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- if (normalizedRequestId) {
- const [existingUsage] = await conn.query(
- `SELECT cost_cents FROM h5_usage_records WHERE request_id = ? LIMIT 1`,
- [normalizedRequestId]
- );
- if (existingUsage[0]) {
- const [walletRows] = await conn.query(
- `SELECT balance_cents, tokens_used FROM h5_user_wallets WHERE user_id = ?`,
- [userId]
- );
- await conn.commit();
- return {
- ok: true,
- costCents: 0,
- balanceCents: walletRows[0] ? Number(walletRows[0].balance_cents) : null,
- tokensUsed: walletRows[0] ? Number(walletRows[0].tokens_used ?? 0) : null,
- deltaInputTokens: 0,
- deltaOutputTokens: 0
- };
- }
- }
- await conn.query(
- `INSERT INTO h5_session_billing_state
- (agent_session_id, user_id, last_accumulated_cost, last_input_tokens, last_output_tokens, updated_at)
- VALUES (?, ?, NULL, 0, 0, ?)
- ON DUPLICATE KEY UPDATE agent_session_id = agent_session_id`,
- [agentSessionId, userId, now]
- );
- const [stateRows] = await conn.query(
- `SELECT last_accumulated_cost, last_input_tokens, last_output_tokens
- FROM h5_session_billing_state
- WHERE agent_session_id = ?
- FOR UPDATE`,
- [agentSessionId]
- );
- const stateRow = stateRows[0];
- const previous = stateRow ? {
- lastAccumulatedCost: stateRow.last_accumulated_cost,
- lastInputTokens: Number(stateRow.last_input_tokens ?? 0),
- lastOutputTokens: Number(stateRow.last_output_tokens ?? 0)
- } : null;
- if (previous && tokenState.accumulatedInputTokens <= Number(previous.lastInputTokens ?? 0) && tokenState.accumulatedOutputTokens <= Number(previous.lastOutputTokens ?? 0)) {
- const [walletRows] = await conn.query(
- `SELECT balance_cents, tokens_used FROM h5_user_wallets WHERE user_id = ?`,
- [userId]
- );
- await conn.commit();
- return {
- ok: true,
- costCents: 0,
- balanceCents: walletRows[0] ? Number(walletRows[0].balance_cents) : null,
- tokensUsed: walletRows[0] ? Number(walletRows[0].tokens_used ?? 0) : null,
- deltaInputTokens: 0,
- deltaOutputTokens: 0
- };
- }
- let costCents = computeDeltaCostCents(previous, tokenState, config);
- const deltaIn = Math.max(
- 0,
- tokenState.accumulatedInputTokens - Number(previous?.lastInputTokens ?? 0)
- );
- const deltaOut = Math.max(
- 0,
- tokenState.accumulatedOutputTokens - Number(previous?.lastOutputTokens ?? 0)
- );
- const deltaTokens = deltaIn + deltaOut;
- await conn.query(
- `INSERT INTO h5_session_billing_state
- (agent_session_id, user_id, last_accumulated_cost, last_input_tokens, last_output_tokens, updated_at)
- VALUES (?, ?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- last_accumulated_cost = VALUES(last_accumulated_cost),
- last_input_tokens = VALUES(last_input_tokens),
- last_output_tokens = VALUES(last_output_tokens),
- updated_at = VALUES(updated_at)`,
- [
- agentSessionId,
- userId,
- tokenState.accumulatedCost,
- tokenState.accumulatedInputTokens,
- tokenState.accumulatedOutputTokens,
- now
- ]
- );
- if (costCents > 0 && subscriptionService) {
- const coverage = await subscriptionService.consumeQuota(userId, deltaTokens, conn);
- if (coverage.fullyCovers) {
- costCents = 0;
- } else if (coverage.overageRate < 1) {
- costCents = Math.max(1, Math.ceil(costCents * coverage.overageRate));
- }
- }
- let balanceAfter = null;
- let tokensUsedAfter = null;
- if (costCents > 0) {
- const [walletRows] = await conn.query(
- `SELECT w.balance_cents, w.tokens_used, u.status
- FROM h5_user_wallets w
- JOIN h5_users u ON u.id = w.user_id
- WHERE w.user_id = ?
- FOR UPDATE`,
- [userId]
- );
- const wallet = walletRows[0];
- if (!wallet) {
- await conn.rollback();
- return { ok: false, message: "\u94B1\u5305\u4E0D\u5B58\u5728", costCents: 0 };
- }
- const currentBalance = Number(wallet.balance_cents ?? 0);
- const nextBalance = Math.max(0, currentBalance - costCents);
- balanceAfter = nextBalance;
- tokensUsedAfter = Number(wallet.tokens_used ?? 0) + deltaTokens;
- await conn.query(
- `UPDATE h5_user_wallets
- SET balance_cents = ?, tokens_used = tokens_used + ?, updated_at = ?
- WHERE user_id = ?`,
- [nextBalance, deltaTokens, now, userId]
- );
- await conn.query(
- `INSERT INTO h5_usage_records
- (user_id, agent_session_id, request_id, input_tokens, output_tokens, cost_cents, balance_after_cents, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- userId,
- agentSessionId,
- normalizedRequestId,
- deltaIn,
- deltaOut,
- costCents,
- nextBalance,
- now
- ]
- );
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, session_id, note, operator_id, created_at)
- VALUES (?, 'deduct', ?, ?, ?, ?, NULL, ?)`,
- [
- userId,
- -costCents,
- deltaTokens,
- agentSessionId,
- `\u5BF9\u8BDD\u6263\u8D39 input=${deltaIn} output=${deltaOut}`,
- now
- ]
- );
- const lowBalanceGift = await grantLowBalanceGiftIfNeeded(conn, {
- userId,
- currentBalance,
- nextBalance,
- now
- });
- balanceAfter = lowBalanceGift.balanceAfter;
- if (balanceAfter <= 0) {
- await conn.query(`UPDATE h5_users SET status = 'suspended', updated_at = ? WHERE id = ?`, [
- now,
- userId
- ]);
- }
- } else {
- const user2 = await getUserById(userId);
- balanceAfter = user2 ? Number(user2.balance_cents) : null;
- tokensUsedAfter = user2 ? Number(user2.tokens_used ?? 0) : null;
- }
- await conn.commit();
- return {
- ok: true,
- costCents,
- balanceCents: balanceAfter,
- tokensUsed: tokensUsedAfter,
- deltaInputTokens: deltaIn,
- deltaOutputTokens: deltaOut
- };
- } catch (err) {
- await conn.rollback();
- throw err;
- } finally {
- conn.release();
- }
- };
- const listUsageRecords = async ({ userId = null, page = 1, pageSize = 20, limit = null } = {}) => {
- if (limit !== null) {
- const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
- const params2 = [];
- const where2 = userId ? "WHERE r.user_id = ?" : "";
- if (userId) params2.push(userId);
- const [rows2] = await pool2.query(
- `SELECT r.id, r.user_id, u.username, r.agent_session_id, r.request_id,
- r.input_tokens, r.output_tokens, r.cost_cents, r.balance_after_cents, r.created_at
- FROM h5_usage_records r JOIN h5_users u ON u.id = r.user_id
- ${where2} ORDER BY r.created_at DESC LIMIT ${safeLimit}`,
- params2
- );
- return rows2.map((row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, agentSessionId: row.agent_session_id, requestId: row.request_id, inputTokens: Number(row.input_tokens), outputTokens: Number(row.output_tokens), costCents: Number(row.cost_cents), balanceAfterCents: Number(row.balance_after_cents), createdAt: Number(row.created_at) }));
- }
- const safePageSize = Math.min(Math.max(Number(pageSize) || 50, 1), 200);
- const safePage = Math.max(Number(page) || 1, 1);
- const offset = (safePage - 1) * safePageSize;
- const params = [];
- const where = userId ? "WHERE r.user_id = ?" : "";
- if (userId) params.push(userId);
- const [[{ total }]] = await pool2.query(
- `SELECT COUNT(*) AS total FROM h5_usage_records r ${where}`,
- params
- );
- const [rows] = await pool2.query(
- `SELECT r.id, r.user_id, u.username, r.agent_session_id, r.request_id,
- r.input_tokens, r.output_tokens, r.cost_cents, r.balance_after_cents, r.created_at
- FROM h5_usage_records r JOIN h5_users u ON u.id = r.user_id
- ${where}
- ORDER BY r.created_at DESC
- LIMIT ${safePageSize} OFFSET ${offset}`,
- params
- );
- return {
- records: rows.map((row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, agentSessionId: row.agent_session_id, requestId: row.request_id, inputTokens: Number(row.input_tokens), outputTokens: Number(row.output_tokens), costCents: Number(row.cost_cents), balanceAfterCents: Number(row.balance_after_cents), createdAt: Number(row.created_at) })),
- total: Number(total),
- page: safePage,
- pageSize: safePageSize
- };
- };
- const listBillingLedger = async ({ userId = null, page = 1, pageSize = 20, limit = null, types = null } = {}) => {
- const buildWhere = (params) => {
- const clauses = [];
- if (userId) {
- clauses.push("l.user_id = ?");
- params.push(userId);
- }
- if (Array.isArray(types) && types.length) {
- clauses.push(`l.type IN (${types.map(() => "?").join(", ")})`);
- params.push(...types);
- }
- return clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
- };
- const mapRow = (row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, type: row.type, amountCents: Number(row.amount_cents), tokens: Number(row.tokens), sessionId: row.session_id, note: row.note, createdAt: Number(row.created_at) });
- if (limit !== null) {
- const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
- const params = [];
- const where2 = buildWhere(params);
- const [rows2] = await pool2.query(`SELECT l.id, l.user_id, u.username, l.type, l.amount_cents, l.tokens, l.session_id, l.note, l.created_at FROM h5_billing_ledger l JOIN h5_users u ON u.id = l.user_id ${where2} ORDER BY l.created_at DESC LIMIT ${safeLimit}`, params);
- return rows2.map(mapRow);
- }
- const safePageSize = Math.min(Math.max(Number(pageSize) || 50, 1), 200);
- const safePage = Math.max(Number(page) || 1, 1);
- const offset = (safePage - 1) * safePageSize;
- const countParams = [];
- const where = buildWhere(countParams);
- const [[{ total }]] = await pool2.query(
- `SELECT COUNT(*) AS total FROM h5_billing_ledger l ${where}`,
- countParams
- );
- const dataParams = [];
- const dataWhere = buildWhere(dataParams);
- const [rows] = await pool2.query(
- `SELECT l.id, l.user_id, u.username, l.type, l.amount_cents, l.tokens,
- l.session_id, l.note, l.created_at
- FROM h5_billing_ledger l JOIN h5_users u ON u.id = l.user_id
- ${dataWhere}
- ORDER BY l.created_at DESC
- LIMIT ${safePageSize} OFFSET ${offset}`,
- dataParams
- );
- return { entries: rows.map(mapRow), total: Number(total), page: safePage, pageSize: safePageSize };
- };
- const getAdminSummary = async () => {
- const since24h = Date.now() - 24 * 60 * 60 * 1e3;
- const [userRows] = await pool2.query(
- `SELECT u.id, u.username, u.display_name, u.role, u.status,
- COALESCE(w.balance_cents, 0) AS balance_cents
- FROM h5_users u
- LEFT JOIN h5_user_wallets w ON w.user_id = u.id`
- );
- let total = 0;
- let active = 0;
- let lowBalance = 0;
- let totalBalanceCents = 0;
- const lowBalanceUsers = [];
- for (const row of userRows) {
- total += 1;
- if (row.status === "active") active += 1;
- const balanceCents = Number(row.balance_cents);
- totalBalanceCents += balanceCents;
- if (row.role === "user" && balanceCents <= 0) {
- lowBalance += 1;
- if (lowBalanceUsers.length < 8) {
- lowBalanceUsers.push({
- id: row.id,
- username: row.username,
- displayName: row.display_name,
- balanceCents
- });
- }
- }
- }
- const [[usage24h]] = await pool2.query(
- `SELECT COUNT(*) AS count, COALESCE(SUM(cost_cents), 0) AS cost_cents
- FROM h5_usage_records
- WHERE created_at >= ?`,
- [since24h]
- );
- const recentUsage = await listUsageRecords({ limit: 8 });
- const recentLedger = await listBillingLedger({ limit: 8 });
- return {
- users: { total, active, lowBalance, totalBalanceCents },
- usage24h: {
- count: Number(usage24h.count),
- costCents: Number(usage24h.cost_cents)
- },
- lowBalanceUsers,
- recentUsage,
- recentLedger
- };
- };
- const syncAdminPassword = async () => {
- const adminUsername = normalizeUsername(process.env.H5_ADMIN_USERNAME ?? "admin");
- const adminPassword = process.env.H5_ADMIN_PASSWORD;
- if (!adminPassword) return;
- const [rows] = await pool2.query(
- `SELECT id FROM h5_users WHERE username = ? AND role = 'admin' LIMIT 1`,
- [adminUsername]
- );
- if (rows.length === 0) return;
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(adminPassword);
- const now = Date.now();
- await pool2.query(
- `UPDATE h5_users SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ? WHERE id = ?`,
- [salt, passwordHash, passwordAlgorithm, now, rows[0].id]
- );
- };
- const seedRoleCapabilityDefaults = async () => {
- const now = Date.now();
- for (const [key, allowed] of Object.entries(DEFAULT_USER_CAPABILITIES)) {
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('role', 'user', ?, ?, ?)
- ON DUPLICATE KEY UPDATE capability_key = capability_key`,
- [key, allowed ? 1 : 0, now]
- );
- }
- };
- const upgradeMemoryStoreCapability = async () => {
- const now = Date.now();
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('role', 'user', 'memory_store', 1, ?)
- ON DUPLICATE KEY UPDATE allowed = 1, updated_at = VALUES(updated_at)`,
- [now]
- );
- };
- const upgradeDefaultUserCapabilities = async () => {
- const now = Date.now();
- for (const key of ["skills", "chat_recall"]) {
- if (!DEFAULT_USER_CAPABILITIES[key]) continue;
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('role', 'user', ?, 1, ?)
- ON DUPLICATE KEY UPDATE allowed = 1, updated_at = VALUES(updated_at)`,
- [key, now]
- );
- }
- };
- const upgradeDefaultUserSkills = async () => {
- const now = Date.now();
- for (const [name, enabled] of Object.entries(DEFAULT_USER_SKILLS)) {
- if (!enabled) continue;
- await pool2.query(
- `INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
- VALUES ('role', 'user', ?, 1, ?)
- ON DUPLICATE KEY UPDATE enabled = 1, updated_at = VALUES(updated_at)`,
- [name, now]
- );
- }
- };
- const serializePolicyValue = (key, value) => {
- const def = POLICY_CATALOG.find((item) => item.key === key);
- if (def?.type === "boolean") return value ? "true" : "false";
- return String(value);
- };
- const parsePolicyValue = (key, raw) => {
- const def = POLICY_CATALOG.find((item) => item.key === key);
- if (def?.type === "boolean") return raw === "true" || raw === "1";
- return raw;
- };
- const seedRolePolicyDefaults = async () => {
- const now = Date.now();
- for (const [key, value] of Object.entries(DEFAULT_USER_POLICIES)) {
- await pool2.query(
- `INSERT INTO h5_user_policies (subject_type, subject_id, policy_key, policy_value, updated_at)
- VALUES ('role', 'user', ?, ?, ?)
- ON DUPLICATE KEY UPDATE policy_key = policy_key`,
- [key, serializePolicyValue(key, value), now]
- );
- }
- };
- const listPolicyEntries = async (subjectType, subjectId) => {
- const [rows] = await pool2.query(
- `SELECT policy_key, policy_value
- FROM h5_user_policies
- WHERE subject_type = ? AND subject_id = ?`,
- [subjectType, subjectId]
- );
- return Object.fromEntries(
- rows.map((row) => [row.policy_key, parsePolicyValue(row.policy_key, row.policy_value)])
- );
- };
- const resolveUserPolicies = async (user) => {
- if (!user || user.role === "admin") {
- return { unrestricted: true, policies: {} };
- }
- const roleDefaults = await listPolicyEntries("role", "user");
- const userOverrides = await listPolicyEntries("user", user.id);
- return {
- unrestricted: false,
- policies: resolvePolicies(roleDefaults, userOverrides)
- };
- };
- const listCapabilityGrants = async (subjectType, subjectId) => {
- const [rows] = await pool2.query(
- `SELECT capability_key, allowed
- FROM h5_capability_grants
- WHERE subject_type = ? AND subject_id = ?`,
- [subjectType, subjectId]
- );
- return Object.fromEntries(
- rows.map((row) => [row.capability_key, Boolean(row.allowed)])
- );
- };
- const resolveUserCapabilities = async (user) => {
- if (!user) return { unrestricted: true, capabilities: {} };
- if (user.role === "admin") {
- const skillMap2 = Object.fromEntries(skillCatalog.map((item) => [item.name, true]));
- return {
- unrestricted: true,
- capabilities: Object.fromEntries(catalogKeys().map((key) => [key, true])),
- skills: skillMap2,
- grantedSkills: grantedSkillNames(skillMap2)
- };
- }
- const roleDefaults = await listCapabilityGrants("role", "user");
- const userOverrides = await listCapabilityGrants("user", user.id);
- const capabilities = {};
- for (const key of catalogKeys()) {
- if (key in userOverrides) {
- capabilities[key] = userOverrides[key];
- } else if (key in roleDefaults) {
- capabilities[key] = roleDefaults[key];
- } else {
- capabilities[key] = DEFAULT_USER_CAPABILITIES[key] ?? false;
- }
- }
- const skillMap = await resolveUserSkillMap(user);
- const withSkills = applySkillGrantsToCapabilities(capabilities, skillMap);
- return {
- unrestricted: false,
- capabilities: clampUserCapabilities(withSkills),
- skills: skillMap,
- grantedSkills: grantedSkillNames(skillMap)
- };
- };
- const getAgentSessionPolicy = async (userId, { toolMode = "chat" } = {}) => {
- const user = await getUserById(userId);
- if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728");
- const capabilityState = await resolveUserCapabilities(user);
- const policyState = await resolveUserPolicies(user);
- await syncUserSkillsForUser(user);
- if (capabilityState.unrestricted) {
- return {
- ...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true, toolMode }),
- policies: {},
- unrestricted: true,
- toolMode
- };
- }
- const effectiveCapabilities = applyPoliciesToCapabilities(
- capabilityState.capabilities,
- policyState.policies
- );
- let sandboxMcp = null;
- if (effectiveCapabilities.static_publish || effectiveCapabilities.private_data_space) {
- try {
- const layout = await publishLayoutFor(user, { migrateLegacy: false });
- sandboxMcp = {
- // When goosed runs in a container its filesystem is split from the portal's,
- // so the host paths the portal would otherwise send (node binary, MCP script)
- // are not resolvable. These env overrides let the portal send container-canonical
- // paths instead. Unset (e.g. local dev, co-located native goosed) => fall back to
- // the portal's own paths, so behavior is unchanged.
- serverPath: resolveSandboxMcpServerPath(process.env.GOOSED_MCP_SERVER_PATH),
- sandboxRoot: resolveGoosedSandboxRoot(h5Root, user),
- userId: user.id,
- nodeExecPath: process.env.GOOSED_MCP_NODE_PATH
- };
- } catch (err) {
- console.warn("[getAgentSessionPolicy] sandbox MCP setup failed, falling back:", err?.message);
- }
- }
- return {
- ...buildAgentExtensionPolicy(effectiveCapabilities, {
- unrestricted: false,
- policies: policyState.policies,
- sandboxMcp,
- toolMode
- }),
- capabilities: effectiveCapabilities,
- policies: policyState.policies,
- unrestricted: false,
- toolMode
- };
- };
- const getCodeAgentSessionPolicy = async (userId) => getAgentSessionPolicy(userId, { toolMode: "code" });
- const getRoleCapabilities = async (role = "user") => {
- const roleDefaults = await listCapabilityGrants("role", role);
- const capabilities = {};
- for (const key of catalogKeys()) {
- capabilities[key] = key in roleDefaults ? roleDefaults[key] : DEFAULT_USER_CAPABILITIES[key] ?? false;
- }
- return { role, capabilities: clampUserCapabilities(capabilities) };
- };
- const setRoleCapabilities = async (role, patch) => {
- if (role !== "user") {
- return { ok: false, message: "\u4EC5\u652F\u6301\u914D\u7F6E\u666E\u901A\u7528\u6237\u89D2\u8272\u9ED8\u8BA4\u6743\u9650" };
- }
- const normalized = normalizeCapabilityPatch(patch);
- const now = Date.now();
- for (const [key, allowed] of Object.entries(normalized)) {
- const effectiveAllowed = USER_NON_GRANTABLE_CAPABILITIES.has(key) ? false : allowed;
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('role', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE allowed = VALUES(allowed), updated_at = VALUES(updated_at)`,
- [role, key, effectiveAllowed ? 1 : 0, now]
- );
- }
- return { ok: true, ...await getRoleCapabilities(role) };
- };
- const getUserCapabilities = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const resolved = await resolveUserCapabilities(user);
- const overrides = await listCapabilityGrants("user", userId);
- return {
- ok: true,
- userId,
- role: user.role,
- unrestricted: resolved.unrestricted,
- capabilities: resolved.capabilities,
- overrides
- };
- };
- const setUserCapabilities = async (userId, patch) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.role === "admin") {
- return { ok: false, message: "\u7BA1\u7406\u5458\u4E0D\u53D7\u80FD\u529B\u9650\u5236" };
- }
- const normalized = normalizeCapabilityPatch(patch);
- const now = Date.now();
- for (const [key, allowed] of Object.entries(normalized)) {
- if (!isValidCapabilityKey(key)) continue;
- const effectiveAllowed = USER_NON_GRANTABLE_CAPABILITIES.has(key) ? false : allowed;
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('user', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE allowed = VALUES(allowed), updated_at = VALUES(updated_at)`,
- [userId, key, effectiveAllowed ? 1 : 0, now]
- );
- }
- return getUserCapabilities(userId);
- };
- const clearUserCapabilityOverrides = async (userId) => {
- await pool2.query(
- `DELETE FROM h5_capability_grants WHERE subject_type = 'user' AND subject_id = ?`,
- [userId]
- );
- return getUserCapabilities(userId);
- };
- const getRolePolicies = async (role = "user") => {
- const roleDefaults = await listPolicyEntries("role", role);
- const policies = resolvePolicies(roleDefaults, {});
- return { role, policies };
- };
- const setRolePolicies = async (role, patch) => {
- if (role !== "user") {
- return { ok: false, message: "\u4EC5\u652F\u6301\u914D\u7F6E\u666E\u901A\u7528\u6237\u89D2\u8272\u9ED8\u8BA4\u7B56\u7565" };
- }
- const normalized = normalizePolicyPatch(patch);
- const now = Date.now();
- for (const [key, value] of Object.entries(normalized)) {
- await pool2.query(
- `INSERT INTO h5_user_policies (subject_type, subject_id, policy_key, policy_value, updated_at)
- VALUES ('role', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE policy_value = VALUES(policy_value), updated_at = VALUES(updated_at)`,
- [role, key, serializePolicyValue(key, value), now]
- );
- }
- return { ok: true, ...await getRolePolicies(role) };
- };
- const getUserPolicies = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const policyState = await resolveUserPolicies(user);
- const overrides = await listPolicyEntries("user", userId);
- return {
- ok: true,
- userId,
- role: user.role,
- unrestricted: policyState.unrestricted,
- policies: policyState.policies,
- overrides
- };
- };
- const setUserPolicies = async (userId, patch) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.role === "admin") {
- return { ok: false, message: "\u7BA1\u7406\u5458\u4E0D\u53D7\u7B56\u7565\u9650\u5236" };
- }
- const normalized = normalizePolicyPatch(patch);
- const now = Date.now();
- for (const [key, value] of Object.entries(normalized)) {
- if (!policyKeys().includes(key)) continue;
- await pool2.query(
- `INSERT INTO h5_user_policies (subject_type, subject_id, policy_key, policy_value, updated_at)
- VALUES ('user', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE policy_value = VALUES(policy_value), updated_at = VALUES(updated_at)`,
- [userId, key, serializePolicyValue(key, value), now]
- );
- }
- return getUserPolicies(userId);
- };
- const clearUserPolicyOverrides = async (userId) => {
- await pool2.query(`DELETE FROM h5_user_policies WHERE subject_type = 'user' AND subject_id = ?`, [
- userId
- ]);
- return getUserPolicies(userId);
- };
- const getRoleSkills = async (role = "user") => {
- const roleDefaults = await listSkillGrants("role", role);
- const skills = resolveSkillMap(roleDefaults, {}, skillCatalog);
- return { role, skills };
- };
- const setRoleSkills = async (role, patch) => {
- if (role !== "user") {
- return { ok: false, message: "\u4EC5\u652F\u6301\u914D\u7F6E\u666E\u901A\u7528\u6237\u89D2\u8272\u9ED8\u8BA4\u6280\u80FD" };
- }
- const normalized = normalizeSkillPatch(skillCatalog, patch);
- const now = Date.now();
- for (const [name, enabled] of Object.entries(normalized)) {
- await pool2.query(
- `INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
- VALUES ('role', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), updated_at = VALUES(updated_at)`,
- [role, name, enabled ? 1 : 0, now]
- );
- }
- const [users] = await pool2.query(`SELECT id, username, role, workspace_root FROM h5_users WHERE role = 'user'`);
- for (const row of users) {
- await syncUserSkillsForUser(row);
- }
- return { ok: true, ...await getRoleSkills(role) };
- };
- const getUserSkills = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const skills = await resolveUserSkillMap(user);
- const overrides = await listSkillGrants("user", userId);
- return {
- ok: true,
- userId,
- role: user.role,
- skills,
- grantedSkills: grantedSkillNames(skills),
- overrides
- };
- };
- const setUserSkills = async (userId, patch) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.role === "admin") {
- return { ok: false, message: "\u7BA1\u7406\u5458\u4E0D\u53D7\u6280\u80FD\u9650\u5236" };
- }
- const normalized = normalizeSkillPatch(skillCatalog, patch);
- const now = Date.now();
- for (const [name, enabled] of Object.entries(normalized)) {
- await pool2.query(
- `INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
- VALUES ('user', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), updated_at = VALUES(updated_at)`,
- [userId, name, enabled ? 1 : 0, now]
- );
- }
- await syncUserSkillsForUser(user);
- return getUserSkills(userId);
- };
- const clearUserSkillOverrides = async (userId) => {
- await pool2.query(`DELETE FROM h5_user_skill_grants WHERE subject_type = 'user' AND subject_id = ?`, [
- userId
- ]);
- const user = await getUserById(userId);
- if (user) await syncUserSkillsForUser(user);
- return getUserSkills(userId);
- };
- const ensureAdminUser = async () => {
- const adminUsername = normalizeUsername(process.env.H5_ADMIN_USERNAME ?? "admin");
- const adminPassword = process.env.H5_ADMIN_PASSWORD;
- const [rows] = await pool2.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [
- adminUsername
- ]);
- if (rows.length === 0) {
- if (!adminPassword) return;
- await createUser({
- username: adminUsername,
- password: adminPassword,
- displayName: "\u7BA1\u7406\u5458",
- balanceCents: 999999999,
- role: "admin"
- });
- } else {
- const adminId = rows[0].id;
- const now = Date.now();
- const adminLayout = await publishLayoutFor({
- id: adminId,
- username: adminUsername,
- displayName: "\u7BA1\u7406\u5458"
- });
- await pool2.query(`UPDATE h5_users SET workspace_root = ?, updated_at = ? WHERE id = ?`, [
- adminLayout.publishDir,
- now,
- adminId
- ]);
- await pool2.query(`DELETE FROM h5_user_path_grants WHERE user_id = ?`, [adminId]);
- await pool2.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [adminId, adminLayout.publishDir]
- );
- await pool2.query(
- `UPDATE h5_user_wallets SET balance_cents = GREATEST(balance_cents, ?), updated_at = ? WHERE user_id = ?`,
- [999999999, now, adminId]
- );
- }
- if (adminPassword) {
- await syncAdminPassword();
- }
- await seedRoleCapabilityDefaults();
- await upgradeMemoryStoreCapability();
- await upgradeDefaultUserCapabilities();
- await seedRolePolicyDefaults();
- await seedRoleSkillDefaults();
- await upgradeDefaultUserSkills();
- await repairAllUserPublishDirs();
- };
- const PENDING_BIND_TTL_MS = 15 * 60 * 1e3;
- const issueUserSession = async (userId, role, now = Date.now()) => {
- const token = crypto5.randomBytes(32).toString("base64url");
- await storeSession(userId, role, token, now);
- return token;
- };
- const findBindingByOpenid = async (appId, openid) => {
- const [rows] = await pool2.query(
- `SELECT wi.user_id, u.status
- FROM h5_user_wechat_identities wi
- JOIN h5_users u ON u.id = wi.user_id
- WHERE wi.app_id = ? AND wi.openid = ?
- LIMIT 1`,
- [appId, openid]
- );
- return rows[0] ?? null;
- };
- const findBindingByUnionid = async (unionid) => {
- if (!unionid) return null;
- const [rows] = await pool2.query(
- `SELECT wi.user_id, wi.app_id, u.status
- FROM h5_user_wechat_identities wi
- JOIN h5_users u ON u.id = wi.user_id
- WHERE wi.unionid = ?
- LIMIT 1`,
- [unionid]
- );
- return rows[0] ?? null;
- };
- const getWechatBindingForUser = async (userId, appId) => {
- const [rows] = await pool2.query(
- `SELECT id, nickname, avatar_url, last_login_at, created_at
- FROM h5_user_wechat_identities
- WHERE user_id = ? AND app_id = ?
- LIMIT 1`,
- [userId, appId]
- );
- return rows[0] ?? null;
- };
- const getWechatOpenidForUser = async (userId, appId) => {
- const [rows] = await pool2.query(
- `SELECT openid
- FROM h5_user_wechat_identities
- WHERE user_id = ? AND app_id = ?
- LIMIT 1`,
- [userId, appId]
- );
- return rows[0]?.openid ?? null;
- };
- const findWechatUserByOpenid = async (appId, openid) => {
- const [rows] = await pool2.query(
- `SELECT wi.user_id, wi.nickname, u.username, u.slug, u.display_name, u.status
- FROM h5_user_wechat_identities wi
- JOIN h5_users u ON u.id = wi.user_id
- WHERE wi.app_id = ? AND wi.openid = ?
- LIMIT 1`,
- [appId, openid]
- );
- return rows[0] ? {
- userId: rows[0].user_id,
- status: rows[0].status,
- nickname: rows[0].nickname,
- username: rows[0].username,
- slug: rows[0].slug,
- displayName: rows[0].display_name
- } : null;
- };
- const getWechatAgentRoute = async (appId, openid) => {
- const [rows] = await pool2.query(
- `SELECT id, user_id, agent_session_id, status, created_at, updated_at
- FROM h5_wechat_agent_routes
- WHERE app_id = ? AND openid = ?
- LIMIT 1`,
- [appId, openid]
- );
- const row = rows[0];
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- agentSessionId: row.agent_session_id,
- status: row.status,
- createdAt: Number(row.created_at ?? 0),
- updatedAt: Number(row.updated_at ?? 0)
- };
- };
- const upsertWechatAgentRoute = async ({
- userId,
- appId,
- openid,
- agentSessionId,
- status = "active",
- now = Date.now()
- }) => {
- 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)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- user_id = VALUES(user_id),
- agent_session_id = VALUES(agent_session_id),
- status = VALUES(status),
- updated_at = VALUES(updated_at)`,
- [id, userId, appId, openid, agentSessionId, status, now, now]
- );
- const route = await getWechatAgentRoute(appId, openid);
- return route?.id ?? id;
- };
- const clearWechatAgentRoute = async (appId, openid) => {
- await pool2.query(`DELETE FROM h5_wechat_agent_routes WHERE app_id = ? AND openid = ?`, [
- appId,
- openid
- ]);
- };
- const touchWechatAgentRoute = async (appId, openid, now = Date.now()) => {
- if (!appId || !openid) return;
- await pool2.query(
- `UPDATE h5_wechat_agent_routes SET updated_at = ? WHERE app_id = ? AND openid = ?`,
- [now, appId, openid]
- );
- };
- const recordWechatMpMessage = async ({
- appId,
- openid,
- msgId,
- now = Date.now()
- }) => {
- if (!appId || !openid || !msgId) return { inserted: true };
- const [result] = await pool2.query(
- `INSERT IGNORE INTO h5_wechat_mp_messages
- (app_id, openid, msg_id, status, created_at, updated_at)
- VALUES (?, ?, ?, 'processing', ?, ?)`,
- [appId, openid, String(msgId), now, now]
- );
- if (Number(result?.affectedRows ?? 0) > 0) return { inserted: true };
- const retryCutoff = now - 10 * 60 * 1e3;
- const [retryResult] = await pool2.query(
- `UPDATE h5_wechat_mp_messages
- SET status = 'processing', agent_session_id = NULL, updated_at = ?
- WHERE app_id = ? AND openid = ? AND msg_id = ?
- AND (status = 'failed' OR (status = 'processing' AND updated_at < ?))`,
- [now, appId, openid, String(msgId), retryCutoff]
- );
- return {
- inserted: Number(retryResult?.affectedRows ?? 0) > 0,
- duplicate: Number(retryResult?.affectedRows ?? 0) === 0
- };
- };
- const finishWechatMpMessage = async ({
- appId,
- openid,
- msgId,
- status = "done",
- agentSessionId = null,
- now = Date.now()
- }) => {
- if (!appId || !openid || !msgId) return;
- const safeStatus = status === "failed" ? "failed" : "done";
- await pool2.query(
- `UPDATE h5_wechat_mp_messages
- SET status = ?, agent_session_id = COALESCE(?, agent_session_id), updated_at = ?
- WHERE app_id = ? AND openid = ? AND msg_id = ?`,
- [safeStatus, agentSessionId, now, appId, openid, String(msgId)]
- );
- };
- const insertWechatMpMessageDetail = async ({
- appId,
- openid,
- userId = null,
- msgId = null,
- msgType,
- displayText = "",
- agentText = "",
- mediaId = null,
- mediaUrl = null,
- mediaPublicUrl = null,
- mediaFormat = null,
- locationLat = null,
- locationLng = null,
- locationLabel = null,
- linkUrl = null,
- linkTitle = null,
- rawXmlHash = null,
- rawJson = null,
- now = Date.now()
- }) => {
- if (!appId || !openid || !msgType) return null;
- 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,
- media_id, media_url, media_public_url, media_format,
- location_lat, location_lng, location_label,
- link_url, link_title, raw_xml_hash, raw_json, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- id,
- appId,
- openid,
- userId,
- msgId ? String(msgId) : null,
- String(msgType),
- displayText || null,
- agentText || null,
- mediaId,
- mediaUrl,
- mediaPublicUrl,
- mediaFormat,
- locationLat,
- locationLng,
- locationLabel,
- linkUrl,
- linkTitle,
- rawXmlHash,
- rawJson ? JSON.stringify(rawJson) : null,
- now
- ]
- );
- return id;
- };
- const bindWechatToUser = async ({
- userId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now = Date.now()
- }) => {
- const existingOpenid = await findBindingByOpenid(appId, openid);
- if (existingOpenid && existingOpenid.user_id !== userId) {
- return { ok: false, message: "\u8BE5\u5FAE\u4FE1\u5DF2\u7ED1\u5B9A\u5176\u4ED6\u8D26\u53F7\uFF0C\u8BF7\u5148\u7528\u8BE5\u8D26\u53F7\u767B\u5F55" };
- }
- const existingUserBind = await getWechatBindingForUser(userId, appId);
- if (existingUserBind) {
- return { ok: false, message: "\u4F60\u7684\u8D26\u53F7\u5DF2\u7ED1\u5B9A\u5176\u4ED6\u5FAE\u4FE1\uFF0C\u9700\u5148\u89E3\u7ED1\u540E\u518D\u8BD5" };
- }
- try {
- await pool2.query(
- `INSERT INTO h5_user_wechat_identities
- (id, user_id, app_id, openid, unionid, nickname, avatar_url, last_login_at, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- crypto5.randomUUID(),
- userId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now,
- now,
- now
- ]
- );
- return { ok: true };
- } catch (err) {
- if (err?.code === "ER_DUP_ENTRY") {
- return { ok: false, message: "\u5FAE\u4FE1\u7ED1\u5B9A\u51B2\u7A81\uFF0C\u8BF7\u91CD\u8BD5" };
- }
- throw err;
- }
- };
- const touchWechatIdentity = async ({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now = Date.now()
- }) => {
- await pool2.query(
- `UPDATE h5_user_wechat_identities
- SET nickname = COALESCE(?, nickname),
- avatar_url = COALESCE(?, avatar_url),
- unionid = COALESCE(?, unionid),
- last_login_at = ?,
- updated_at = ?
- WHERE app_id = ? AND openid = ?`,
- [nickname, avatarUrl, unionid, now, now, appId, openid]
- );
- };
- const pruneWechatPendingBinds = async (now = Date.now()) => {
- await pool2.query(`DELETE FROM h5_wechat_pending_binds WHERE expires_at <= ?`, [now]);
- };
- const createWechatPendingBind = async ({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- returnTo = "/",
- utmSource = null,
- utmMedium = null,
- utmCampaign = null,
- now = Date.now()
- }) => {
- await pruneWechatPendingBinds(now);
- 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,
- utm_source, utm_medium, utm_campaign, expires_at, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- token,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- returnTo,
- utmSource,
- utmMedium,
- utmCampaign,
- now + PENDING_BIND_TTL_MS,
- now
- ]
- );
- return token;
- };
- const getWechatPendingBind = async (token, now = Date.now()) => {
- if (!token) return null;
- await pruneWechatPendingBinds(now);
- const [rows] = await pool2.query(
- `SELECT token, app_id, openid, unionid, nickname, avatar_url, return_to,
- utm_source, utm_medium, utm_campaign, expires_at
- FROM h5_wechat_pending_binds
- WHERE token = ?
- LIMIT 1`,
- [token]
- );
- const row = rows[0];
- if (!row || Number(row.expires_at) <= now) return null;
- return row;
- };
- const consumeWechatPendingBind = async (token) => {
- await pool2.query(`DELETE FROM h5_wechat_pending_binds WHERE token = ?`, [token]);
- };
- const loginBoundWechatUser = async ({
- userId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now = Date.now()
- }) => {
- await touchWechatIdentity({ appId, openid, unionid, nickname, avatarUrl, now });
- const user = await getUserById(userId);
- if (!user || user.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- const token = await issueUserSession(user.id, user.role, now);
- return { ok: true, token, user: publicUser(user), isNewUser: false };
- };
- const generateWechatUsername = async (openid) => {
- const cleaned = String(openid).replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
- const suffix = cleaned.slice(-8) || crypto5.randomBytes(4).toString("hex");
- let candidate = `wx_${suffix}`.slice(0, 32);
- if (!isValidUsername(candidate)) {
- 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))}${crypto5.randomBytes(2).toString("hex")}`.slice(
- 0,
- 32
- );
- }
- return `wx_${crypto5.randomBytes(6).toString("hex")}`.slice(0, 32);
- };
- const registerViaWechat = async ({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now = Date.now()
- }) => {
- const normalized = await generateWechatUsername(openid);
- const randomPassword = crypto5.randomBytes(24).toString("base64url");
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(randomPassword);
- const userId = crypto5.randomUUID();
- const layout = await publishLayoutFor({ id: userId, username: normalized });
- const workspaceRoot = layout.publishDir;
- const displayName = nickname?.trim() || "\u5FAE\u4FE1\u7528\u6237";
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- await conn.query(
- `INSERT INTO h5_users
- (id, username, slug, email, display_name, salt, password_hash, password_algorithm,
- role, status, plan_type, workspace_root, signup_source, low_balance_gift_eligible,
- low_balance_gift_granted_at, created_at, updated_at)
- VALUES (?, ?, ?, NULL, ?, ?, ?, ?, 'user', 'active', 'free', ?, 'wechat', 1, NULL, ?, ?)`,
- [
- userId,
- normalized,
- normalized,
- displayName,
- salt,
- passwordHash,
- passwordAlgorithm,
- workspaceRoot,
- now,
- now
- ]
- );
- await conn.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)`,
- [userId, defaultSignupBalanceCents, now]
- );
- await recordSignupBonus(conn, userId, defaultSignupBalanceCents, now);
- await conn.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [userId, workspaceRoot]
- );
- await conn.query(
- `INSERT INTO h5_user_wechat_identities
- (id, user_id, app_id, openid, unionid, nickname, avatar_url, last_login_at, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- crypto5.randomUUID(),
- userId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now,
- now,
- now
- ]
- );
- await initializeDefaultSpace(conn, userId, {
- quotaBytes: Number(process.env.MINDSPACE_FREE_QUOTA_BYTES ?? 5 * 1024 * 1024),
- now
- });
- await conn.commit();
- ensureWorkspace(workspaceRoot);
- ensureUserMemoryProfile(workspaceRoot, {
- userId,
- displayName,
- username: normalized,
- slug: normalized
- });
- if (subscriptionService) {
- subscriptionService.grantSubscription(userId, "free", null, null, "\u6CE8\u518C\u8D60\u9001\u514D\u8D39\u5957\u9910").catch(() => {
- });
- }
- const user = await getUserById(userId);
- return { ok: true, user: publicUser(user) };
- } catch (err) {
- await conn.rollback();
- if (err?.code === "ER_DUP_ENTRY") {
- return { ok: false, message: "\u5FAE\u4FE1\u8D26\u53F7\u6CE8\u518C\u51B2\u7A81\uFF0C\u8BF7\u91CD\u8BD5" };
- }
- throw err;
- } finally {
- conn.release();
- }
- };
- const resolveWechatAuth = async ({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- intent = "login",
- bindUserId = null,
- returnTo = "/",
- utmSource = null,
- utmMedium = null,
- utmCampaign = null,
- now = Date.now()
- }) => {
- let binding = await findBindingByOpenid(appId, openid);
- if (!binding && unionid) {
- const unionBinding = await findBindingByUnionid(unionid);
- if (unionBinding) {
- const linked = await bindWechatToUser({
- userId: unionBinding.user_id,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now
- });
- if (!linked.ok) return linked;
- binding = { user_id: unionBinding.user_id, status: unionBinding.status };
- }
- }
- if (binding) {
- if (binding.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- return loginBoundWechatUser({
- userId: binding.user_id,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now
- }).then((result) => result.ok ? { ...result, action: "login" } : result);
- }
- if (intent === "bind" && bindUserId) {
- const user = await getUserById(bindUserId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- const bound = await bindWechatToUser({
- userId: bindUserId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now
- });
- if (!bound.ok) return bound;
- const token = await issueUserSession(user.id, user.role, now);
- return {
- ok: true,
- action: "login",
- token,
- user: publicUser(user),
- isNewUser: false,
- bound: true
- };
- }
- if (intent === "register") {
- const registered = await registerViaWechat({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now
- });
- if (!registered.ok) return registered;
- const token = await issueUserSession(registered.user.id, registered.user.role, now);
- return {
- ok: true,
- action: "login",
- token,
- user: registered.user,
- isNewUser: true
- };
- }
- const pendingToken = await createWechatPendingBind({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- returnTo,
- utmSource,
- utmMedium,
- utmCampaign,
- now
- });
- return {
- ok: true,
- action: "binding_gate",
- pendingToken,
- wechatProfile: {
- nickname: nickname ?? null,
- avatarUrl: avatarUrl ?? null
- },
- returnTo,
- utmSource,
- utmMedium,
- utmCampaign
- };
- };
- const completeWechatRegister = async ({ pendingToken, now = Date.now() }) => {
- const pending = await getWechatPendingBind(pendingToken, now);
- if (!pending) {
- return { ok: false, message: "\u7ED1\u5B9A\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5FAE\u4FE1\u767B\u5F55" };
- }
- const registered = await registerViaWechat({
- appId: pending.app_id,
- openid: pending.openid,
- unionid: pending.unionid,
- nickname: pending.nickname,
- avatarUrl: pending.avatar_url,
- now
- });
- if (!registered.ok) return registered;
- await consumeWechatPendingBind(pendingToken);
- const token = await issueUserSession(registered.user.id, registered.user.role, now);
- return {
- ok: true,
- token,
- user: registered.user,
- isNewUser: true,
- returnTo: pending.return_to || "/",
- utmSource: pending.utm_source,
- utmMedium: pending.utm_medium,
- utmCampaign: pending.utm_campaign
- };
- };
- const completeWechatBindAccount = async ({
- pendingToken,
- username,
- password,
- ip = "unknown",
- now = Date.now()
- }) => {
- const pending = await getWechatPendingBind(pendingToken, now);
- if (!pending) {
- return { ok: false, message: "\u7ED1\u5B9A\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5FAE\u4FE1\u767B\u5F55" };
- }
- const loginResult = await login({ username, password, ip, now });
- if (!loginResult.ok) return loginResult;
- const bound = await bindWechatToUser({
- userId: loginResult.user.id,
- appId: pending.app_id,
- openid: pending.openid,
- unionid: pending.unionid,
- nickname: pending.nickname,
- avatarUrl: pending.avatar_url,
- now
- });
- if (!bound.ok) return bound;
- await consumeWechatPendingBind(pendingToken);
- return {
- ok: true,
- token: loginResult.token,
- user: loginResult.user,
- isNewUser: false,
- bound: true,
- returnTo: pending.return_to || "/"
- };
- };
- const getWechatBindingStatus = async (userId, appId) => {
- const row = await getWechatBindingForUser(userId, appId);
- if (!row) return { bound: false };
- return {
- bound: true,
- nickname: row.nickname,
- avatarUrl: row.avatar_url,
- lastLoginAt: Number(row.last_login_at),
- boundAt: Number(row.created_at)
- };
- };
- const loginByWechat = async (params) => {
- const result = await resolveWechatAuth({ ...params, intent: "login" });
- if (!result.ok) return result;
- if (result.action === "binding_gate") {
- return { ok: false, message: "\u9700\u8981\u5B8C\u6210\u8D26\u53F7\u7ED1\u5B9A" };
- }
- return result;
- };
- return {
- USER_COOKIE,
- register,
- login,
- loginByWechat,
- resolveWechatAuth,
- completeWechatRegister,
- completeWechatBindAccount,
- getWechatPendingBind,
- getWechatBindingStatus,
- getWechatOpenidForUser,
- setRechargeNotifier(callback) {
- rechargeNotifier = typeof callback === "function" ? callback : null;
- },
- findWechatUserByOpenid,
- getWechatAgentRoute,
- upsertWechatAgentRoute,
- clearWechatAgentRoute,
- touchWechatAgentRoute,
- recordWechatMpMessage,
- finishWechatMpMessage,
- insertWechatMpMessageDetail,
- resetPassword,
- verify,
- revoke,
- revokeAllSessionsForUser,
- getMe,
- listPathGrants,
- resolveWorkingDir,
- getUserPublishLayout,
- isPathAllowed,
- repairAllUserPublishDirs,
- registerAgentSession,
- getSessionNode,
- getSessionTarget,
- unregisterAgentSession,
- ownsSession,
- listOwnedSessionIds,
- setSessionOrigin,
- getSessionOrigins,
- canUseChat,
- getUserById,
- getUserPublic,
- listUsers,
- createUser,
- updateUser,
- purchaseSpaceQuota,
- recharge,
- billSessionUsage,
- listUsageRecords,
- listBillingLedger,
- getAdminSummary,
- ensureAdminUser,
- seedRoleCapabilityDefaults,
- resolveUserCapabilities,
- getAgentSessionPolicy,
- getCodeAgentSessionPolicy,
- getRoleCapabilities,
- setRoleCapabilities,
- getUserCapabilities,
- setUserCapabilities,
- clearUserCapabilityOverrides,
- resolveUserPolicies,
- getRolePolicies,
- setRolePolicies,
- getUserPolicies,
- setUserPolicies,
- clearUserPolicyOverrides,
- getRoleSkills,
- setRoleSkills,
- getUserSkills,
- setUserSkills,
- clearUserSkillOverrides,
- syncUserSkillsForUser,
- capabilityCatalog: CAPABILITY_CATALOG,
- policyCatalog: POLICY_CATALOG,
- skillCatalog,
- publicUser
- };
-}
-
-// scripts/agent-run-worker.mjs
-var root = path12.join(path12.dirname(fileURLToPath5(import.meta.url)), "..");
-function loadEnvFile(filePath) {
- 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("=");
- if (eq < 0) continue;
- const key = trimmed.slice(0, eq).trim();
- const value = trimmed.slice(eq + 1).trim();
- if (!process.env[key]) process.env[key] = value;
- }
-}
-function parseTargets() {
- const csv = String(process.env.TKMIND_API_TARGETS ?? "").trim();
- if (csv) {
- return csv.split(",").map((item) => item.trim()).filter(Boolean);
- }
- return [process.env.TKMIND_API_TARGET ?? "https://127.0.0.1:18006"];
-}
-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),
- 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;
-}
-function printHelp() {
- console.log([
- "Usage:",
- " 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 || 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 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],
- apiTargets,
- apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? "local-dev-secret",
- userAuth
-});
-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)
-});
-var stopping = false;
-process.on("SIGINT", () => {
- stopping = true;
-});
-process.on("SIGTERM", () => {
- stopping = true;
-});
-async function runOnce() {
- 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);
- while (!stopping) {
- const status = await gateway.getQueueStatus();
- if (status.inFlight === 0 && status.pendingDispatches === 0) return;
- if (Date.now() - startedAt > maxWaitMs) {
- throw new Error(`agent run worker did not drain within ${maxWaitMs}ms`);
- }
- await new Promise((resolve) => setTimeout(resolve, 500));
- }
-}
-var exitCode = 0;
-try {
- if (args.status) {
- console.log(JSON.stringify({ ok: true, queue: await gateway.getQueueStatus() }, null, 2));
- } 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 }));
- while (!stopping) {
- await runOnce();
- await new Promise((resolve) => setTimeout(resolve, Math.max(200, args.pollMs)));
- }
- }
-} catch (err) {
- exitCode = 1;
- console.error(err instanceof Error ? err.message : String(err));
-} finally {
- await pool.end().catch(() => {
- });
- if (args.status || args.once) process.exit(exitCode);
-}
diff --git a/.runtime/portal/scripts/check-agent-code-run-entry.mjs b/.runtime/portal/scripts/check-agent-code-run-entry.mjs
deleted file mode 100644
index df47876..0000000
--- a/.runtime/portal/scripts/check-agent-code-run-entry.mjs
+++ /dev/null
@@ -1,187 +0,0 @@
-#!/usr/bin/env node
-import fs from 'node:fs';
-import path from 'node:path';
-import { Agent, fetch as undiciFetch } from 'undici';
-import mysql from 'mysql2/promise';
-
-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 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 code-run entry 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',
- };
-}
-
-function truthy(value) {
- return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase());
-}
-
-function listJsAssets(distRoot) {
- const assetsDir = path.join(distRoot, 'assets');
- if (!fs.existsSync(assetsDir)) return [];
- return fs.readdirSync(assetsDir)
- .filter((name) => name.endsWith('.js'))
- .map((name) => path.join(assetsDir, name));
-}
-
-function checkFrontendBundle(distRoot) {
- const jsAssets = listJsAssets(distRoot);
- const text = jsAssets.map((file) => fs.readFileSync(file, 'utf8')).join('\n');
- return {
- distRoot,
- jsAssetCount: jsAssets.length,
- hasAgentRunEndpoint: text.includes('/agent/runs'),
- hasToolModePayload: text.includes('tool_mode'),
- hasCodeLiteral: text.includes('"code"') || text.includes("'code'") || text.includes(':code'),
- note: 'VITE_AGENT_CODE_RUNS_* are build-time flags; runtime checks can prove code entry is bundled, not whether a past build enabled the flag.',
- };
-}
-
-async function queryToolGrants() {
- const conn = await mysql.createConnection(parseMysqlConfig());
- try {
- const [roleRows] = await conn.query(
- `SELECT capability_key, allowed
- FROM h5_capability_grants
- WHERE subject_type = 'role'
- AND subject_id = 'user'
- AND capability_key IN ('aider', 'openhands')
- ORDER BY capability_key`,
- );
- const roleDefaults = Object.fromEntries(
- roleRows.map((row) => [row.capability_key, Boolean(row.allowed)]),
- );
-
- const [userRows] = await conn.query(
- `SELECT subject_id, capability_key, allowed
- FROM h5_capability_grants
- WHERE subject_type = 'user'
- AND capability_key IN ('aider', 'openhands')
- AND allowed = 1
- ORDER BY subject_id, capability_key`,
- );
-
- const grantsByUser = new Map();
- for (const row of userRows) {
- if (!grantsByUser.has(row.subject_id)) grantsByUser.set(row.subject_id, []);
- grantsByUser.get(row.subject_id).push(row.capability_key);
- }
- return {
- roleDefaults,
- whitelistUserCount: grantsByUser.size,
- sampledUsers: [...grantsByUser.entries()].slice(0, 10).map(([userId, grantedTools]) => ({
- userId,
- grantedTools: grantedTools.sort(),
- chatHasCodeTools: false,
- codeHasCodeTools: grantedTools.includes('aider') || grantedTools.includes('openhands'),
- })),
- };
- } finally {
- await conn.end();
- }
-}
-
-const envFile = process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env');
-loadEnvFile(envFile);
-
-const distRoot = path.resolve(process.env.MEMIND_H5_DIST_ROOT || path.join(process.cwd(), 'dist'));
-const publicBase = (process.env.H5_PUBLIC_BASE_URL || 'https://mm.tkmind.cn').replace(/\/$/, '');
-const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false } });
-
-async function fetchJson(pathname) {
- const res = await undiciFetch(`${publicBase}${pathname}`, {
- dispatcher: publicBase.startsWith('https://127.0.0.1') ? insecureDispatcher : undefined,
- });
- const text = await res.text();
- let json = null;
- try {
- json = JSON.parse(text);
- } catch {
- json = null;
- }
- return { ok: res.ok, status: res.status, json, text: json ? undefined : text.slice(0, 160) };
-}
-
-const frontend = checkFrontendBundle(distRoot);
-const runtime = await fetchJson('/api/runtime/status').catch((err) => ({ ok: false, error: err.message }));
-const toolGrants = await queryToolGrants().catch((err) => ({
- ok: false,
- error: err instanceof Error ? (err.message || err.code || err.name) : String(err),
-}));
-
-const buildFlags = {
- serverCodeRunsEnabledEnv: process.env.MEMIND_AGENT_CODE_RUNS_ENABLED ?? null,
- 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.',
-};
-
-const runtimePolicy = runtime.json?.toolRuntime ?? {};
-const roleDefaults = toolGrants.roleDefaults ?? {};
-const ok = Boolean(
- frontend.jsAssetCount > 0 &&
- frontend.hasAgentRunEndpoint &&
- frontend.hasToolModePayload &&
- runtime.ok &&
- runtimePolicy.defaultMode === 'chat' &&
- runtimePolicy.codeToolMode === 'code' &&
- runtimePolicy.chatInjectsCodeTools === false &&
- runtimePolicy.codeRunsEnabled === buildFlags.serverCodeRunsEnabledTruthy &&
- roleDefaults.aider === false &&
- roleDefaults.openhands === false &&
- (toolGrants.sampledUsers ?? []).every((user) => user.chatHasCodeTools === false) &&
- (toolGrants.sampledUsers ?? []).every((user) => user.codeHasCodeTools === true)
-);
-
-console.log(JSON.stringify({
- ok,
- checkedAt: new Date().toISOString(),
- publicBase,
- envFile,
- frontend,
- buildFlags,
- runtimePolicy,
- toolGrants,
- writes: {
- database: false,
- mindSpace: false,
- createsAgentRun: false,
- },
-}, null, 2));
-
-process.exit(ok ? 0 : 1);
diff --git a/.runtime/portal/scripts/check-agent-run-worker.mjs b/.runtime/portal/scripts/check-agent-run-worker.mjs
deleted file mode 100755
index 9860231..0000000
--- a/.runtime/portal/scripts/check-agent-run-worker.mjs
+++ /dev/null
@@ -1,229 +0,0 @@
-#!/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
deleted file mode 100644
index 2f0155b..0000000
--- a/.runtime/portal/scripts/check-mindspace-public-links.mjs
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/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-stream-runtime.mjs b/.runtime/portal/scripts/check-stream-runtime.mjs
deleted file mode 100755
index fd9730e..0000000
--- a/.runtime/portal/scripts/check-stream-runtime.mjs
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env node
-import fs from 'node:fs';
-import path from 'node:path';
-import { Agent, fetch as undiciFetch } from 'undici';
-
-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;
- }
-}
-
-loadEnvFile(path.join(process.cwd(), '.env'));
-
-const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false } });
-const publicBase = (process.env.H5_PUBLIC_BASE_URL || 'https://mm.tkmind.cn').replace(/\/$/, '');
-const targets = (process.env.TKMIND_API_TARGETS || process.env.TKMIND_API_TARGET || '')
- .split(',')
- .map((value) => value.trim())
- .filter(Boolean);
-
-async function fetchText(url, init = {}) {
- const res = await undiciFetch(url, {
- ...init,
- dispatcher: url.startsWith('https://127.0.0.1') ? insecureDispatcher : undefined,
- });
- const text = await res.text();
- return { res, text };
-}
-
-async function checkJson(pathname) {
- const { res, text } = await fetchText(`${publicBase}${pathname}`);
- let json = null;
- try {
- json = JSON.parse(text);
- } catch {
- json = null;
- }
- return { ok: res.ok, status: res.status, json, text: json ? undefined : text.slice(0, 160) };
-}
-
-async function checkSseHeaders(pathname) {
- const { res } = await fetchText(`${publicBase}${pathname}`, {
- method: 'HEAD',
- headers: { Accept: 'text/event-stream' },
- });
- return {
- ok: res.status === 401 || res.ok,
- status: res.status,
- xAccelBuffering: res.headers.get('x-accel-buffering'),
- contentType: res.headers.get('content-type'),
- cacheControl: res.headers.get('cache-control'),
- };
-}
-
-async function checkTarget(target) {
- const { res, text } = await fetchText(new URL('/status', target).toString());
- return { target, ok: res.ok, status: res.status, text: text.slice(0, 80) };
-}
-
-const result = {
- ok: true,
- publicBase,
- checkedAt: new Date().toISOString(),
- status: await checkJson('/api/status').catch((err) => ({ ok: false, error: err.message })),
- runtime: await checkJson('/api/runtime/status').catch((err) => ({ ok: false, error: err.message })),
- sse: {
- sessions: await checkSseHeaders('/api/sessions/check-stream-runtime/events').catch((err) => ({
- ok: false,
- error: err.message,
- })),
- agentRuns: await checkSseHeaders('/api/agent/runs/check-stream-runtime/events').catch((err) => ({
- ok: false,
- error: err.message,
- })),
- },
- targets: await Promise.all(targets.map((target) => checkTarget(target).catch((err) => ({
- target,
- ok: false,
- error: err.message,
- })))),
-};
-
-result.ok = Boolean(
- result.status.ok &&
- result.runtime.ok &&
- result.sse.sessions.ok &&
- result.sse.agentRuns.ok &&
- result.targets.every((target) => target.ok),
-);
-
-console.log(JSON.stringify(result, null, 2));
-process.exit(result.ok ? 0 : 1);
diff --git a/.runtime/portal/scripts/check-tool-runtime.mjs b/.runtime/portal/scripts/check-tool-runtime.mjs
deleted file mode 100755
index 4a5b7ba..0000000
--- a/.runtime/portal/scripts/check-tool-runtime.mjs
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/env node
-import fs from 'node:fs';
-import path from 'node:path';
-import mysql from 'mysql2/promise';
-
-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 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 tool runtime 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',
- };
-}
-
-loadEnvFile(process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env'));
-
-const conn = await mysql.createConnection(parseMysqlConfig());
-try {
- const [roleRows] = await conn.query(
- `SELECT capability_key, allowed
- FROM h5_capability_grants
- WHERE subject_type = 'role'
- AND subject_id = 'user'
- AND capability_key IN ('aider', 'openhands')
- ORDER BY capability_key`,
- );
- const roleDefaults = Object.fromEntries(
- roleRows.map((row) => [row.capability_key, Boolean(row.allowed)]),
- );
-
- const [userRows] = await conn.query(
- `SELECT subject_id, capability_key, allowed
- FROM h5_capability_grants
- WHERE subject_type = 'user'
- AND capability_key IN ('aider', 'openhands')
- AND allowed = 1
- ORDER BY subject_id, capability_key`,
- );
-
- const grantsByUser = new Map();
- for (const row of userRows) {
- if (!grantsByUser.has(row.subject_id)) grantsByUser.set(row.subject_id, []);
- grantsByUser.get(row.subject_id).push(row.capability_key);
- }
- const sampledUsers = [...grantsByUser.entries()].slice(0, 10).map(([userId, grantedTools]) => ({
- userId,
- grantedTools: grantedTools.sort(),
- chatHasCodeTools: false,
- codeHasCodeTools: grantedTools.includes('aider') || grantedTools.includes('openhands'),
- }));
-
- const ok = Boolean(
- roleDefaults.aider === false &&
- roleDefaults.openhands === false &&
- sampledUsers.every((user) => user.chatHasCodeTools === false) &&
- sampledUsers.every((user) => user.codeHasCodeTools === true),
- );
-
- console.log(JSON.stringify({
- ok,
- checkedAt: new Date().toISOString(),
- roleDefaults,
- whitelistUserCount: grantsByUser.size,
- sampledUsers,
- runtimePolicy: {
- defaultMode: 'chat',
- codeToolMode: 'code',
- 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);
-} finally {
- await conn.end();
-}
diff --git a/.runtime/portal/scripts/install-agent-run-guard-agent.sh b/.runtime/portal/scripts/install-agent-run-guard-agent.sh
deleted file mode 100755
index 9f87abd..0000000
--- a/.runtime/portal/scripts/install-agent-run-guard-agent.sh
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/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
deleted file mode 100755
index c843d68..0000000
--- a/.runtime/portal/scripts/install-agent-run-worker-agent.sh
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/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
deleted file mode 100755
index 178d36e..0000000
--- a/.runtime/portal/scripts/install-runtime-heartbeat-agent.sh
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/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-metrics-agent.sh b/.runtime/portal/scripts/install-runtime-metrics-agent.sh
deleted file mode 100755
index 960b5ad..0000000
--- a/.runtime/portal/scripts/install-runtime-metrics-agent.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/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_METRICS_SCRIPT:-$ROOT/scripts/runtime-worker-metrics.mjs}"
-LABEL="${MEMIND_RUNTIME_METRICS_LABEL:-cn.tkmind.memind-runtime-metrics}"
-PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist"
-LOG="$HOME/Library/Logs/memind-runtime-metrics.log"
-GUI="gui/$(id -u)"
-INTERVAL="${MEMIND_RUNTIME_METRICS_INTERVAL:-60}"
-
-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 metrics script not found: $SCRIPT" >&2
- exit 1
-fi
-
-cat > "$PLIST" <
-
-
-
- Label
- $LABEL
- ProgramArguments
-
- $NODE_BIN
- $SCRIPT
- sample
-
- 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
-
-
-
-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: ${INTERVAL}s"
-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
deleted file mode 100755
index 058b9b2..0000000
--- a/.runtime/portal/scripts/install-runtime-slo-report-agent.sh
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/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_LABEL:-cn.tkmind.memind-runtime-slo-report}"
-PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist"
-LOG="$HOME/Library/Logs/memind-runtime-slo-report.log"
-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"
-
-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
- StartCalendarInterval
-
- Hour
- $HOUR
- Minute
- $MINUTE
-
- 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: 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
deleted file mode 100755
index 93e10e1..0000000
--- a/.runtime/portal/scripts/install-runtime-slo-soak-agent.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/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
deleted file mode 100644
index beeecce..0000000
--- a/.runtime/portal/scripts/load-env.mjs
+++ /dev/null
@@ -1,20 +0,0 @@
-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
deleted file mode 100755
index 3d543ea..0000000
--- a/.runtime/portal/scripts/memind-portal-tunnel.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/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/run-memind-portal-prod.sh b/.runtime/portal/scripts/run-memind-portal-prod.sh
deleted file mode 100755
index 5ef9b2d..0000000
--- a/.runtime/portal/scripts/run-memind-portal-prod.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-cd "$ROOT"
-
-if [[ -f "${ROOT}/.env" ]]; then
- set -a
- # shellcheck disable=SC1091
- source "${ROOT}/.env"
- set +a
-fi
-
-export NODE_ENV=production
-export H5_PORT="${H5_PORT:-8081}"
-export H5_PUBLIC_BASE_URL="${H5_PUBLIC_BASE_URL:-https://mm.tkmind.cn}"
-export TKMIND_API_TARGETS="${TKMIND_API_TARGETS:-https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008,https://127.0.0.1:18009}"
-export TKMIND_API_TARGET="${TKMIND_API_TARGET:-https://127.0.0.1:18006}"
-export TKMIND_API_TARGET_1="${TKMIND_API_TARGET_1:-https://127.0.0.1:18007}"
-
-NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
-if [[ ! -x "${NODE_BIN}" ]]; then
- NODE_BIN="$(command -v node)"
-fi
-
-export GOOSED_MCP_NODE_PATH="${GOOSED_MCP_NODE_PATH:-${NODE_BIN}}"
-export GOOSED_MCP_SERVER_PATH="${GOOSED_MCP_SERVER_PATH:-${ROOT}/mindspace-sandbox-mcp.mjs}"
-
-exec "${NODE_BIN}" "${ROOT}/server.mjs"
diff --git a/.runtime/portal/scripts/runtime-slo-report.mjs b/.runtime/portal/scripts/runtime-slo-report.mjs
deleted file mode 100755
index 4d66980..0000000
--- a/.runtime/portal/scripts/runtime-slo-report.mjs
+++ /dev/null
@@ -1,459 +0,0 @@
-#!/usr/bin/env node
-import fs from 'node:fs';
-import path from 'node:path';
-import { Agent, fetch as undiciFetch } from 'undici';
-import mysql from 'mysql2/promise';
-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 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 SLO report: ${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',
- };
-}
-
-function walkCount(root) {
- const stack = [root];
- let files = 0;
- let dirs = 0;
- let bytes = 0;
- while (stack.length) {
- const current = stack.pop();
- let entries = [];
- try {
- entries = fs.readdirSync(current, { withFileTypes: true });
- } catch {
- continue;
- }
- for (const entry of entries) {
- const full = path.join(current, entry.name);
- if (entry.isDirectory()) {
- dirs += 1;
- stack.push(full);
- continue;
- }
- if (!entry.isFile()) continue;
- files += 1;
- try {
- bytes += fs.statSync(full).size;
- } catch {
- // ignore files that disappear while counting
- }
- }
- }
- return { files, dirs, bytes };
-}
-
-function uniquePaths(paths) {
- const seen = new Set();
- const result = [];
- for (const item of paths) {
- const resolved = path.resolve(item);
- if (seen.has(resolved)) continue;
- seen.add(resolved);
- result.push(resolved);
- }
- return result;
-}
-
-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] [--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'],
- ['h5_user_sessions', 'sessions'],
- ['h5_agent_runs', 'agentRuns'],
- ['h5_agent_run_events', 'agentRunEvents'],
- ['h5_capability_grants', 'capabilityGrants'],
- ['h5_llm_provider_keys', 'llmProviderKeys'],
- ['mindspace_assets', 'mindspaceAssets'],
- ['mindspace_pages', 'mindspacePages'],
- ['mindspace_publications', 'mindspacePublications'],
- ];
-}
-
-async function safeTableCount(conn, table) {
- try {
- const [rows] = await conn.query(`SELECT COUNT(*) AS count FROM \`${table}\``);
- return Number(rows[0]?.count ?? 0);
- } catch (err) {
- return { error: err instanceof Error ? err.message : String(err) };
- }
-}
-
-async function readDbSummary() {
- const conn = await mysql.createConnection(parseMysqlConfig());
- try {
- const result = {};
- for (const [table, key] of tableCountQueries()) {
- result[key] = await safeTableCount(conn, table);
- }
- return result;
- } finally {
- await conn.end();
- }
-}
-
-async function readRedisSummary(namespace, redisUrl) {
- const client = createClient({ url: redisUrl });
- await client.connect();
- try {
- const workerKeys = await client.keys(`${namespace}:worker:*:active_streams`);
- const streamKeys = await client.keys(`${namespace}:stream:*:status`);
- const sessionKeys = await client.keys(`${namespace}:session:*:target`);
- return {
- workerCount: workerKeys.length,
- streamStatusCount: streamKeys.length,
- sessionTargetCount: sessionKeys.length,
- };
- } finally {
- await client.quit();
- }
-}
-
-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,
- 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,
- activeStreams: worker.activeStreams,
- streamOpenCount: worker.streamOpenCount,
- streamAbortCount: worker.streamAbortCount,
- streamErrorCount: worker.streamErrorCount,
- ewmaFirstTokenMs: worker.ewmaFirstTokenMs,
- lastFirstTokenMs: worker.lastFirstTokenMs,
- lastFirstTokenAt: worker.lastFirstTokenAt,
- firstTokenCount: worker.firstTokenCount,
- firstToken5m: worker.firstToken5m ?? null,
- firstToken1h: worker.firstToken1h ?? null,
- cpuLoad: worker.cpuLoad,
- memoryPressure: worker.memoryPressure,
- 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,
- drain: worker.drain,
- })),
- };
-}
-
-const envFile = process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env');
-const appRoot = process.env.MEMIND_APP_ROOT || path.dirname(path.resolve(envFile));
-loadEnvFile(envFile);
-const args = parseArgs(process.argv.slice(2));
-if (args.help) {
- printHelp();
- process.exit(0);
-}
-
-const publicBase = (process.env.H5_PUBLIC_BASE_URL || 'https://mm.tkmind.cn').replace(/\/$/, '');
-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 mindSpaceRoots = uniquePaths([
- path.join(appRoot, 'MindSpace'),
- process.env.MINDSPACE_STORAGE_ROOT || path.join(appRoot, 'data', 'mindspace'),
-]);
-const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
-
-async function fetchRuntime() {
- const res = await undiciFetch(`${publicBase}/api/runtime/status`, {
- dispatcher: publicBase.startsWith('https://127.0.0.1') ? dispatcher : undefined,
- });
- const text = await res.text();
- return {
- ok: res.ok,
- status: res.status,
- json: JSON.parse(text),
- };
-}
-
-const runtime = await fetchRuntime().catch((err) => ({ ok: false, error: err.message }));
-const runtimeSummary = runtime.ok ? summarizeRuntime(runtime.json) : null;
-const db = await readDbSummary().catch((err) => ({ error: err instanceof Error ? err.message : String(err) }));
-const redis = await readRedisSummary(namespace, redisUrl).catch((err) => ({
- error: err instanceof Error ? err.message : String(err),
-}));
-const mindSpace = mindSpaceRoots.map((root) => (
- fs.existsSync(root)
- ? { root, ...walkCount(root) }
- : { root, error: 'missing' }
-));
-
-const failures = [];
-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}`);
- if (worker.firstTokenCount > 0 && worker.ewmaFirstTokenMs <= 0) failures.push(`${worker.id}_first_token_ewma_missing`);
-}
-if (runtimeSummary?.toolRuntime?.chatInjectsCodeTools !== false) failures.push('chat_injects_code_tools');
-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,
- checkedAt: new Date().toISOString(),
- publicBase,
- appRoot,
- envFile,
- namespace,
- runtime: runtimeSummary,
- db,
- redis,
- mindSpace,
- failures,
- writes: {
- database: false,
- mindSpace: false,
- redis: false,
- report: Boolean(args.writeReport),
- reportPrune: Boolean(args.prune),
- },
- reportPrune,
-};
-
-function markdownReport(payload) {
- const workers = payload.runtime?.workers ?? [];
- const lines = [
- `# Memind Runtime SLO Report`,
- '',
- `- checkedAt: ${payload.checkedAt}`,
- `- ok: ${payload.ok}`,
- `- failures: ${payload.failures.length ? payload.failures.join(', ') : 'none'}`,
- `- publicBase: ${payload.publicBase}`,
- `- appRoot: ${payload.appRoot}`,
- '',
- '## Workers',
- '',
- '| worker | healthy | active | errors | first-token 5m p50/p95 | first-token 1h p50/p95 | heartbeat | metricsFresh | score |',
- '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |',
- ];
- for (const worker of workers) {
- lines.push([
- worker.id,
- worker.healthy,
- worker.activeStreams,
- 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(/$/, ' |'));
- }
- lines.push(
- '',
- '## 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),
- '```',
- '',
- '## Writes',
- '',
- '```json',
- JSON.stringify(payload.writes, null, 2),
- '```',
- '',
- '## Report Prune',
- '',
- '```json',
- JSON.stringify(payload.reportPrune ?? null, null, 2),
- '```',
- '',
- );
- return lines.join('\n');
-}
-
-if (args.writeReport) {
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
- report.reportFiles = {
- json: path.join(reportDir, `${stamp}.json`),
- markdown: path.join(reportDir, `${stamp}.md`),
- };
- fs.writeFileSync(report.reportFiles.json, `${JSON.stringify(report, null, 2)}\n`);
- fs.writeFileSync(report.reportFiles.markdown, markdownReport(report));
-}
-
-console.log(JSON.stringify(report, null, 2));
-process.exit(report.ok ? 0 : 1);
diff --git a/.runtime/portal/scripts/runtime-worker-drain.mjs b/.runtime/portal/scripts/runtime-worker-drain.mjs
deleted file mode 100755
index 6bfa659..0000000
--- a/.runtime/portal/scripts/runtime-worker-drain.mjs
+++ /dev/null
@@ -1,140 +0,0 @@
-#!/usr/bin/env node
-import fs from 'node:fs';
-import path from 'node:path';
-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;
- }
-}
-
-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 configuredWorkers = (process.env.TKMIND_API_TARGETS || process.env.TKMIND_API_TARGET || '')
- .split(',')
- .map((value) => value.trim())
- .filter(Boolean)
- .map((_, index) => `goosed-${index + 1}`);
-const action = process.argv[2] || 'status';
-const positionalArgs = process.argv.slice(3).filter((arg) => !arg.startsWith('--'));
-const workerId = positionalArgs[0] || null;
-const apply = process.argv.includes('--apply');
-const staleMsArg = process.argv.find((arg) => arg.startsWith('--stale-ms='));
-const staleMs = Math.max(
- 60_000,
- Number(staleMsArg?.split('=').at(1) || process.env.MEMIND_RUNTIME_STALE_STREAM_MS || 15 * 60 * 1000),
-);
-
-function usage() {
- console.error('Usage: node scripts/runtime-worker-drain.mjs [goosed-N] [--apply] [--stale-ms=900000]');
-}
-
-function workerKey(id, field) {
- return [namespace, 'worker', id, field].join(':');
-}
-
-if (!['status', 'drain', 'undrain', 'reconcile'].includes(action)) {
- usage();
- process.exit(2);
-}
-if (['drain', 'undrain'].includes(action) && !workerId) {
- usage();
- 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();
-
-if (action === 'drain') {
- await client.set(workerKey(workerId, 'drain'), '1');
-}
-if (action === 'undrain') {
- await client.del(workerKey(workerId, 'drain'));
-}
-
-const keys = await client.keys(workerKey('*', 'active_streams'));
-const workers = [
- ...configuredWorkers,
- ...keys
- .map((key) => key.split(':').at(-2))
- .filter(Boolean),
-];
-if (workerId && !workers.includes(workerId)) workers.push(workerId);
-
-const rows = [];
-const now = Date.now();
-for (const id of [...new Set(workers)].sort()) {
- const values = await client.mGet([
- workerKey(id, 'active_streams'),
- workerKey(id, 'drain'),
- workerKey(id, 'stream_open_count'),
- workerKey(id, 'stream_abort_count'),
- workerKey(id, 'stream_error_count'),
- workerKey(id, 'last_stream_started_at'),
- workerKey(id, 'last_stream_ended_at'),
- workerKey(id, 'last_stream_reconciled_at'),
- workerKey(id, 'stream_reconcile_count'),
- ]);
- const activeStreams = Number(values[0] || 0);
- const lastStreamStartedAt = values[5] ? Number(values[5]) : null;
- const lastStreamEndedAt = values[6] ? Number(values[6]) : null;
- const staleAgeMs = lastStreamStartedAt ? now - lastStreamStartedAt : null;
- const stale = Boolean(
- activeStreams > 0 &&
- lastStreamStartedAt &&
- staleAgeMs > staleMs &&
- (
- !lastStreamEndedAt ||
- lastStreamEndedAt >= lastStreamStartedAt ||
- now - lastStreamEndedAt > staleMs
- ),
- );
- if (action === 'reconcile' && stale && apply) {
- await client
- .multi()
- .set(workerKey(id, 'active_streams'), '0')
- .set(workerKey(id, 'last_stream_reconciled_at'), String(now))
- .incr(workerKey(id, 'stream_reconcile_count'))
- .exec();
- }
- rows.push({
- id,
- activeStreams,
- drain: /^(1|true|yes)$/i.test(String(values[1] || '')),
- streamOpenCount: Number(values[2] || 0),
- streamAbortCount: Number(values[3] || 0),
- streamErrorCount: Number(values[4] || 0),
- lastStreamStartedAt,
- lastStreamEndedAt,
- lastStreamReconciledAt: values[7] ? Number(values[7]) : null,
- streamReconcileCount: Number(values[8] || 0),
- stale,
- staleAgeMs,
- reconciled: action === 'reconcile' && stale && apply,
- });
-}
-
-await client.quit();
-
-console.log(JSON.stringify({
- ok: true,
- action,
- workerId,
- namespace,
- dryRun: action === 'reconcile' ? !apply : undefined,
- staleMs: action === 'reconcile' ? staleMs : undefined,
- workers: rows,
-}, null, 2));
diff --git a/.runtime/portal/scripts/runtime-worker-heartbeat.mjs b/.runtime/portal/scripts/runtime-worker-heartbeat.mjs
deleted file mode 100755
index 9de4194..0000000
--- a/.runtime/portal/scripts/runtime-worker-heartbeat.mjs
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/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/runtime-worker-metrics.mjs b/.runtime/portal/scripts/runtime-worker-metrics.mjs
deleted file mode 100755
index c2cf080..0000000
--- a/.runtime/portal/scripts/runtime-worker-metrics.mjs
+++ /dev/null
@@ -1,171 +0,0 @@
-#!/usr/bin/env node
-import fs from 'node:fs';
-import path from 'node:path';
-import { execFileSync } from 'node:child_process';
-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 sh(command) {
- return execFileSync('/bin/zsh', ['-lc', command], { encoding: 'utf8' }).trim();
-}
-
-function parsePercent(value) {
- const n = Number(String(value ?? '').replace('%', '').trim());
- return Number.isFinite(n) ? n : 0;
-}
-
-function parseBytes(value) {
- const match = String(value ?? '').trim().match(/^([\d.]+)\s*([kmgt]?i?b)?$/i);
- if (!match) return 0;
- const n = Number(match[1]);
- if (!Number.isFinite(n)) return 0;
- const unit = (match[2] || 'b').toLowerCase();
- const scale = {
- b: 1,
- kb: 1e3,
- mb: 1e6,
- gb: 1e9,
- tb: 1e12,
- kib: 1024,
- mib: 1024 ** 2,
- gib: 1024 ** 3,
- tib: 1024 ** 4,
- }[unit] ?? 1;
- return Math.round(n * scale);
-}
-
-function parseMemUsage(value) {
- const [used, limit] = String(value ?? '').split('/').map((part) => part.trim());
- return {
- usedBytes: parseBytes(used),
- limitBytes: parseBytes(limit),
- };
-}
-
-function targetWorkerIds() {
- 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,
- container: `goosed-prod-${index + 1}`,
- }));
-}
-
-function readDockerStats(container) {
- try {
- const line = sh(`docker stats --no-stream --format '{{json .}}' ${JSON.stringify(container)} 2>/dev/null`);
- return line ? JSON.parse(line) : null;
- } catch {
- return null;
- }
-}
-
-function readDockerInspect(container) {
- try {
- const line = sh(`docker inspect --format '{{json .State}}' ${JSON.stringify(container)} 2>/dev/null`);
- return line ? JSON.parse(line) : null;
- } catch {
- return null;
- }
-}
-
-function readContainerFdCount(container) {
- try {
- return Number(sh(`docker exec ${JSON.stringify(container)} sh -lc 'ls /proc/1/fd 2>/dev/null | wc -l'`)) || 0;
- } catch {
- return 0;
- }
-}
-
-function workerKey(namespace, id, field) {
- return [namespace, 'worker', id, field].join(':');
-}
-
-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] || 'sample';
-const dryRun = process.argv.includes('--dry-run') || action === 'status';
-const fdWarn = Number(process.env.GOOSED_FD_WARN ?? 180);
-const now = Date.now();
-
-if (!['sample', 'status'].includes(action)) {
- console.error('Usage: node scripts/runtime-worker-metrics.mjs [--dry-run]');
- process.exit(2);
-}
-
-const workers = [];
-for (const worker of targetWorkerIds()) {
- const stats = readDockerStats(worker.container);
- const state = readDockerInspect(worker.container);
- const fdCount = readContainerFdCount(worker.container);
- const mem = parseMemUsage(stats?.MemUsage);
- const cpuLoad = parsePercent(stats?.CPUPerc);
- const memoryPressure = parsePercent(stats?.MemPerc);
- const fdPressure = fdWarn > 0 ? Number((fdCount / fdWarn).toFixed(4)) : 0;
- workers.push({
- ...worker,
- ok: Boolean(stats && state?.Running),
- health: state?.Health?.Status ?? state?.Status ?? null,
- hostPid: Number(state?.Pid ?? 0) || null,
- cpuLoad,
- memoryPressure,
- fdPressure,
- fdCount,
- pids: Number(stats?.PIDs ?? 0),
- memUsedBytes: mem.usedBytes,
- memLimitBytes: mem.limitBytes,
- sampledAt: now,
- });
-}
-
-if (!dryRun) {
- const client = createClient({ url: redisUrl });
- client.on('error', (err) => {
- console.error(`Redis error: ${err instanceof Error ? err.message : err}`);
- });
- await client.connect();
- for (const worker of workers) {
- await client
- .multi()
- .set(workerKey(namespace, worker.id, 'cpu_load'), String(worker.cpuLoad))
- .set(workerKey(namespace, worker.id, 'memory_pressure'), String(worker.memoryPressure))
- .set(workerKey(namespace, worker.id, 'fd_pressure'), String(worker.fdPressure))
- .set(workerKey(namespace, worker.id, 'fd_count'), String(worker.fdCount))
- .set(workerKey(namespace, worker.id, 'container_pids'), String(worker.pids))
- .set(workerKey(namespace, worker.id, 'container_name'), worker.container)
- .set(workerKey(namespace, worker.id, 'container_health'), worker.health ?? '')
- .set(workerKey(namespace, worker.id, 'container_host_pid'), String(worker.hostPid ?? ''))
- .set(workerKey(namespace, worker.id, 'metrics_sampled_at'), String(worker.sampledAt))
- .set(workerKey(namespace, worker.id, 'heartbeat'), String(now), { EX: 90 })
- .exec();
- }
- await client.quit();
-}
-
-console.log(JSON.stringify({
- ok: workers.every((worker) => worker.ok),
- action,
- dryRun,
- namespace,
- redisWrites: !dryRun,
- workers,
-}, null, 2));
-
-process.exit(workers.every((worker) => worker.ok) ? 0 : 1);
diff --git a/.runtime/portal/scripts/wechat-mp-menu.mjs b/.runtime/portal/scripts/wechat-mp-menu.mjs
deleted file mode 100755
index 494c6e9..0000000
--- a/.runtime/portal/scripts/wechat-mp-menu.mjs
+++ /dev/null
@@ -1,138 +0,0 @@
-#!/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
deleted file mode 100644
index ddf5fc5..0000000
--- a/.runtime/portal/server.mjs
+++ /dev/null
@@ -1,42328 +0,0 @@
-import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
-var __create = Object.create;
-var __defProp = Object.defineProperty;
-var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
-var __getOwnPropNames = Object.getOwnPropertyNames;
-var __getProtoOf = Object.getPrototypeOf;
-var __hasOwnProp = Object.prototype.hasOwnProperty;
-var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
-}) : x)(function(x) {
- if (typeof require !== "undefined") return require.apply(this, arguments);
- throw Error('Dynamic require of "' + x + '" is not supported');
-});
-var __esm = (fn, res) => function __init() {
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
-};
-var __commonJS = (cb, mod) => function __require2() {
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
-};
-var __export = (target, all) => {
- for (var name in all)
- __defProp(target, name, { get: all[name], enumerable: true });
-};
-var __copyProps = (to, from, except, desc) => {
- if (from && typeof from === "object" || typeof from === "function") {
- for (let key of __getOwnPropNames(from))
- if (!__hasOwnProp.call(to, key) && key !== except)
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
- }
- return to;
-};
-var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
- // If the importer is in node compatibility mode or this is not an ESM
- // file that has been converted to a CommonJS file using a Babel-
- // compatible transform (i.e. "__esModule" has not been set), then set
- // "default" to the CommonJS "module.exports" for node compatibility.
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
- mod
-));
-
-// node_modules/.pnpm/postgres-array@2.0.0/node_modules/postgres-array/index.js
-var require_postgres_array = __commonJS({
- "node_modules/.pnpm/postgres-array@2.0.0/node_modules/postgres-array/index.js"(exports) {
- "use strict";
- exports.parse = function(source, transform) {
- return new ArrayParser(source, transform).parse();
- };
- var ArrayParser = class _ArrayParser {
- constructor(source, transform) {
- this.source = source;
- this.transform = transform || identity;
- this.position = 0;
- this.entries = [];
- this.recorded = [];
- this.dimension = 0;
- }
- isEof() {
- return this.position >= this.source.length;
- }
- nextCharacter() {
- var character = this.source[this.position++];
- if (character === "\\") {
- return {
- value: this.source[this.position++],
- escaped: true
- };
- }
- return {
- value: character,
- escaped: false
- };
- }
- record(character) {
- this.recorded.push(character);
- }
- newEntry(includeEmpty) {
- var entry;
- if (this.recorded.length > 0 || includeEmpty) {
- entry = this.recorded.join("");
- if (entry === "NULL" && !includeEmpty) {
- entry = null;
- }
- if (entry !== null) entry = this.transform(entry);
- this.entries.push(entry);
- this.recorded = [];
- }
- }
- consumeDimensions() {
- if (this.source[0] === "[") {
- while (!this.isEof()) {
- var char = this.nextCharacter();
- if (char.value === "=") break;
- }
- }
- }
- parse(nested) {
- var character, parser, quote;
- this.consumeDimensions();
- while (!this.isEof()) {
- character = this.nextCharacter();
- if (character.value === "{" && !quote) {
- this.dimension++;
- if (this.dimension > 1) {
- parser = new _ArrayParser(this.source.substr(this.position - 1), this.transform);
- this.entries.push(parser.parse(true));
- this.position += parser.position - 2;
- }
- } else if (character.value === "}" && !quote) {
- this.dimension--;
- if (!this.dimension) {
- this.newEntry();
- if (nested) return this.entries;
- }
- } else if (character.value === '"' && !character.escaped) {
- if (quote) this.newEntry(true);
- quote = !quote;
- } else if (character.value === "," && !quote) {
- this.newEntry();
- } else {
- this.record(character.value);
- }
- }
- if (this.dimension !== 0) {
- throw new Error("array dimension not balanced");
- }
- return this.entries;
- }
- };
- function identity(value) {
- return value;
- }
- }
-});
-
-// node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/arrayParser.js
-var require_arrayParser = __commonJS({
- "node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/arrayParser.js"(exports, module) {
- var array = require_postgres_array();
- module.exports = {
- create: function(source, transform) {
- return {
- parse: function() {
- return array.parse(source, transform);
- }
- };
- }
- };
- }
-});
-
-// node_modules/.pnpm/postgres-date@1.0.7/node_modules/postgres-date/index.js
-var require_postgres_date = __commonJS({
- "node_modules/.pnpm/postgres-date@1.0.7/node_modules/postgres-date/index.js"(exports, module) {
- "use strict";
- var DATE_TIME = /(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?.*?( BC)?$/;
- var DATE = /^(\d{1,})-(\d{2})-(\d{2})( BC)?$/;
- var TIME_ZONE = /([Z+-])(\d{2})?:?(\d{2})?:?(\d{2})?/;
- var INFINITY = /^-?infinity$/;
- module.exports = function parseDate(isoDate) {
- if (INFINITY.test(isoDate)) {
- return Number(isoDate.replace("i", "I"));
- }
- var matches = DATE_TIME.exec(isoDate);
- if (!matches) {
- return getDate(isoDate) || null;
- }
- var isBC = !!matches[8];
- var year = parseInt(matches[1], 10);
- if (isBC) {
- year = bcYearToNegativeYear(year);
- }
- var month = parseInt(matches[2], 10) - 1;
- var day = matches[3];
- var hour = parseInt(matches[4], 10);
- var minute = parseInt(matches[5], 10);
- var second = parseInt(matches[6], 10);
- var ms = matches[7];
- ms = ms ? 1e3 * parseFloat(ms) : 0;
- var date;
- var offset = timeZoneOffset(isoDate);
- if (offset != null) {
- date = new Date(Date.UTC(year, month, day, hour, minute, second, ms));
- if (is0To99(year)) {
- date.setUTCFullYear(year);
- }
- if (offset !== 0) {
- date.setTime(date.getTime() - offset);
- }
- } else {
- date = new Date(year, month, day, hour, minute, second, ms);
- if (is0To99(year)) {
- date.setFullYear(year);
- }
- }
- return date;
- };
- function getDate(isoDate) {
- var matches = DATE.exec(isoDate);
- if (!matches) {
- return;
- }
- var year = parseInt(matches[1], 10);
- var isBC = !!matches[4];
- if (isBC) {
- year = bcYearToNegativeYear(year);
- }
- var month = parseInt(matches[2], 10) - 1;
- var day = matches[3];
- var date = new Date(year, month, day);
- if (is0To99(year)) {
- date.setFullYear(year);
- }
- return date;
- }
- function timeZoneOffset(isoDate) {
- if (isoDate.endsWith("+00")) {
- return 0;
- }
- var zone = TIME_ZONE.exec(isoDate.split(" ")[1]);
- if (!zone) return;
- var type = zone[1];
- if (type === "Z") {
- return 0;
- }
- var sign = type === "-" ? -1 : 1;
- var offset = parseInt(zone[2], 10) * 3600 + parseInt(zone[3] || 0, 10) * 60 + parseInt(zone[4] || 0, 10);
- return offset * sign * 1e3;
- }
- function bcYearToNegativeYear(year) {
- return -(year - 1);
- }
- function is0To99(num) {
- return num >= 0 && num < 100;
- }
- }
-});
-
-// node_modules/.pnpm/xtend@4.0.2/node_modules/xtend/mutable.js
-var require_mutable = __commonJS({
- "node_modules/.pnpm/xtend@4.0.2/node_modules/xtend/mutable.js"(exports, module) {
- module.exports = extend;
- var hasOwnProperty = Object.prototype.hasOwnProperty;
- function extend(target) {
- for (var i = 1; i < arguments.length; i++) {
- var source = arguments[i];
- for (var key in source) {
- if (hasOwnProperty.call(source, key)) {
- target[key] = source[key];
- }
- }
- }
- return target;
- }
- }
-});
-
-// node_modules/.pnpm/postgres-interval@1.2.0/node_modules/postgres-interval/index.js
-var require_postgres_interval = __commonJS({
- "node_modules/.pnpm/postgres-interval@1.2.0/node_modules/postgres-interval/index.js"(exports, module) {
- "use strict";
- var extend = require_mutable();
- module.exports = PostgresInterval;
- function PostgresInterval(raw) {
- if (!(this instanceof PostgresInterval)) {
- return new PostgresInterval(raw);
- }
- extend(this, parse(raw));
- }
- var properties = ["seconds", "minutes", "hours", "days", "months", "years"];
- PostgresInterval.prototype.toPostgres = function() {
- var filtered = properties.filter(this.hasOwnProperty, this);
- if (this.milliseconds && filtered.indexOf("seconds") < 0) {
- filtered.push("seconds");
- }
- if (filtered.length === 0) return "0";
- return filtered.map(function(property) {
- var value = this[property] || 0;
- if (property === "seconds" && this.milliseconds) {
- value = (value + this.milliseconds / 1e3).toFixed(6).replace(/\.?0+$/, "");
- }
- return value + " " + property;
- }, this).join(" ");
- };
- var propertiesISOEquivalent = {
- years: "Y",
- months: "M",
- days: "D",
- hours: "H",
- minutes: "M",
- seconds: "S"
- };
- var dateProperties = ["years", "months", "days"];
- var timeProperties = ["hours", "minutes", "seconds"];
- PostgresInterval.prototype.toISOString = PostgresInterval.prototype.toISO = function() {
- var datePart = dateProperties.map(buildProperty, this).join("");
- var timePart = timeProperties.map(buildProperty, this).join("");
- return "P" + datePart + "T" + timePart;
- function buildProperty(property) {
- var value = this[property] || 0;
- if (property === "seconds" && this.milliseconds) {
- value = (value + this.milliseconds / 1e3).toFixed(6).replace(/0+$/, "");
- }
- return value + propertiesISOEquivalent[property];
- }
- };
- var NUMBER = "([+-]?\\d+)";
- var YEAR = NUMBER + "\\s+years?";
- var MONTH = NUMBER + "\\s+mons?";
- var DAY = NUMBER + "\\s+days?";
- var TIME = "([+-])?([\\d]*):(\\d\\d):(\\d\\d)\\.?(\\d{1,6})?";
- var INTERVAL = new RegExp([YEAR, MONTH, DAY, TIME].map(function(regexString) {
- return "(" + regexString + ")?";
- }).join("\\s*"));
- var positions = {
- years: 2,
- months: 4,
- days: 6,
- hours: 9,
- minutes: 10,
- seconds: 11,
- milliseconds: 12
- };
- var negatives = ["hours", "minutes", "seconds", "milliseconds"];
- function parseMilliseconds(fraction) {
- var microseconds = fraction + "000000".slice(fraction.length);
- return parseInt(microseconds, 10) / 1e3;
- }
- function parse(interval) {
- if (!interval) return {};
- var matches = INTERVAL.exec(interval);
- var isNegative = matches[8] === "-";
- return Object.keys(positions).reduce(function(parsed, property) {
- var position = positions[property];
- var value = matches[position];
- if (!value) return parsed;
- value = property === "milliseconds" ? parseMilliseconds(value) : parseInt(value, 10);
- if (!value) return parsed;
- if (isNegative && ~negatives.indexOf(property)) {
- value *= -1;
- }
- parsed[property] = value;
- return parsed;
- }, {});
- }
- }
-});
-
-// node_modules/.pnpm/postgres-bytea@1.0.1/node_modules/postgres-bytea/index.js
-var require_postgres_bytea = __commonJS({
- "node_modules/.pnpm/postgres-bytea@1.0.1/node_modules/postgres-bytea/index.js"(exports, module) {
- "use strict";
- var bufferFrom = Buffer.from || Buffer;
- module.exports = function parseBytea(input) {
- if (/^\\x/.test(input)) {
- return bufferFrom(input.substr(2), "hex");
- }
- var output = "";
- var i = 0;
- while (i < input.length) {
- if (input[i] !== "\\") {
- output += input[i];
- ++i;
- } else {
- if (/[0-7]{3}/.test(input.substr(i + 1, 3))) {
- output += String.fromCharCode(parseInt(input.substr(i + 1, 3), 8));
- i += 4;
- } else {
- var backslashes = 1;
- while (i + backslashes < input.length && input[i + backslashes] === "\\") {
- backslashes++;
- }
- for (var k = 0; k < Math.floor(backslashes / 2); ++k) {
- output += "\\";
- }
- i += Math.floor(backslashes / 2) * 2;
- }
- }
- }
- return bufferFrom(output, "binary");
- };
- }
-});
-
-// node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/textParsers.js
-var require_textParsers = __commonJS({
- "node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/textParsers.js"(exports, module) {
- var array = require_postgres_array();
- var arrayParser = require_arrayParser();
- var parseDate = require_postgres_date();
- var parseInterval = require_postgres_interval();
- var parseByteA = require_postgres_bytea();
- function allowNull(fn) {
- return function nullAllowed(value) {
- if (value === null) return value;
- return fn(value);
- };
- }
- function parseBool(value) {
- if (value === null) return value;
- return value === "TRUE" || value === "t" || value === "true" || value === "y" || value === "yes" || value === "on" || value === "1";
- }
- function parseBoolArray(value) {
- if (!value) return null;
- return array.parse(value, parseBool);
- }
- function parseBaseTenInt(string) {
- return parseInt(string, 10);
- }
- function parseIntegerArray(value) {
- if (!value) return null;
- return array.parse(value, allowNull(parseBaseTenInt));
- }
- function parseBigIntegerArray(value) {
- if (!value) return null;
- return array.parse(value, allowNull(function(entry) {
- return parseBigInteger(entry).trim();
- }));
- }
- var parsePointArray = function(value) {
- if (!value) {
- return null;
- }
- var p = arrayParser.create(value, function(entry) {
- if (entry !== null) {
- entry = parsePoint(entry);
- }
- return entry;
- });
- return p.parse();
- };
- var parseFloatArray = function(value) {
- if (!value) {
- return null;
- }
- var p = arrayParser.create(value, function(entry) {
- if (entry !== null) {
- entry = parseFloat(entry);
- }
- return entry;
- });
- return p.parse();
- };
- var parseStringArray = function(value) {
- if (!value) {
- return null;
- }
- var p = arrayParser.create(value);
- return p.parse();
- };
- var parseDateArray = function(value) {
- if (!value) {
- return null;
- }
- var p = arrayParser.create(value, function(entry) {
- if (entry !== null) {
- entry = parseDate(entry);
- }
- return entry;
- });
- return p.parse();
- };
- var parseIntervalArray = function(value) {
- if (!value) {
- return null;
- }
- var p = arrayParser.create(value, function(entry) {
- if (entry !== null) {
- entry = parseInterval(entry);
- }
- return entry;
- });
- return p.parse();
- };
- var parseByteAArray = function(value) {
- if (!value) {
- return null;
- }
- return array.parse(value, allowNull(parseByteA));
- };
- var parseInteger = function(value) {
- return parseInt(value, 10);
- };
- var parseBigInteger = function(value) {
- var valStr = String(value);
- if (/^\d+$/.test(valStr)) {
- return valStr;
- }
- return value;
- };
- var parseJsonArray = function(value) {
- if (!value) {
- return null;
- }
- return array.parse(value, allowNull(JSON.parse));
- };
- var parsePoint = function(value) {
- if (value[0] !== "(") {
- return null;
- }
- value = value.substring(1, value.length - 1).split(",");
- return {
- x: parseFloat(value[0]),
- y: parseFloat(value[1])
- };
- };
- var parseCircle = function(value) {
- if (value[0] !== "<" && value[1] !== "(") {
- return null;
- }
- var point = "(";
- var radius = "";
- var pointParsed = false;
- for (var i = 2; i < value.length - 1; i++) {
- if (!pointParsed) {
- point += value[i];
- }
- if (value[i] === ")") {
- pointParsed = true;
- continue;
- } else if (!pointParsed) {
- continue;
- }
- if (value[i] === ",") {
- continue;
- }
- radius += value[i];
- }
- var result = parsePoint(point);
- result.radius = parseFloat(radius);
- return result;
- };
- var init = function(register) {
- register(20, parseBigInteger);
- register(21, parseInteger);
- register(23, parseInteger);
- register(26, parseInteger);
- register(700, parseFloat);
- register(701, parseFloat);
- register(16, parseBool);
- register(1082, parseDate);
- register(1114, parseDate);
- register(1184, parseDate);
- register(600, parsePoint);
- register(651, parseStringArray);
- register(718, parseCircle);
- register(1e3, parseBoolArray);
- register(1001, parseByteAArray);
- register(1005, parseIntegerArray);
- register(1007, parseIntegerArray);
- register(1028, parseIntegerArray);
- register(1016, parseBigIntegerArray);
- register(1017, parsePointArray);
- register(1021, parseFloatArray);
- register(1022, parseFloatArray);
- register(1231, parseFloatArray);
- register(1014, parseStringArray);
- register(1015, parseStringArray);
- register(1008, parseStringArray);
- register(1009, parseStringArray);
- register(1040, parseStringArray);
- register(1041, parseStringArray);
- register(1115, parseDateArray);
- register(1182, parseDateArray);
- register(1185, parseDateArray);
- register(1186, parseInterval);
- register(1187, parseIntervalArray);
- register(17, parseByteA);
- register(114, JSON.parse.bind(JSON));
- register(3802, JSON.parse.bind(JSON));
- register(199, parseJsonArray);
- register(3807, parseJsonArray);
- register(3907, parseStringArray);
- register(2951, parseStringArray);
- register(791, parseStringArray);
- register(1183, parseStringArray);
- register(1270, parseStringArray);
- };
- module.exports = {
- init
- };
- }
-});
-
-// node_modules/.pnpm/pg-int8@1.0.1/node_modules/pg-int8/index.js
-var require_pg_int8 = __commonJS({
- "node_modules/.pnpm/pg-int8@1.0.1/node_modules/pg-int8/index.js"(exports, module) {
- "use strict";
- var BASE = 1e6;
- function readInt8(buffer) {
- var high = buffer.readInt32BE(0);
- var low = buffer.readUInt32BE(4);
- var sign = "";
- if (high < 0) {
- high = ~high + (low === 0);
- low = ~low + 1 >>> 0;
- sign = "-";
- }
- var result = "";
- var carry;
- var t;
- var digits;
- var pad;
- var l;
- var i;
- {
- carry = high % BASE;
- high = high / BASE >>> 0;
- t = 4294967296 * carry + low;
- low = t / BASE >>> 0;
- digits = "" + (t - BASE * low);
- if (low === 0 && high === 0) {
- return sign + digits + result;
- }
- pad = "";
- l = 6 - digits.length;
- for (i = 0; i < l; i++) {
- pad += "0";
- }
- result = pad + digits + result;
- }
- {
- carry = high % BASE;
- high = high / BASE >>> 0;
- t = 4294967296 * carry + low;
- low = t / BASE >>> 0;
- digits = "" + (t - BASE * low);
- if (low === 0 && high === 0) {
- return sign + digits + result;
- }
- pad = "";
- l = 6 - digits.length;
- for (i = 0; i < l; i++) {
- pad += "0";
- }
- result = pad + digits + result;
- }
- {
- carry = high % BASE;
- high = high / BASE >>> 0;
- t = 4294967296 * carry + low;
- low = t / BASE >>> 0;
- digits = "" + (t - BASE * low);
- if (low === 0 && high === 0) {
- return sign + digits + result;
- }
- pad = "";
- l = 6 - digits.length;
- for (i = 0; i < l; i++) {
- pad += "0";
- }
- result = pad + digits + result;
- }
- {
- carry = high % BASE;
- t = 4294967296 * carry + low;
- digits = "" + t % BASE;
- return sign + digits + result;
- }
- }
- module.exports = readInt8;
- }
-});
-
-// node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/binaryParsers.js
-var require_binaryParsers = __commonJS({
- "node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/binaryParsers.js"(exports, module) {
- var parseInt64 = require_pg_int8();
- var parseBits = function(data, bits, offset, invert, callback) {
- offset = offset || 0;
- invert = invert || false;
- callback = callback || function(lastValue, newValue, bits2) {
- return lastValue * Math.pow(2, bits2) + newValue;
- };
- var offsetBytes = offset >> 3;
- var inv = function(value) {
- if (invert) {
- return ~value & 255;
- }
- return value;
- };
- var mask = 255;
- var firstBits = 8 - offset % 8;
- if (bits < firstBits) {
- mask = 255 << 8 - bits & 255;
- firstBits = bits;
- }
- if (offset) {
- mask = mask >> offset % 8;
- }
- var result = 0;
- if (offset % 8 + bits >= 8) {
- result = callback(0, inv(data[offsetBytes]) & mask, firstBits);
- }
- var bytes = bits + offset >> 3;
- for (var i = offsetBytes + 1; i < bytes; i++) {
- result = callback(result, inv(data[i]), 8);
- }
- var lastBits = (bits + offset) % 8;
- if (lastBits > 0) {
- result = callback(result, inv(data[bytes]) >> 8 - lastBits, lastBits);
- }
- return result;
- };
- var parseFloatFromBits = function(data, precisionBits, exponentBits) {
- var bias = Math.pow(2, exponentBits - 1) - 1;
- var sign = parseBits(data, 1);
- var exponent = parseBits(data, exponentBits, 1);
- if (exponent === 0) {
- return 0;
- }
- var precisionBitsCounter = 1;
- var parsePrecisionBits = function(lastValue, newValue, bits) {
- if (lastValue === 0) {
- lastValue = 1;
- }
- for (var i = 1; i <= bits; i++) {
- precisionBitsCounter /= 2;
- if ((newValue & 1 << bits - i) > 0) {
- lastValue += precisionBitsCounter;
- }
- }
- return lastValue;
- };
- var mantissa = parseBits(data, precisionBits, exponentBits + 1, false, parsePrecisionBits);
- if (exponent == Math.pow(2, exponentBits + 1) - 1) {
- if (mantissa === 0) {
- return sign === 0 ? Infinity : -Infinity;
- }
- return NaN;
- }
- return (sign === 0 ? 1 : -1) * Math.pow(2, exponent - bias) * mantissa;
- };
- var parseInt16 = function(value) {
- if (parseBits(value, 1) == 1) {
- return -1 * (parseBits(value, 15, 1, true) + 1);
- }
- return parseBits(value, 15, 1);
- };
- var parseInt32 = function(value) {
- if (parseBits(value, 1) == 1) {
- return -1 * (parseBits(value, 31, 1, true) + 1);
- }
- return parseBits(value, 31, 1);
- };
- var parseFloat32 = function(value) {
- return parseFloatFromBits(value, 23, 8);
- };
- var parseFloat64 = function(value) {
- return parseFloatFromBits(value, 52, 11);
- };
- var parseNumeric = function(value) {
- var sign = parseBits(value, 16, 32);
- if (sign == 49152) {
- return NaN;
- }
- var weight = Math.pow(1e4, parseBits(value, 16, 16));
- var result = 0;
- var digits = [];
- var ndigits = parseBits(value, 16);
- for (var i = 0; i < ndigits; i++) {
- result += parseBits(value, 16, 64 + 16 * i) * weight;
- weight /= 1e4;
- }
- var scale = Math.pow(10, parseBits(value, 16, 48));
- return (sign === 0 ? 1 : -1) * Math.round(result * scale) / scale;
- };
- var parseDate = function(isUTC, value) {
- var sign = parseBits(value, 1);
- var rawValue = parseBits(value, 63, 1);
- var result = new Date((sign === 0 ? 1 : -1) * rawValue / 1e3 + 9466848e5);
- if (!isUTC) {
- result.setTime(result.getTime() + result.getTimezoneOffset() * 6e4);
- }
- result.usec = rawValue % 1e3;
- result.getMicroSeconds = function() {
- return this.usec;
- };
- result.setMicroSeconds = function(value2) {
- this.usec = value2;
- };
- result.getUTCMicroSeconds = function() {
- return this.usec;
- };
- return result;
- };
- var parseArray = function(value) {
- var dim = parseBits(value, 32);
- var flags = parseBits(value, 32, 32);
- var elementType = parseBits(value, 32, 64);
- var offset = 96;
- var dims = [];
- for (var i = 0; i < dim; i++) {
- dims[i] = parseBits(value, 32, offset);
- offset += 32;
- offset += 32;
- }
- var parseElement = function(elementType2) {
- var length = parseBits(value, 32, offset);
- offset += 32;
- if (length == 4294967295) {
- return null;
- }
- var result;
- if (elementType2 == 23 || elementType2 == 20) {
- result = parseBits(value, length * 8, offset);
- offset += length * 8;
- return result;
- } else if (elementType2 == 25) {
- result = value.toString(this.encoding, offset >> 3, (offset += length << 3) >> 3);
- return result;
- } else {
- console.log("ERROR: ElementType not implemented: " + elementType2);
- }
- };
- var parse = function(dimension, elementType2) {
- var array = [];
- var i2;
- if (dimension.length > 1) {
- var count = dimension.shift();
- for (i2 = 0; i2 < count; i2++) {
- array[i2] = parse(dimension, elementType2);
- }
- dimension.unshift(count);
- } else {
- for (i2 = 0; i2 < dimension[0]; i2++) {
- array[i2] = parseElement(elementType2);
- }
- }
- return array;
- };
- return parse(dims, elementType);
- };
- var parseText = function(value) {
- return value.toString("utf8");
- };
- var parseBool = function(value) {
- if (value === null) return null;
- return parseBits(value, 8) > 0;
- };
- var init = function(register) {
- register(20, parseInt64);
- register(21, parseInt16);
- register(23, parseInt32);
- register(26, parseInt32);
- register(1700, parseNumeric);
- register(700, parseFloat32);
- register(701, parseFloat64);
- register(16, parseBool);
- register(1114, parseDate.bind(null, false));
- register(1184, parseDate.bind(null, true));
- register(1e3, parseArray);
- register(1007, parseArray);
- register(1016, parseArray);
- register(1008, parseArray);
- register(1009, parseArray);
- register(25, parseText);
- };
- module.exports = {
- init
- };
- }
-});
-
-// node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/builtins.js
-var require_builtins = __commonJS({
- "node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/lib/builtins.js"(exports, module) {
- module.exports = {
- BOOL: 16,
- BYTEA: 17,
- CHAR: 18,
- INT8: 20,
- INT2: 21,
- INT4: 23,
- REGPROC: 24,
- TEXT: 25,
- OID: 26,
- TID: 27,
- XID: 28,
- CID: 29,
- JSON: 114,
- XML: 142,
- PG_NODE_TREE: 194,
- SMGR: 210,
- PATH: 602,
- POLYGON: 604,
- CIDR: 650,
- FLOAT4: 700,
- FLOAT8: 701,
- ABSTIME: 702,
- RELTIME: 703,
- TINTERVAL: 704,
- CIRCLE: 718,
- MACADDR8: 774,
- MONEY: 790,
- MACADDR: 829,
- INET: 869,
- ACLITEM: 1033,
- BPCHAR: 1042,
- VARCHAR: 1043,
- DATE: 1082,
- TIME: 1083,
- TIMESTAMP: 1114,
- TIMESTAMPTZ: 1184,
- INTERVAL: 1186,
- TIMETZ: 1266,
- BIT: 1560,
- VARBIT: 1562,
- NUMERIC: 1700,
- REFCURSOR: 1790,
- REGPROCEDURE: 2202,
- REGOPER: 2203,
- REGOPERATOR: 2204,
- REGCLASS: 2205,
- REGTYPE: 2206,
- UUID: 2950,
- TXID_SNAPSHOT: 2970,
- PG_LSN: 3220,
- PG_NDISTINCT: 3361,
- PG_DEPENDENCIES: 3402,
- TSVECTOR: 3614,
- TSQUERY: 3615,
- GTSVECTOR: 3642,
- REGCONFIG: 3734,
- REGDICTIONARY: 3769,
- JSONB: 3802,
- REGNAMESPACE: 4089,
- REGROLE: 4096
- };
- }
-});
-
-// node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/index.js
-var require_pg_types = __commonJS({
- "node_modules/.pnpm/pg-types@2.2.0/node_modules/pg-types/index.js"(exports) {
- var textParsers = require_textParsers();
- var binaryParsers = require_binaryParsers();
- var arrayParser = require_arrayParser();
- var builtinTypes = require_builtins();
- exports.getTypeParser = getTypeParser;
- exports.setTypeParser = setTypeParser;
- exports.arrayParser = arrayParser;
- exports.builtins = builtinTypes;
- var typeParsers = {
- text: {},
- binary: {}
- };
- function noParse(val) {
- return String(val);
- }
- function getTypeParser(oid, format) {
- format = format || "text";
- if (!typeParsers[format]) {
- return noParse;
- }
- return typeParsers[format][oid] || noParse;
- }
- function setTypeParser(oid, format, parseFn) {
- if (typeof format == "function") {
- parseFn = format;
- format = "text";
- }
- typeParsers[format][oid] = parseFn;
- }
- textParsers.init(function(oid, converter) {
- typeParsers.text[oid] = converter;
- });
- binaryParsers.init(function(oid, converter) {
- typeParsers.binary[oid] = converter;
- });
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/defaults.js
-var require_defaults = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/defaults.js"(exports, module) {
- "use strict";
- var user;
- try {
- user = process.platform === "win32" ? process.env.USERNAME : process.env.USER;
- } catch {
- }
- module.exports = {
- // database host. defaults to localhost
- host: "localhost",
- // database user's name
- user,
- // name of database to connect
- database: void 0,
- // database user's password
- password: null,
- // a Postgres connection string to be used instead of setting individual connection items
- // NOTE: Setting this value will cause it to override any other value (such as database or user) defined
- // in the defaults object.
- connectionString: void 0,
- // database port
- port: 5432,
- // number of rows to return at a time from a prepared statement's
- // portal. 0 will return all rows at once
- rows: 0,
- // binary result mode
- binary: false,
- // Connection pool options - see https://github.com/brianc/node-pg-pool
- // number of connections to use in connection pool
- // 0 will disable connection pooling
- max: 10,
- // max milliseconds a client can go unused before it is removed
- // from the pool and destroyed
- idleTimeoutMillis: 3e4,
- client_encoding: "",
- ssl: false,
- // SSL negotiation style: 'postgres' (traditional SSLRequest) or 'direct'
- sslnegotiation: void 0,
- application_name: void 0,
- fallback_application_name: void 0,
- options: void 0,
- parseInputDatesAsUTC: false,
- // max milliseconds any query using this connection will execute for before timing out in error.
- // false=unlimited
- statement_timeout: false,
- // Abort any statement that waits longer than the specified duration in milliseconds while attempting to acquire a lock.
- // false=unlimited
- lock_timeout: false,
- // Terminate any session with an open transaction that has been idle for longer than the specified duration in milliseconds
- // false=unlimited
- idle_in_transaction_session_timeout: false,
- // max milliseconds to wait for query to complete (client side)
- query_timeout: false,
- connect_timeout: 0,
- keepalives: 1,
- keepalives_idle: 0
- };
- var pgTypes = require_pg_types();
- var parseBigInteger = pgTypes.getTypeParser(20, "text");
- var parseBigIntegerArray = pgTypes.getTypeParser(1016, "text");
- module.exports.__defineSetter__("parseInt8", function(val) {
- pgTypes.setTypeParser(20, "text", val ? pgTypes.getTypeParser(23, "text") : parseBigInteger);
- pgTypes.setTypeParser(1016, "text", val ? pgTypes.getTypeParser(1007, "text") : parseBigIntegerArray);
- });
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/utils.js
-var require_utils = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/utils.js"(exports, module) {
- "use strict";
- var defaults2 = require_defaults();
- var { isDate } = __require("util/types");
- function escapeElement(elementRepresentation) {
- const escaped = elementRepresentation.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
- return '"' + escaped + '"';
- }
- function arrayString(val) {
- let result = "{";
- for (let i = 0; i < val.length; i++) {
- if (i > 0) {
- result += ",";
- }
- let item = val[i];
- if (item == null) {
- result += "NULL";
- } else if (Array.isArray(item)) {
- result += arrayString(item);
- } else if (ArrayBuffer.isView(item)) {
- if (!(item instanceof Buffer)) {
- item = Buffer.from(item.buffer, item.byteOffset, item.byteLength);
- }
- result += "\\\\x" + item.toString("hex");
- } else {
- result += escapeElement(prepareValue(item));
- }
- }
- result += "}";
- return result;
- }
- var prepareValue = function(val, seen) {
- if (val == null) {
- return null;
- }
- if (typeof val === "object") {
- if (val instanceof Buffer) {
- return val;
- }
- if (ArrayBuffer.isView(val)) {
- return Buffer.from(val.buffer, val.byteOffset, val.byteLength);
- }
- if (isDate(val)) {
- if (defaults2.parseInputDatesAsUTC) {
- return dateToStringUTC(val);
- } else {
- return dateToString(val);
- }
- }
- if (Array.isArray(val)) {
- return arrayString(val);
- }
- return prepareObject(val, seen);
- }
- return val.toString();
- };
- function prepareObject(val, seen) {
- if (val && typeof val.toPostgres === "function") {
- seen = seen || [];
- if (seen.indexOf(val) !== -1) {
- throw new Error('circular reference detected while preparing "' + val + '" for query');
- }
- seen.push(val);
- return prepareValue(val.toPostgres(prepareValue), seen);
- }
- return JSON.stringify(val);
- }
- function dateToString(date) {
- let offset = -date.getTimezoneOffset();
- let year = date.getFullYear();
- const isBCYear = year < 1;
- if (isBCYear) year = Math.abs(year) + 1;
- let ret = String(year).padStart(4, "0") + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0") + "T" + String(date.getHours()).padStart(2, "0") + ":" + String(date.getMinutes()).padStart(2, "0") + ":" + String(date.getSeconds()).padStart(2, "0") + "." + String(date.getMilliseconds()).padStart(3, "0");
- if (offset < 0) {
- ret += "-";
- offset *= -1;
- } else {
- ret += "+";
- }
- ret += String(Math.floor(offset / 60)).padStart(2, "0") + ":" + String(offset % 60).padStart(2, "0");
- if (isBCYear) ret += " BC";
- return ret;
- }
- function dateToStringUTC(date) {
- let year = date.getUTCFullYear();
- const isBCYear = year < 1;
- if (isBCYear) year = Math.abs(year) + 1;
- let ret = String(year).padStart(4, "0") + "-" + String(date.getUTCMonth() + 1).padStart(2, "0") + "-" + String(date.getUTCDate()).padStart(2, "0") + "T" + String(date.getUTCHours()).padStart(2, "0") + ":" + String(date.getUTCMinutes()).padStart(2, "0") + ":" + String(date.getUTCSeconds()).padStart(2, "0") + "." + String(date.getUTCMilliseconds()).padStart(3, "0");
- ret += "+00:00";
- if (isBCYear) ret += " BC";
- return ret;
- }
- function normalizeQueryConfig(config, values, callback) {
- config = typeof config === "string" ? { text: config } : config;
- if (values) {
- if (typeof values === "function") {
- config.callback = values;
- } else {
- config.values = values;
- }
- }
- if (callback) {
- config.callback = callback;
- }
- return config;
- }
- var escapeIdentifier2 = function(str) {
- return '"' + str.replace(/"/g, '""') + '"';
- };
- var escapeLiteral2 = function(str) {
- let hasBackslash = false;
- let escaped = "'";
- if (str == null) {
- return "''";
- }
- if (typeof str !== "string") {
- return "''";
- }
- for (let i = 0; i < str.length; i++) {
- const c = str[i];
- if (c === "'") {
- escaped += c + c;
- } else if (c === "\\") {
- escaped += c + c;
- hasBackslash = true;
- } else {
- escaped += c;
- }
- }
- escaped += "'";
- if (hasBackslash === true) {
- escaped = " E" + escaped;
- }
- return escaped;
- };
- module.exports = {
- prepareValue: function prepareValueWrapper(value) {
- return prepareValue(value);
- },
- normalizeQueryConfig,
- escapeIdentifier: escapeIdentifier2,
- escapeLiteral: escapeLiteral2
- };
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/crypto/utils.js
-var require_utils2 = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/crypto/utils.js"(exports, module) {
- var nodeCrypto = __require("crypto");
- module.exports = {
- postgresMd5PasswordHash,
- randomBytes,
- deriveKey,
- sha256,
- hashByName,
- hmacSha256,
- md5
- };
- var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
- var subtleCrypto = webCrypto.subtle;
- var textEncoder = new TextEncoder();
- function randomBytes(length) {
- return webCrypto.getRandomValues(Buffer.alloc(length));
- }
- async function md5(string) {
- try {
- return nodeCrypto.createHash("md5").update(string, "utf-8").digest("hex");
- } catch (e) {
- const data = typeof string === "string" ? textEncoder.encode(string) : string;
- const hash = await subtleCrypto.digest("MD5", data);
- return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
- }
- }
- async function postgresMd5PasswordHash(user, password, salt) {
- const inner = await md5(password + user);
- const outer = await md5(Buffer.concat([Buffer.from(inner), salt]));
- return "md5" + outer;
- }
- async function sha256(text) {
- return await subtleCrypto.digest("SHA-256", text);
- }
- async function hashByName(hashName, text) {
- return await subtleCrypto.digest(hashName, text);
- }
- async function hmacSha256(keyBuffer, msg) {
- const key = await subtleCrypto.importKey("raw", keyBuffer, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
- return await subtleCrypto.sign("HMAC", key, textEncoder.encode(msg));
- }
- async function deriveKey(password, salt, iterations) {
- const key = await subtleCrypto.importKey("raw", textEncoder.encode(password), "PBKDF2", false, ["deriveBits"]);
- const params = { name: "PBKDF2", hash: "SHA-256", salt, iterations };
- return await subtleCrypto.deriveBits(params, key, 32 * 8, ["deriveBits"]);
- }
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/crypto/cert-signatures.js
-var require_cert_signatures = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/crypto/cert-signatures.js"(exports, module) {
- function x509Error(msg, cert) {
- return new Error("SASL channel binding: " + msg + " when parsing public certificate " + cert.toString("base64"));
- }
- function readASN1Length(data, index) {
- let length = data[index++];
- if (length < 128) return { length, index };
- const lengthBytes = length & 127;
- if (lengthBytes > 4) throw x509Error("bad length", data);
- length = 0;
- for (let i = 0; i < lengthBytes; i++) {
- length = length << 8 | data[index++];
- }
- return { length, index };
- }
- function readASN1OID(data, index) {
- if (data[index++] !== 6) throw x509Error("non-OID data", data);
- const { length: OIDLength, index: indexAfterOIDLength } = readASN1Length(data, index);
- index = indexAfterOIDLength;
- const lastIndex = index + OIDLength;
- const byte1 = data[index++];
- let oid = (byte1 / 40 >> 0) + "." + byte1 % 40;
- while (index < lastIndex) {
- let value = 0;
- while (index < lastIndex) {
- const nextByte = data[index++];
- value = value << 7 | nextByte & 127;
- if (nextByte < 128) break;
- }
- oid += "." + value;
- }
- return { oid, index };
- }
- function expectASN1Seq(data, index) {
- if (data[index++] !== 48) throw x509Error("non-sequence data", data);
- return readASN1Length(data, index);
- }
- function signatureAlgorithmHashFromCertificate(data, index) {
- if (index === void 0) index = 0;
- index = expectASN1Seq(data, index).index;
- const { length: certInfoLength, index: indexAfterCertInfoLength } = expectASN1Seq(data, index);
- index = indexAfterCertInfoLength + certInfoLength;
- index = expectASN1Seq(data, index).index;
- const { oid, index: indexAfterOID } = readASN1OID(data, index);
- switch (oid) {
- // RSA
- case "1.2.840.113549.1.1.4":
- return "MD5";
- case "1.2.840.113549.1.1.5":
- return "SHA-1";
- case "1.2.840.113549.1.1.11":
- return "SHA-256";
- case "1.2.840.113549.1.1.12":
- return "SHA-384";
- case "1.2.840.113549.1.1.13":
- return "SHA-512";
- case "1.2.840.113549.1.1.14":
- return "SHA-224";
- case "1.2.840.113549.1.1.15":
- return "SHA512-224";
- case "1.2.840.113549.1.1.16":
- return "SHA512-256";
- // ECDSA
- case "1.2.840.10045.4.1":
- return "SHA-1";
- case "1.2.840.10045.4.3.1":
- return "SHA-224";
- case "1.2.840.10045.4.3.2":
- return "SHA-256";
- case "1.2.840.10045.4.3.3":
- return "SHA-384";
- case "1.2.840.10045.4.3.4":
- return "SHA-512";
- // RSASSA-PSS: hash is indicated separately
- case "1.2.840.113549.1.1.10": {
- index = indexAfterOID;
- index = expectASN1Seq(data, index).index;
- if (data[index++] !== 160) throw x509Error("non-tag data", data);
- index = readASN1Length(data, index).index;
- index = expectASN1Seq(data, index).index;
- const { oid: hashOID } = readASN1OID(data, index);
- switch (hashOID) {
- // standalone hash OIDs
- case "1.2.840.113549.2.5":
- return "MD5";
- case "1.3.14.3.2.26":
- return "SHA-1";
- case "2.16.840.1.101.3.4.2.1":
- return "SHA-256";
- case "2.16.840.1.101.3.4.2.2":
- return "SHA-384";
- case "2.16.840.1.101.3.4.2.3":
- return "SHA-512";
- }
- throw x509Error("unknown hash OID " + hashOID, data);
- }
- // Ed25519 -- see https: return//github.com/openssl/openssl/issues/15477
- case "1.3.101.110":
- case "1.3.101.112":
- return "SHA-512";
- // Ed448 -- still not in pg 17.2 (if supported, digest would be SHAKE256 x 64 bytes)
- case "1.3.101.111":
- case "1.3.101.113":
- throw x509Error("Ed448 certificate channel binding is not currently supported by Postgres");
- }
- throw x509Error("unknown OID " + oid, data);
- }
- module.exports = { signatureAlgorithmHashFromCertificate };
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/crypto/sasl.js
-var require_sasl = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/crypto/sasl.js"(exports, module) {
- "use strict";
- var crypto37 = require_utils2();
- var { signatureAlgorithmHashFromCertificate } = require_cert_signatures();
- function saslprep(password) {
- const nonAsciiSpace = /[\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000]/g;
- const mappedToNothing = /[\u00AD\u034F\u1806\u180B\u180C\u180D\u200C\u200D\u2060\uFE00-\uFE0F\uFEFF]/g;
- return password.replace(nonAsciiSpace, " ").replace(mappedToNothing, "").normalize("NFKC");
- }
- var DEFAULT_MAX_SCRAM_ITERATIONS = 1e5;
- function startSession(mechanisms, stream, scramMaxIterations = DEFAULT_MAX_SCRAM_ITERATIONS) {
- const candidates = ["SCRAM-SHA-256"];
- if (stream) candidates.unshift("SCRAM-SHA-256-PLUS");
- const mechanism = candidates.find((candidate) => mechanisms.includes(candidate));
- if (!mechanism) {
- throw new Error("SASL: Only mechanism(s) " + candidates.join(" and ") + " are supported");
- }
- if (mechanism === "SCRAM-SHA-256-PLUS" && typeof stream.getPeerCertificate !== "function") {
- throw new Error("SASL: Mechanism SCRAM-SHA-256-PLUS requires a certificate");
- }
- const clientNonce = crypto37.randomBytes(18).toString("base64");
- const gs2Header = mechanism === "SCRAM-SHA-256-PLUS" ? "p=tls-server-end-point" : stream ? "y" : "n";
- return {
- mechanism,
- clientNonce,
- response: gs2Header + ",,n=*,r=" + clientNonce,
- message: "SASLInitialResponse",
- scramMaxIterations
- };
- }
- async function continueSession(session, password, serverData, stream) {
- if (session.message !== "SASLInitialResponse") {
- throw new Error("SASL: Last message was not SASLInitialResponse");
- }
- if (typeof password !== "string") {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string");
- }
- if (password === "") {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a non-empty string");
- }
- if (typeof serverData !== "string") {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: serverData must be a string");
- }
- const sv = parseServerFirstMessage(serverData);
- if (!sv.nonce.startsWith(session.clientNonce)) {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce does not start with client nonce");
- } else if (sv.nonce.length === session.clientNonce.length) {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce is too short");
- }
- const scramMaxIterations = typeof session.scramMaxIterations === "number" ? session.scramMaxIterations : DEFAULT_MAX_SCRAM_ITERATIONS;
- if (scramMaxIterations !== 0 && sv.iteration > scramMaxIterations) {
- throw new Error(
- "SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration count " + sv.iteration + " exceeds scramMaxIterations of " + scramMaxIterations
- );
- }
- const clientFirstMessageBare = "n=*,r=" + session.clientNonce;
- const serverFirstMessage = "r=" + sv.nonce + ",s=" + sv.salt + ",i=" + sv.iteration;
- let channelBinding = stream ? "eSws" : "biws";
- if (session.mechanism === "SCRAM-SHA-256-PLUS") {
- const peerCert = stream.getPeerCertificate().raw;
- let hashName = signatureAlgorithmHashFromCertificate(peerCert);
- if (hashName === "MD5" || hashName === "SHA-1") hashName = "SHA-256";
- 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 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 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;
- }
- function finalizeSession(session, serverData) {
- if (session.message !== "SASLResponse") {
- throw new Error("SASL: Last message was not SASLResponse");
- }
- if (typeof serverData !== "string") {
- throw new Error("SASL: SCRAM-SERVER-FINAL-MESSAGE: serverData must be a string");
- }
- const { serverSignature } = parseServerFinalMessage(serverData);
- if (serverSignature !== session.serverSignature) {
- throw new Error("SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature does not match");
- }
- }
- function isPrintableChars(text) {
- if (typeof text !== "string") {
- throw new TypeError("SASL: text must be a string");
- }
- return text.split("").map((_, i) => text.charCodeAt(i)).every((c) => c >= 33 && c <= 43 || c >= 45 && c <= 126);
- }
- function isBase64(text) {
- return /^(?:[a-zA-Z0-9+/]{4})*(?:[a-zA-Z0-9+/]{2}==|[a-zA-Z0-9+/]{3}=)?$/.test(text);
- }
- function parseAttributePairs(text) {
- if (typeof text !== "string") {
- throw new TypeError("SASL: attribute pairs text must be a string");
- }
- return new Map(
- text.split(",").map((attrValue) => {
- if (!/^.=/.test(attrValue)) {
- throw new Error("SASL: Invalid attribute pair entry");
- }
- const name = attrValue[0];
- const value = attrValue.substring(2);
- return [name, value];
- })
- );
- }
- function parseServerFirstMessage(data) {
- const attrPairs = parseAttributePairs(data);
- const nonce = attrPairs.get("r");
- if (!nonce) {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce missing");
- } else if (!isPrintableChars(nonce)) {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: nonce must only contain printable characters");
- }
- const salt = attrPairs.get("s");
- if (!salt) {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: salt missing");
- } else if (!isBase64(salt)) {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: salt must be base64");
- }
- const iterationText = attrPairs.get("i");
- if (!iterationText) {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: iteration missing");
- } else if (!/^[1-9][0-9]*$/.test(iterationText)) {
- throw new Error("SASL: SCRAM-SERVER-FIRST-MESSAGE: invalid iteration count");
- }
- const iteration = parseInt(iterationText, 10);
- return {
- nonce,
- salt,
- iteration
- };
- }
- function parseServerFinalMessage(serverData) {
- const attrPairs = parseAttributePairs(serverData);
- const error = attrPairs.get("e");
- const serverSignature = attrPairs.get("v");
- if (error) {
- throw new Error(`SASL: SCRAM-SERVER-FINAL-MESSAGE: server returned error: "${error}"`);
- }
- if (!serverSignature) {
- throw new Error("SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature is missing");
- } else if (!isBase64(serverSignature)) {
- throw new Error("SASL: SCRAM-SERVER-FINAL-MESSAGE: server signature must be base64");
- }
- return {
- serverSignature
- };
- }
- function xorBuffers(a, b) {
- if (!Buffer.isBuffer(a)) {
- throw new TypeError("first argument must be a Buffer");
- }
- if (!Buffer.isBuffer(b)) {
- throw new TypeError("second argument must be a Buffer");
- }
- if (a.length !== b.length) {
- throw new Error("Buffer lengths must match");
- }
- if (a.length === 0) {
- throw new Error("Buffers cannot be empty");
- }
- return Buffer.from(a.map((_, i) => a[i] ^ b[i]));
- }
- module.exports = {
- startSession,
- continueSession,
- finalizeSession,
- DEFAULT_MAX_SCRAM_ITERATIONS
- };
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/type-overrides.js
-var require_type_overrides = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/type-overrides.js"(exports, module) {
- "use strict";
- var types2 = require_pg_types();
- function TypeOverrides2(userTypes) {
- this._types = userTypes || types2;
- this.text = {};
- this.binary = {};
- }
- TypeOverrides2.prototype.getOverrides = function(format) {
- switch (format) {
- case "text":
- return this.text;
- case "binary":
- return this.binary;
- default:
- return {};
- }
- };
- TypeOverrides2.prototype.setTypeParser = function(oid, format, parseFn) {
- if (typeof format === "function") {
- parseFn = format;
- format = "text";
- }
- this.getOverrides(format)[oid] = parseFn;
- };
- TypeOverrides2.prototype.getTypeParser = function(oid, format) {
- format = format || "text";
- return this.getOverrides(format)[oid] || this._types.getTypeParser(oid, format);
- };
- module.exports = TypeOverrides2;
- }
-});
-
-// node_modules/.pnpm/pg-connection-string@2.14.0/node_modules/pg-connection-string/index.js
-var require_pg_connection_string = __commonJS({
- "node_modules/.pnpm/pg-connection-string@2.14.0/node_modules/pg-connection-string/index.js"(exports, module) {
- "use strict";
- function parse(str, options = {}) {
- if (str.charAt(0) === "/") {
- const config2 = str.split(" ");
- return { host: config2[0], database: config2[1] };
- }
- const config = /* @__PURE__ */ Object.create(null);
- let result;
- let dummyHost = false;
- if (/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str)) {
- str = encodeURI(str).replace(/%25(\d\d)/g, "%$1");
- }
- try {
- try {
- result = new URL(str, "postgres://base");
- } catch (e) {
- result = new URL(str.replace("@/", "@___DUMMY___/"), "postgres://base");
- dummyHost = true;
- }
- } catch (err) {
- err.input && (err.input = "*****REDACTED*****");
- throw err;
- }
- for (const entry of result.searchParams.entries()) {
- config[entry[0]] = entry[1];
- }
- config.user = config.user || decodeURIComponent(result.username);
- config.password = config.password || decodeURIComponent(result.password);
- if (result.protocol == "socket:") {
- config.host = decodeURI(result.pathname);
- config.database = result.searchParams.get("db");
- config.client_encoding = result.searchParams.get("encoding");
- return config;
- }
- const hostname = dummyHost ? "" : result.hostname;
- if (!config.host) {
- config.host = decodeURIComponent(hostname);
- } else if (hostname && /^%2f/i.test(hostname)) {
- result.pathname = hostname + result.pathname;
- }
- if (!config.port) {
- config.port = result.port;
- }
- const pathname = result.pathname.slice(1) || null;
- config.database = pathname ? decodeURI(pathname) : null;
- if (config.ssl === "true" || config.ssl === "1") {
- config.ssl = true;
- }
- if (config.ssl === "0") {
- config.ssl = false;
- }
- if (config.sslcert || config.sslkey || config.sslrootcert || config.sslmode) {
- config.ssl = {};
- }
- if (config.sslnegotiation === "direct" && config.ssl === void 0) {
- config.ssl = true;
- }
- const fs33 = config.sslcert || config.sslkey || config.sslrootcert ? __require("fs") : null;
- if (config.sslcert) {
- config.ssl.cert = fs33.readFileSync(config.sslcert).toString();
- }
- if (config.sslkey) {
- config.ssl.key = fs33.readFileSync(config.sslkey).toString();
- }
- if (config.sslrootcert) {
- 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.");
- }
- if (config.uselibpqcompat === "true" || options.useLibpqCompat) {
- switch (config.sslmode) {
- case "disable": {
- config.ssl = false;
- break;
- }
- case "prefer": {
- config.ssl.rejectUnauthorized = false;
- break;
- }
- case "require": {
- if (config.sslrootcert) {
- config.ssl.checkServerIdentity = function() {
- };
- } else {
- config.ssl.rejectUnauthorized = false;
- }
- break;
- }
- case "verify-ca": {
- if (!config.ssl.ca) {
- throw new Error(
- "SECURITY WARNING: Using sslmode=verify-ca requires specifying a CA with sslrootcert. If a public CA is used, verify-ca allows connections to a server that somebody else may have registered with the CA, making you vulnerable to Man-in-the-Middle attacks. Either specify a custom CA certificate with sslrootcert parameter or use sslmode=verify-full for proper security."
- );
- }
- config.ssl.checkServerIdentity = function() {
- };
- break;
- }
- case "verify-full": {
- break;
- }
- }
- } else {
- switch (config.sslmode) {
- case "disable": {
- config.ssl = false;
- break;
- }
- case "prefer":
- case "require":
- case "verify-ca":
- case "verify-full": {
- if (config.sslmode !== "verify-full") {
- deprecatedSslModeWarning(config.sslmode);
- }
- break;
- }
- case "no-verify": {
- config.ssl.rejectUnauthorized = false;
- break;
- }
- }
- }
- return config;
- }
- function toConnectionOptions(sslConfig) {
- const connectionOptions = Object.entries(sslConfig).reduce((c, [key, value]) => {
- if (value !== void 0 && value !== null) {
- c[key] = value;
- }
- return c;
- }, /* @__PURE__ */ Object.create(null));
- return connectionOptions;
- }
- function toClientConfig(config) {
- const poolConfig = Object.entries(config).reduce((c, [key, value]) => {
- if (key === "ssl") {
- const sslConfig = value;
- if (typeof sslConfig === "boolean") {
- c[key] = sslConfig;
- }
- if (typeof sslConfig === "object") {
- c[key] = toConnectionOptions(sslConfig);
- }
- } else if (value !== void 0 && value !== null) {
- if (key === "port") {
- if (value !== "") {
- const v = parseInt(value, 10);
- if (isNaN(v)) {
- throw new Error(`Invalid ${key}: ${value}`);
- }
- c[key] = v;
- }
- } else {
- c[key] = value;
- }
- }
- return c;
- }, /* @__PURE__ */ Object.create(null));
- return poolConfig;
- }
- function parseIntoClientConfig(str) {
- return toClientConfig(parse(str));
- }
- function deprecatedSslModeWarning(sslmode) {
- if (!deprecatedSslModeWarning.warned && typeof process !== "undefined" && process.emitWarning) {
- deprecatedSslModeWarning.warned = true;
- process.emitWarning(`SECURITY WARNING: The SSL modes 'prefer', 'require', and 'verify-ca' are treated as aliases for 'verify-full'.
-In the next major version (pg-connection-string v3.0.0 and pg v9.0.0), these modes will adopt standard libpq semantics, which have weaker security guarantees.
-
-To prepare for this change:
-- If you want the current behavior, explicitly use 'sslmode=verify-full'
-- If you want libpq compatibility now, use 'uselibpqcompat=true&sslmode=${sslmode}'
-
-See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode definitions.`);
- }
- }
- module.exports = parse;
- parse.parse = parse;
- parse.toClientConfig = toClientConfig;
- parse.parseIntoClientConfig = parseIntoClientConfig;
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/connection-parameters.js
-var require_connection_parameters = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/connection-parameters.js"(exports, module) {
- "use strict";
- var dns = __require("dns");
- var defaults2 = require_defaults();
- var parse = require_pg_connection_string().parse;
- var val = function(key, config, envVar) {
- if (config[key]) {
- return config[key];
- }
- if (envVar === void 0) {
- envVar = process.env["PG" + key.toUpperCase()];
- } else if (envVar === false) {
- } else {
- envVar = process.env[envVar];
- }
- return envVar || defaults2[key];
- };
- var readSSLConfigFromEnvironment = function() {
- switch (process.env.PGSSLMODE) {
- case "disable":
- return false;
- case "prefer":
- case "require":
- case "verify-ca":
- case "verify-full":
- return true;
- case "no-verify":
- return { rejectUnauthorized: false };
- }
- return defaults2.ssl;
- };
- var quoteParamValue = function(value) {
- return "'" + ("" + value).replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'";
- };
- var add = function(params, config, paramName) {
- const value = config[paramName];
- if (value !== void 0 && value !== null) {
- params.push(paramName + "=" + quoteParamValue(value));
- }
- };
- var ConnectionParameters = class {
- constructor(config) {
- config = typeof config === "string" ? parse(config) : config || {};
- if (config.connectionString) {
- config = Object.assign({}, config, parse(config.connectionString));
- }
- this.user = val("user", config);
- this.database = val("database", config);
- if (this.database === void 0) {
- this.database = this.user;
- }
- this.port = parseInt(val("port", config), 10);
- this.host = val("host", config);
- Object.defineProperty(this, "password", {
- configurable: true,
- enumerable: false,
- writable: true,
- value: val("password", config)
- });
- this.binary = val("binary", config);
- this.options = val("options", config);
- this.ssl = typeof config.ssl === "undefined" ? readSSLConfigFromEnvironment() : config.ssl;
- if (typeof this.ssl === "string") {
- if (this.ssl === "true") {
- this.ssl = true;
- }
- }
- if (this.ssl === "no-verify") {
- this.ssl = { rejectUnauthorized: false };
- }
- if (this.ssl && this.ssl.key) {
- Object.defineProperty(this.ssl, "key", {
- enumerable: false
- });
- }
- this.sslnegotiation = val("sslnegotiation", config, "PGSSLNEGOTIATION");
- if (this.sslnegotiation !== void 0 && this.sslnegotiation !== "postgres" && this.sslnegotiation !== "direct") {
- throw new Error(
- `Invalid sslnegotiation value: "${this.sslnegotiation}". Valid values are "postgres" and "direct".`
- );
- }
- if (this.sslnegotiation === "direct" && !this.ssl) {
- throw new Error("sslnegotiation=direct requires SSL to be enabled");
- }
- this.client_encoding = val("client_encoding", config);
- this.replication = val("replication", config);
- this.isDomainSocket = !(this.host || "").indexOf("/");
- this.application_name = val("application_name", config, "PGAPPNAME");
- this.fallback_application_name = val("fallback_application_name", config, false);
- this.statement_timeout = val("statement_timeout", config, false);
- this.lock_timeout = val("lock_timeout", config, false);
- this.idle_in_transaction_session_timeout = val("idle_in_transaction_session_timeout", config, false);
- this.query_timeout = val("query_timeout", config, false);
- if (config.connectionTimeoutMillis === void 0) {
- this.connect_timeout = process.env.PGCONNECT_TIMEOUT || 0;
- } else {
- this.connect_timeout = Math.floor(config.connectionTimeoutMillis / 1e3);
- }
- if (config.keepAlive === false) {
- this.keepalives = 0;
- } else if (config.keepAlive === true) {
- this.keepalives = 1;
- }
- if (typeof config.keepAliveInitialDelayMillis === "number") {
- this.keepalives_idle = Math.floor(config.keepAliveInitialDelayMillis / 1e3);
- }
- }
- getLibpqConnectionString(cb) {
- const params = [];
- add(params, this, "user");
- add(params, this, "password");
- add(params, this, "port");
- add(params, this, "application_name");
- add(params, this, "fallback_application_name");
- add(params, this, "connect_timeout");
- add(params, this, "options");
- const ssl = typeof this.ssl === "object" ? this.ssl : this.ssl ? { sslmode: this.ssl } : {};
- add(params, ssl, "sslmode");
- add(params, ssl, "sslca");
- add(params, ssl, "sslkey");
- add(params, ssl, "sslcert");
- add(params, ssl, "sslrootcert");
- add(params, this, "sslnegotiation");
- if (this.database) {
- params.push("dbname=" + quoteParamValue(this.database));
- }
- if (this.replication) {
- params.push("replication=" + quoteParamValue(this.replication));
- }
- if (this.host) {
- params.push("host=" + quoteParamValue(this.host));
- }
- if (this.isDomainSocket) {
- return cb(null, params.join(" "));
- }
- if (this.client_encoding) {
- params.push("client_encoding=" + quoteParamValue(this.client_encoding));
- }
- dns.lookup(this.host, function(err, address) {
- if (err) return cb(err, null);
- params.push("hostaddr=" + quoteParamValue(address));
- return cb(null, params.join(" "));
- });
- }
- };
- module.exports = ConnectionParameters;
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/result.js
-var require_result = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/result.js"(exports, module) {
- "use strict";
- var types2 = require_pg_types();
- var matchRegexp = /^([A-Za-z]+)(?: (\d+))?(?: (\d+))?/;
- var Result2 = class {
- constructor(rowMode, types3) {
- this.command = null;
- this.rowCount = null;
- this.oid = null;
- this.rows = [];
- this.fields = [];
- this._parsers = void 0;
- this._types = types3;
- this.RowCtor = null;
- this.rowAsArray = rowMode === "array";
- if (this.rowAsArray) {
- this.parseRow = this._parseRowAsArray;
- }
- this._prebuiltEmptyResultObject = null;
- }
- // adds a command complete message
- addCommandComplete(msg) {
- let match;
- if (msg.text) {
- match = matchRegexp.exec(msg.text);
- } else {
- match = matchRegexp.exec(msg.command);
- }
- if (match) {
- this.command = match[1];
- if (match[3]) {
- this.oid = parseInt(match[2], 10);
- this.rowCount = parseInt(match[3], 10);
- } else if (match[2]) {
- this.rowCount = parseInt(match[2], 10);
- }
- }
- }
- _parseRowAsArray(rowData) {
- const row = new Array(rowData.length);
- for (let i = 0, len = rowData.length; i < len; i++) {
- const rawValue = rowData[i];
- if (rawValue !== null) {
- row[i] = this._parsers[i](rawValue);
- } else {
- row[i] = null;
- }
- }
- return row;
- }
- parseRow(rowData) {
- const row = { ...this._prebuiltEmptyResultObject };
- for (let i = 0, len = rowData.length; i < len; i++) {
- const rawValue = rowData[i];
- const field = this.fields[i].name;
- if (rawValue !== null) {
- const v = this.fields[i].format === "binary" ? Buffer.from(rawValue) : rawValue;
- row[field] = this._parsers[i](v);
- } else {
- row[field] = null;
- }
- }
- return row;
- }
- addRow(row) {
- this.rows.push(row);
- }
- addFields(fieldDescriptions) {
- this.fields = fieldDescriptions;
- if (this.fields.length) {
- this._parsers = new Array(fieldDescriptions.length);
- }
- const row = /* @__PURE__ */ Object.create(null);
- for (let i = 0; i < fieldDescriptions.length; i++) {
- const desc = fieldDescriptions[i];
- row[desc.name] = null;
- if (this._types) {
- this._parsers[i] = this._types.getTypeParser(desc.dataTypeID, desc.format || "text");
- } else {
- this._parsers[i] = types2.getTypeParser(desc.dataTypeID, desc.format || "text");
- }
- }
- this._prebuiltEmptyResultObject = { ...row };
- }
- };
- module.exports = Result2;
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/query.js
-var require_query = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/query.js"(exports, module) {
- "use strict";
- var { EventEmitter: EventEmitter2 } = __require("events");
- var Result2 = require_result();
- var utils = require_utils();
- var Query2 = class extends EventEmitter2 {
- constructor(config, values, callback) {
- super();
- config = utils.normalizeQueryConfig(config, values, callback);
- this.text = config.text;
- this.values = config.values;
- this.rows = config.rows;
- this.types = config.types;
- this.name = config.name;
- this.queryMode = config.queryMode;
- this.binary = config.binary;
- this.portal = config.portal || "";
- this.callback = config.callback;
- this._rowMode = config.rowMode;
- if (process.domain && config.callback) {
- this.callback = process.domain.bind(config.callback);
- }
- this._result = new Result2(this._rowMode, this.types);
- this._results = this._result;
- this._canceledDueToError = false;
- }
- requiresPreparation() {
- if (this.queryMode === "extended") {
- return true;
- }
- if (this.name) {
- return true;
- }
- if (this.rows) {
- return true;
- }
- if (!this.text) {
- return false;
- }
- if (!this.values) {
- return false;
- }
- return this.values.length > 0;
- }
- _checkForMultirow() {
- if (this._result.command) {
- if (!Array.isArray(this._results)) {
- this._results = [this._result];
- }
- this._result = new Result2(this._rowMode, this._result._types);
- this._results.push(this._result);
- }
- }
- // associates row metadata from the supplied
- // message with this query object
- // metadata used when parsing row results
- handleRowDescription(msg) {
- this._checkForMultirow();
- this._result.addFields(msg.fields);
- this._accumulateRows = this.callback || !this.listeners("row").length;
- }
- handleDataRow(msg) {
- let row;
- if (this._canceledDueToError) {
- return;
- }
- try {
- row = this._result.parseRow(msg.fields);
- } catch (err) {
- this._canceledDueToError = err;
- return;
- }
- this.emit("row", row, this._result);
- if (this._accumulateRows) {
- this._result.addRow(row);
- }
- }
- handleCommandComplete(msg, connection) {
- this._checkForMultirow();
- this._result.addCommandComplete(msg);
- if (this.rows) {
- connection.sync();
- }
- }
- // if a named prepared statement is created with empty query text
- // the backend will send an emptyQuery message but *not* a command complete message
- // since we pipeline sync immediately after execute we don't need to do anything here
- // unless we have rows specified, in which case we did not pipeline the initial sync call
- handleEmptyQuery(connection) {
- if (this.rows) {
- connection.sync();
- }
- }
- handleError(err, connection) {
- if (this._canceledDueToError) {
- err = this._canceledDueToError;
- this._canceledDueToError = false;
- }
- if (this.callback) {
- return this.callback(err);
- }
- this.emit("error", err);
- }
- handleReadyForQuery(con) {
- if (this._canceledDueToError) {
- return this.handleError(this._canceledDueToError, con);
- }
- if (this.callback) {
- try {
- this.callback(null, this._results);
- } catch (err) {
- process.nextTick(() => {
- throw err;
- });
- }
- }
- this.emit("end", this._results);
- }
- submit(connection) {
- if (typeof this.text !== "string" && typeof this.name !== "string") {
- return new Error("A query must have either text or a name. Supplying neither is unsupported.");
- }
- const previous = connection.parsedStatements[this.name];
- if (this.text && previous && this.text !== previous) {
- return new Error(`Prepared statements must be unique - '${this.name}' was used for a different statement`);
- }
- if (this.values && !Array.isArray(this.values)) {
- return new Error("Query values must be an array");
- }
- if (this.requiresPreparation()) {
- connection.stream.cork && connection.stream.cork();
- try {
- this.prepare(connection);
- } finally {
- connection.stream.uncork && connection.stream.uncork();
- }
- } else {
- connection.query(this.text);
- }
- return null;
- }
- hasBeenParsed(connection) {
- return this.name && connection.parsedStatements[this.name];
- }
- handlePortalSuspended(connection) {
- this._getRows(connection, this.rows);
- }
- _getRows(connection, rows) {
- connection.execute({
- portal: this.portal,
- rows
- });
- if (!rows) {
- connection.sync();
- } else {
- connection.flush();
- }
- }
- // http://developer.postgresql.org/pgdocs/postgres/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY
- prepare(connection) {
- if (!this.hasBeenParsed(connection)) {
- connection.parse({
- text: this.text,
- name: this.name,
- types: this.types
- });
- }
- try {
- connection.bind({
- portal: this.portal,
- statement: this.name,
- values: this.values,
- binary: this.binary,
- valueMapper: utils.prepareValue
- });
- } catch (err) {
- connection.close({ type: "S", name: this.name });
- connection.sync();
- this.handleError(err, connection);
- return;
- }
- connection.describe({
- type: "P",
- name: this.portal || ""
- });
- this._getRows(connection, this.rows);
- }
- handleCopyInResponse(connection) {
- connection.sendCopyFail("No source stream defined");
- }
- handleCopyData(msg, connection) {
- }
- };
- module.exports = Query2;
- }
-});
-
-// node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/messages.js
-var require_messages = __commonJS({
- "node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/messages.js"(exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.NoticeMessage = exports.DataRowMessage = exports.CommandCompleteMessage = exports.ReadyForQueryMessage = exports.NotificationResponseMessage = exports.BackendKeyDataMessage = exports.AuthenticationMD5Password = exports.ParameterStatusMessage = exports.ParameterDescriptionMessage = exports.RowDescriptionMessage = exports.Field = exports.CopyResponse = exports.CopyDataMessage = exports.DatabaseError = exports.copyDone = exports.emptyQuery = exports.replicationStart = exports.portalSuspended = exports.noData = exports.closeComplete = exports.bindComplete = exports.parseComplete = void 0;
- exports.parseComplete = {
- name: "parseComplete",
- length: 5
- };
- exports.bindComplete = {
- name: "bindComplete",
- length: 5
- };
- exports.closeComplete = {
- name: "closeComplete",
- length: 5
- };
- exports.noData = {
- name: "noData",
- length: 5
- };
- exports.portalSuspended = {
- name: "portalSuspended",
- length: 5
- };
- exports.replicationStart = {
- name: "replicationStart",
- length: 4
- };
- exports.emptyQuery = {
- name: "emptyQuery",
- length: 4
- };
- exports.copyDone = {
- name: "copyDone",
- length: 4
- };
- var DatabaseError2 = class extends Error {
- constructor(message, length, name) {
- super(message);
- this.length = length;
- this.name = name;
- }
- };
- exports.DatabaseError = DatabaseError2;
- var CopyDataMessage = class {
- constructor(length, chunk) {
- this.length = length;
- this.chunk = chunk;
- this.name = "copyData";
- }
- };
- exports.CopyDataMessage = CopyDataMessage;
- var CopyResponse = class {
- constructor(length, name, binary, columnCount) {
- this.length = length;
- this.name = name;
- this.binary = binary;
- this.columnTypes = new Array(columnCount);
- }
- };
- exports.CopyResponse = CopyResponse;
- var Field = class {
- constructor(name, tableID, columnID, dataTypeID, dataTypeSize, dataTypeModifier, format) {
- this.name = name;
- this.tableID = tableID;
- this.columnID = columnID;
- this.dataTypeID = dataTypeID;
- this.dataTypeSize = dataTypeSize;
- this.dataTypeModifier = dataTypeModifier;
- this.format = format;
- }
- };
- exports.Field = Field;
- var RowDescriptionMessage = class {
- constructor(length, fieldCount) {
- this.length = length;
- this.fieldCount = fieldCount;
- this.name = "rowDescription";
- this.fields = new Array(this.fieldCount);
- }
- };
- exports.RowDescriptionMessage = RowDescriptionMessage;
- var ParameterDescriptionMessage = class {
- constructor(length, parameterCount) {
- this.length = length;
- this.parameterCount = parameterCount;
- this.name = "parameterDescription";
- this.dataTypeIDs = new Array(this.parameterCount);
- }
- };
- exports.ParameterDescriptionMessage = ParameterDescriptionMessage;
- var ParameterStatusMessage = class {
- constructor(length, parameterName, parameterValue) {
- this.length = length;
- this.parameterName = parameterName;
- this.parameterValue = parameterValue;
- this.name = "parameterStatus";
- }
- };
- exports.ParameterStatusMessage = ParameterStatusMessage;
- var AuthenticationMD5Password = class {
- constructor(length, salt) {
- this.length = length;
- this.salt = salt;
- this.name = "authenticationMD5Password";
- }
- };
- exports.AuthenticationMD5Password = AuthenticationMD5Password;
- var BackendKeyDataMessage = class {
- constructor(length, processID, secretKey) {
- this.length = length;
- this.processID = processID;
- this.secretKey = secretKey;
- this.name = "backendKeyData";
- }
- };
- exports.BackendKeyDataMessage = BackendKeyDataMessage;
- var NotificationResponseMessage = class {
- constructor(length, processId, channel, payload) {
- this.length = length;
- this.processId = processId;
- this.channel = channel;
- this.payload = payload;
- this.name = "notification";
- }
- };
- exports.NotificationResponseMessage = NotificationResponseMessage;
- var ReadyForQueryMessage = class {
- constructor(length, status) {
- this.length = length;
- this.status = status;
- this.name = "readyForQuery";
- }
- };
- exports.ReadyForQueryMessage = ReadyForQueryMessage;
- var CommandCompleteMessage = class {
- constructor(length, text) {
- this.length = length;
- this.text = text;
- this.name = "commandComplete";
- }
- };
- exports.CommandCompleteMessage = CommandCompleteMessage;
- var DataRowMessage = class {
- constructor(length, fields) {
- this.length = length;
- this.fields = fields;
- this.name = "dataRow";
- this.fieldCount = fields.length;
- }
- };
- exports.DataRowMessage = DataRowMessage;
- var NoticeMessage = class {
- constructor(length, message) {
- this.length = length;
- this.message = message;
- this.name = "notice";
- }
- };
- exports.NoticeMessage = NoticeMessage;
- }
-});
-
-// node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/buffer-writer.js
-var require_buffer_writer = __commonJS({
- "node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/buffer-writer.js"(exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Writer = void 0;
- var Writer = class {
- constructor(size = 256) {
- this.size = size;
- this.offset = 5;
- this.headerPosition = 0;
- this.buffer = Buffer.allocUnsafe(size);
- }
- ensure(size) {
- const remaining = this.buffer.length - this.offset;
- if (remaining < size) {
- const oldBuffer = this.buffer;
- const newSize = oldBuffer.length + (oldBuffer.length >> 1) + size;
- this.buffer = Buffer.allocUnsafe(newSize);
- oldBuffer.copy(this.buffer);
- }
- }
- addInt32(num) {
- this.ensure(4);
- this.buffer[this.offset++] = num >>> 24 & 255;
- this.buffer[this.offset++] = num >>> 16 & 255;
- this.buffer[this.offset++] = num >>> 8 & 255;
- this.buffer[this.offset++] = num >>> 0 & 255;
- return this;
- }
- addInt16(num) {
- this.ensure(2);
- this.buffer[this.offset++] = num >>> 8 & 255;
- this.buffer[this.offset++] = num >>> 0 & 255;
- return this;
- }
- addCString(string) {
- if (!string) {
- this.ensure(1);
- } else {
- const len = Buffer.byteLength(string);
- this.ensure(len + 1);
- this.buffer.write(string, this.offset, "utf-8");
- this.offset += len;
- }
- this.buffer[this.offset++] = 0;
- return this;
- }
- addString(string = "") {
- const len = Buffer.byteLength(string);
- this.ensure(len);
- this.buffer.write(string, this.offset);
- this.offset += len;
- return this;
- }
- // Write an Int32 byte-length prefix immediately followed by the string's UTF-8
- // bytes. Postgres' Bind wire format prefixes every parameter with its length,
- // and doing it in one method computes Buffer.byteLength ONCE — the previous
- // `addInt32(Buffer.byteLength(s)).addString(s)` pairing scanned the string
- // three times (byteLength for the prefix, byteLength again inside addString,
- // then the encode), which is costly for large text parameters.
- addInt32PrefixedString(string) {
- const len = Buffer.byteLength(string);
- this.ensure(4 + len);
- const buffer = this.buffer;
- let offset = this.offset;
- buffer[offset++] = len >>> 24 & 255;
- buffer[offset++] = len >>> 16 & 255;
- buffer[offset++] = len >>> 8 & 255;
- buffer[offset++] = len >>> 0 & 255;
- buffer.write(string, offset, "utf-8");
- this.offset = offset + len;
- return this;
- }
- add(otherBuffer) {
- this.ensure(otherBuffer.length);
- otherBuffer.copy(this.buffer, this.offset);
- this.offset += otherBuffer.length;
- return this;
- }
- join(code) {
- if (code) {
- this.buffer[this.headerPosition] = code;
- const length = this.offset - (this.headerPosition + 1);
- this.buffer.writeInt32BE(length, this.headerPosition + 1);
- }
- return this.buffer.slice(code ? 0 : 5, this.offset);
- }
- flush(code) {
- const result = this.join(code);
- this.offset = 5;
- this.headerPosition = 0;
- this.buffer = Buffer.allocUnsafe(this.size);
- return result;
- }
- clear() {
- this.offset = 5;
- this.headerPosition = 0;
- }
- };
- exports.Writer = Writer;
- }
-});
-
-// node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/serializer.js
-var require_serializer = __commonJS({
- "node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/serializer.js"(exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.serialize = void 0;
- var buffer_writer_1 = require_buffer_writer();
- var writer = new buffer_writer_1.Writer();
- var startup = (opts) => {
- writer.addInt16(3).addInt16(0);
- for (const key of Object.keys(opts)) {
- writer.addCString(key).addCString(opts[key]);
- }
- writer.addCString("client_encoding").addCString("UTF8");
- const bodyBuffer = writer.addCString("").flush();
- const length = bodyBuffer.length + 4;
- return new buffer_writer_1.Writer().addInt32(length).add(bodyBuffer).flush();
- };
- var requestSsl = () => {
- const response = Buffer.allocUnsafe(8);
- response.writeInt32BE(8, 0);
- response.writeInt32BE(80877103, 4);
- return response;
- };
- var password = (password2) => {
- return writer.addCString(password2).flush(
- 112
- /* code.startup */
- );
- };
- var sendSASLInitialResponseMessage = function(mechanism, initialResponse) {
- writer.addCString(mechanism).addInt32PrefixedString(initialResponse);
- return writer.flush(
- 112
- /* code.startup */
- );
- };
- var sendSCRAMClientFinalMessage = function(additionalData) {
- return writer.addString(additionalData).flush(
- 112
- /* code.startup */
- );
- };
- var query = (text) => {
- return writer.addCString(text).flush(
- 81
- /* code.query */
- );
- };
- var emptyArray = [];
- var parse = (query2) => {
- const name = query2.name || "";
- if (name.length > 63) {
- console.error("Warning! Postgres only supports 63 characters for query names.");
- console.error("You supplied %s (%s)", name, name.length);
- console.error("This can cause conflicts and silent errors executing queries");
- }
- const types2 = query2.types || emptyArray;
- const len = types2.length;
- const buffer = writer.addCString(name).addCString(query2.text).addInt16(len);
- for (let i = 0; i < len; i++) {
- buffer.addInt32(types2[i]);
- }
- return writer.flush(
- 80
- /* code.parse */
- );
- };
- var paramWriter = new buffer_writer_1.Writer();
- var writeValues = function(values, valueMapper) {
- for (let i = 0; i < values.length; i++) {
- const mappedVal = valueMapper ? valueMapper(values[i], i) : values[i];
- if (mappedVal == null) {
- writer.addInt16(
- 0
- /* ParamType.STRING */
- );
- paramWriter.addInt32(-1);
- } else if (mappedVal instanceof Buffer) {
- writer.addInt16(
- 1
- /* ParamType.BINARY */
- );
- paramWriter.addInt32(mappedVal.length);
- paramWriter.add(mappedVal);
- } else {
- writer.addInt16(
- 0
- /* ParamType.STRING */
- );
- paramWriter.addInt32PrefixedString(mappedVal);
- }
- }
- };
- var bind = (config = {}) => {
- const portal = config.portal || "";
- const statement = config.statement || "";
- const binary = config.binary || false;
- const values = config.values || emptyArray;
- const len = values.length;
- writer.addCString(portal).addCString(statement);
- writer.addInt16(len);
- try {
- writeValues(values, config.valueMapper);
- } catch (err) {
- writer.clear();
- paramWriter.clear();
- throw err;
- }
- writer.addInt16(len);
- writer.add(paramWriter.flush());
- writer.addInt16(1);
- writer.addInt16(
- binary ? 1 : 0
- /* ParamType.STRING */
- );
- return writer.flush(
- 66
- /* code.bind */
- );
- };
- var emptyExecute = Buffer.from([69, 0, 0, 0, 9, 0, 0, 0, 0, 0]);
- var execute = (config) => {
- if (!config || !config.portal && !config.rows) {
- return emptyExecute;
- }
- const portal = config.portal || "";
- const rows = config.rows || 0;
- const portalLength = Buffer.byteLength(portal);
- const len = 4 + portalLength + 1 + 4;
- const buff = Buffer.allocUnsafe(1 + len);
- buff[0] = 69;
- buff.writeInt32BE(len, 1);
- buff.write(portal, 5, "utf-8");
- buff[portalLength + 5] = 0;
- buff.writeUInt32BE(rows, buff.length - 4);
- return buff;
- };
- var cancel = (processID, secretKey) => {
- const buffer = Buffer.allocUnsafe(16);
- buffer.writeInt32BE(16, 0);
- buffer.writeInt16BE(1234, 4);
- buffer.writeInt16BE(5678, 6);
- buffer.writeInt32BE(processID, 8);
- buffer.writeInt32BE(secretKey, 12);
- return buffer;
- };
- var cstringMessage = (code, string) => {
- const stringLen = Buffer.byteLength(string);
- const len = 4 + stringLen + 1;
- const buffer = Buffer.allocUnsafe(1 + len);
- buffer[0] = code;
- buffer.writeInt32BE(len, 1);
- buffer.write(string, 5, "utf-8");
- buffer[len] = 0;
- return buffer;
- };
- var emptyDescribePortal = writer.addCString("P").flush(
- 68
- /* code.describe */
- );
- var emptyDescribeStatement = writer.addCString("S").flush(
- 68
- /* code.describe */
- );
- var describe = (msg) => {
- return msg.name ? cstringMessage(68, `${msg.type}${msg.name || ""}`) : msg.type === "P" ? emptyDescribePortal : emptyDescribeStatement;
- };
- var close = (msg) => {
- const text = `${msg.type}${msg.name || ""}`;
- return cstringMessage(67, text);
- };
- var copyData = (chunk) => {
- return writer.add(chunk).flush(
- 100
- /* code.copyFromChunk */
- );
- };
- var copyFail = (message) => {
- return cstringMessage(102, message);
- };
- var codeOnlyBuffer = (code) => Buffer.from([code, 0, 0, 0, 4]);
- var flushBuffer = codeOnlyBuffer(
- 72
- /* code.flush */
- );
- var syncBuffer = codeOnlyBuffer(
- 83
- /* code.sync */
- );
- var endBuffer = codeOnlyBuffer(
- 88
- /* code.end */
- );
- var copyDoneBuffer = codeOnlyBuffer(
- 99
- /* code.copyDone */
- );
- var serialize = {
- startup,
- password,
- requestSsl,
- sendSASLInitialResponseMessage,
- sendSCRAMClientFinalMessage,
- query,
- parse,
- bind,
- execute,
- describe,
- close,
- flush: () => flushBuffer,
- sync: () => syncBuffer,
- end: () => endBuffer,
- copyData,
- copyDone: () => copyDoneBuffer,
- copyFail,
- cancel
- };
- exports.serialize = serialize;
- }
-});
-
-// node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/buffer-reader.js
-var require_buffer_reader = __commonJS({
- "node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/buffer-reader.js"(exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.BufferReader = void 0;
- var BufferReader = class {
- constructor(offset = 0) {
- this.offset = offset;
- this.buffer = Buffer.allocUnsafe(0);
- this.encoding = "utf-8";
- }
- setBuffer(offset, buffer) {
- this.offset = offset;
- this.buffer = buffer;
- }
- int16() {
- const result = this.buffer.readInt16BE(this.offset);
- this.offset += 2;
- return result;
- }
- byte() {
- const result = this.buffer[this.offset];
- this.offset++;
- return result;
- }
- int32() {
- const result = this.buffer.readInt32BE(this.offset);
- this.offset += 4;
- return result;
- }
- uint32() {
- const result = this.buffer.readUInt32BE(this.offset);
- this.offset += 4;
- return result;
- }
- string(length) {
- const result = this.buffer.toString(this.encoding, this.offset, this.offset + length);
- this.offset += length;
- return result;
- }
- cstring() {
- const start = this.offset;
- let end = start;
- while (this.buffer[end++]) {
- }
- this.offset = end;
- return this.buffer.toString(this.encoding, start, end - 1);
- }
- bytes(length) {
- const result = this.buffer.slice(this.offset, this.offset + length);
- this.offset += length;
- return result;
- }
- };
- exports.BufferReader = BufferReader;
- }
-});
-
-// node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/parser.js
-var require_parser = __commonJS({
- "node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/parser.js"(exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Parser = void 0;
- var messages_1 = require_messages();
- var buffer_reader_1 = require_buffer_reader();
- var CODE_LENGTH = 1;
- var LEN_LENGTH = 4;
- var HEADER_LENGTH = CODE_LENGTH + LEN_LENGTH;
- var LATEINIT_LENGTH = -1;
- var emptyBuffer = Buffer.allocUnsafe(0);
- var Parser = class {
- constructor(opts) {
- this.buffer = emptyBuffer;
- this.bufferLength = 0;
- this.bufferOffset = 0;
- this.reader = new buffer_reader_1.BufferReader();
- if ((opts === null || opts === void 0 ? void 0 : opts.mode) === "binary") {
- throw new Error("Binary mode not supported yet");
- }
- this.mode = (opts === null || opts === void 0 ? void 0 : opts.mode) || "text";
- }
- parse(buffer, callback) {
- this.mergeBuffer(buffer);
- const bufferFullLength = this.bufferOffset + this.bufferLength;
- let offset = this.bufferOffset;
- while (offset + HEADER_LENGTH <= bufferFullLength) {
- const code = this.buffer[offset];
- const length = this.buffer.readUInt32BE(offset + CODE_LENGTH);
- const fullMessageLength = CODE_LENGTH + length;
- if (fullMessageLength + offset <= bufferFullLength) {
- const message = this.handlePacket(offset + HEADER_LENGTH, code, length, this.buffer);
- callback(message);
- offset += fullMessageLength;
- } else {
- break;
- }
- }
- if (offset === bufferFullLength) {
- this.buffer = emptyBuffer;
- this.bufferLength = 0;
- this.bufferOffset = 0;
- } else {
- this.bufferLength = bufferFullLength - offset;
- this.bufferOffset = offset;
- }
- }
- mergeBuffer(buffer) {
- if (this.bufferLength > 0) {
- const newLength = this.bufferLength + buffer.byteLength;
- const newFullLength = newLength + this.bufferOffset;
- if (newFullLength > this.buffer.byteLength) {
- let newBuffer;
- if (newLength <= this.buffer.byteLength && this.bufferOffset >= this.bufferLength) {
- newBuffer = this.buffer;
- } else {
- let newBufferLength = this.buffer.byteLength * 2;
- while (newLength >= newBufferLength) {
- newBufferLength *= 2;
- }
- newBuffer = Buffer.allocUnsafe(newBufferLength);
- }
- this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset + this.bufferLength);
- this.buffer = newBuffer;
- this.bufferOffset = 0;
- }
- buffer.copy(this.buffer, this.bufferOffset + this.bufferLength);
- this.bufferLength = newLength;
- } else {
- this.buffer = buffer;
- this.bufferOffset = 0;
- this.bufferLength = buffer.byteLength;
- }
- }
- handlePacket(offset, code, length, bytes) {
- const { reader } = this;
- reader.setBuffer(offset, bytes);
- let message;
- switch (code) {
- case 50:
- message = messages_1.bindComplete;
- break;
- case 49:
- message = messages_1.parseComplete;
- break;
- case 51:
- message = messages_1.closeComplete;
- break;
- case 110:
- message = messages_1.noData;
- break;
- case 115:
- message = messages_1.portalSuspended;
- break;
- case 99:
- message = messages_1.copyDone;
- break;
- case 87:
- message = messages_1.replicationStart;
- break;
- case 73:
- message = messages_1.emptyQuery;
- break;
- case 68:
- message = parseDataRowMessage(reader);
- break;
- case 67:
- message = parseCommandCompleteMessage(reader);
- break;
- case 90:
- message = parseReadyForQueryMessage(reader);
- break;
- case 65:
- message = parseNotificationMessage(reader);
- break;
- case 82:
- message = parseAuthenticationResponse(reader, length);
- break;
- case 83:
- message = parseParameterStatusMessage(reader);
- break;
- case 75:
- message = parseBackendKeyData(reader);
- break;
- case 69:
- message = parseErrorMessage(reader, "error");
- break;
- case 78:
- message = parseErrorMessage(reader, "notice");
- break;
- case 84:
- message = parseRowDescriptionMessage(reader);
- break;
- case 116:
- message = parseParameterDescriptionMessage(reader);
- break;
- case 71:
- message = parseCopyInMessage(reader);
- break;
- case 72:
- message = parseCopyOutMessage(reader);
- break;
- case 100:
- message = parseCopyData(reader, length);
- break;
- default:
- return new messages_1.DatabaseError("received invalid response: " + code.toString(16), length, "error");
- }
- reader.setBuffer(0, emptyBuffer);
- message.length = length;
- return message;
- }
- };
- exports.Parser = Parser;
- var parseReadyForQueryMessage = (reader) => {
- const status = reader.string(1);
- return new messages_1.ReadyForQueryMessage(LATEINIT_LENGTH, status);
- };
- var parseCommandCompleteMessage = (reader) => {
- const text = reader.cstring();
- return new messages_1.CommandCompleteMessage(LATEINIT_LENGTH, text);
- };
- var parseCopyData = (reader, length) => {
- const chunk = reader.bytes(length - 4);
- return new messages_1.CopyDataMessage(LATEINIT_LENGTH, chunk);
- };
- var parseCopyInMessage = (reader) => parseCopyMessage(reader, "copyInResponse");
- var parseCopyOutMessage = (reader) => parseCopyMessage(reader, "copyOutResponse");
- var parseCopyMessage = (reader, messageName) => {
- const isBinary = reader.byte() !== 0;
- const columnCount = reader.int16();
- const message = new messages_1.CopyResponse(LATEINIT_LENGTH, messageName, isBinary, columnCount);
- for (let i = 0; i < columnCount; i++) {
- message.columnTypes[i] = reader.int16();
- }
- return message;
- };
- var parseNotificationMessage = (reader) => {
- const processId = reader.int32();
- const channel = reader.cstring();
- const payload = reader.cstring();
- return new messages_1.NotificationResponseMessage(LATEINIT_LENGTH, processId, channel, payload);
- };
- var parseRowDescriptionMessage = (reader) => {
- const fieldCount = reader.int16();
- const message = new messages_1.RowDescriptionMessage(LATEINIT_LENGTH, fieldCount);
- for (let i = 0; i < fieldCount; i++) {
- message.fields[i] = parseField(reader);
- }
- return message;
- };
- var parseField = (reader) => {
- const name = reader.cstring();
- const tableID = reader.uint32();
- const columnID = reader.int16();
- const dataTypeID = reader.uint32();
- const dataTypeSize = reader.int16();
- const dataTypeModifier = reader.int32();
- const mode = reader.int16() === 0 ? "text" : "binary";
- return new messages_1.Field(name, tableID, columnID, dataTypeID, dataTypeSize, dataTypeModifier, mode);
- };
- var parseParameterDescriptionMessage = (reader) => {
- const parameterCount = reader.int16();
- const message = new messages_1.ParameterDescriptionMessage(LATEINIT_LENGTH, parameterCount);
- for (let i = 0; i < parameterCount; i++) {
- message.dataTypeIDs[i] = reader.int32();
- }
- return message;
- };
- var parseDataRowMessage = (reader) => {
- const fieldCount = reader.int16();
- const fields = new Array(fieldCount);
- for (let i = 0; i < fieldCount; i++) {
- const len = reader.int32();
- fields[i] = len === -1 ? null : reader.string(len);
- }
- return new messages_1.DataRowMessage(LATEINIT_LENGTH, fields);
- };
- var parseParameterStatusMessage = (reader) => {
- const name = reader.cstring();
- const value = reader.cstring();
- return new messages_1.ParameterStatusMessage(LATEINIT_LENGTH, name, value);
- };
- var parseBackendKeyData = (reader) => {
- const processID = reader.int32();
- const secretKey = reader.int32();
- return new messages_1.BackendKeyDataMessage(LATEINIT_LENGTH, processID, secretKey);
- };
- var parseAuthenticationResponse = (reader, length) => {
- const code = reader.int32();
- const message = {
- name: "authenticationOk",
- length
- };
- switch (code) {
- case 0:
- break;
- case 3:
- if (message.length === 8) {
- message.name = "authenticationCleartextPassword";
- }
- break;
- case 5:
- if (message.length === 12) {
- message.name = "authenticationMD5Password";
- const salt = reader.bytes(4);
- return new messages_1.AuthenticationMD5Password(LATEINIT_LENGTH, salt);
- }
- break;
- case 10:
- {
- message.name = "authenticationSASL";
- message.mechanisms = [];
- let mechanism;
- do {
- mechanism = reader.cstring();
- if (mechanism) {
- message.mechanisms.push(mechanism);
- }
- } while (mechanism);
- }
- break;
- case 11:
- message.name = "authenticationSASLContinue";
- message.data = reader.string(length - 8);
- break;
- case 12:
- message.name = "authenticationSASLFinal";
- message.data = reader.string(length - 8);
- break;
- default:
- throw new Error("Unknown authenticationOk message type " + code);
- }
- return message;
- };
- var parseErrorMessage = (reader, name) => {
- const fields = {};
- let fieldType = reader.string(1);
- while (fieldType !== "\0") {
- fields[fieldType] = reader.cstring();
- fieldType = reader.string(1);
- }
- const messageValue = fields.M;
- const message = name === "notice" ? new messages_1.NoticeMessage(LATEINIT_LENGTH, messageValue) : new messages_1.DatabaseError(messageValue, LATEINIT_LENGTH, name);
- message.severity = fields.S;
- message.code = fields.C;
- message.detail = fields.D;
- message.hint = fields.H;
- message.position = fields.P;
- message.internalPosition = fields.p;
- message.internalQuery = fields.q;
- message.where = fields.W;
- message.schema = fields.s;
- message.table = fields.t;
- message.column = fields.c;
- message.dataType = fields.d;
- message.constraint = fields.n;
- message.file = fields.F;
- message.line = fields.L;
- message.routine = fields.R;
- return message;
- };
- }
-});
-
-// node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/index.js
-var require_dist = __commonJS({
- "node_modules/.pnpm/pg-protocol@1.15.0/node_modules/pg-protocol/dist/index.js"(exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.DatabaseError = exports.serialize = void 0;
- exports.parse = parse;
- var messages_1 = require_messages();
- Object.defineProperty(exports, "DatabaseError", { enumerable: true, get: function() {
- return messages_1.DatabaseError;
- } });
- var serializer_1 = require_serializer();
- Object.defineProperty(exports, "serialize", { enumerable: true, get: function() {
- return serializer_1.serialize;
- } });
- var parser_1 = require_parser();
- function parse(stream, callback) {
- const parser = new parser_1.Parser();
- stream.on("data", (buffer) => parser.parse(buffer, callback));
- return new Promise((resolve) => stream.on("end", () => resolve()));
- }
- }
-});
-
-// node_modules/.pnpm/pg-cloudflare@1.4.0/node_modules/pg-cloudflare/dist/empty.js
-var require_empty = __commonJS({
- "node_modules/.pnpm/pg-cloudflare@1.4.0/node_modules/pg-cloudflare/dist/empty.js"(exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.default = {};
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/stream.js
-var require_stream = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/stream.js"(exports, module) {
- var { getStream, getSecureStream } = getStreamFuncs();
- module.exports = {
- /**
- * Get a socket stream compatible with the current runtime environment.
- * @returns {Duplex}
- */
- getStream,
- /**
- * Get a TLS secured socket, compatible with the current environment,
- * using the socket and other settings given in `options`.
- * @returns {Duplex}
- */
- getSecureStream
- };
- function getNodejsStreamFuncs() {
- function getStream2(ssl) {
- const net2 = __require("net");
- return new net2.Socket();
- }
- function getSecureStream2(options) {
- const tls = __require("tls");
- return tls.connect(options);
- }
- return {
- getStream: getStream2,
- getSecureStream: getSecureStream2
- };
- }
- function getCloudflareStreamFuncs() {
- function getStream2(ssl) {
- const { CloudflareSocket } = require_empty();
- return new CloudflareSocket(ssl);
- }
- function getSecureStream2(options) {
- options.socket.startTls(options);
- return options.socket;
- }
- return {
- getStream: getStream2,
- getSecureStream: getSecureStream2
- };
- }
- function isCloudflareRuntime() {
- if (typeof navigator === "object" && navigator !== null && typeof navigator.userAgent === "string") {
- return navigator.userAgent === "Cloudflare-Workers";
- }
- if (typeof Response === "function") {
- const resp = new Response(null, { cf: { thing: true } });
- if (typeof resp.cf === "object" && resp.cf !== null && resp.cf.thing) {
- return true;
- }
- }
- return false;
- }
- function getStreamFuncs() {
- if (isCloudflareRuntime()) {
- return getCloudflareStreamFuncs();
- }
- return getNodejsStreamFuncs();
- }
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/connection.js
-var require_connection = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/connection.js"(exports, module) {
- "use strict";
- 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 EventEmitter2 {
- constructor(config) {
- super();
- config = config || {};
- this.stream = config.stream || getStream(config.ssl);
- if (typeof this.stream === "function") {
- this.stream = this.stream(config);
- }
- this._keepAlive = config.keepAlive;
- this._keepAliveInitialDelayMillis = config.keepAliveInitialDelayMillis;
- this.parsedStatements = {};
- this.ssl = config.ssl || false;
- this.sslNegotiation = config.sslNegotiation || "postgres";
- this._ending = false;
- this._emitMessage = false;
- const self = this;
- this.on("newListener", function(eventName) {
- if (eventName === "message") {
- self._emitMessage = true;
- }
- });
- }
- connect(port, host) {
- const self = this;
- this._connecting = true;
- this.stream.setNoDelay(true);
- this.stream.connect(port, host);
- this.stream.once("connect", function() {
- if (self._keepAlive) {
- self.stream.setKeepAlive(true, self._keepAliveInitialDelayMillis);
- }
- self.emit("connect");
- });
- const reportStreamError = function(error) {
- if (self._ending && (error.code === "ECONNRESET" || error.code === "EPIPE")) {
- return;
- }
- self.emit("error", error);
- };
- this.stream.on("error", reportStreamError);
- this.stream.on("close", function() {
- self.emit("end");
- });
- if (!this.ssl) {
- return this.attachListeners(this.stream);
- }
- if (this.sslNegotiation === "direct") {
- return this.stream.once("connect", function() {
- self.upgradeToSSL(host, reportStreamError);
- });
- }
- this.stream.once("data", function(buffer) {
- const responseCode = buffer.toString("utf8");
- switch (responseCode) {
- case "S":
- break;
- case "N":
- self.stream.end();
- return self.emit("error", new Error("The server does not support SSL connections"));
- default:
- self.stream.end();
- return self.emit("error", new Error("There was an error establishing an SSL connection"));
- }
- self.upgradeToSSL(host, reportStreamError);
- });
- }
- upgradeToSSL(host, reportStreamError) {
- const self = this;
- const options = {
- socket: self.stream
- };
- if (self.ssl !== true) {
- Object.assign(options, self.ssl);
- if ("key" in self.ssl) {
- options.key = self.ssl.key;
- }
- }
- if (self.sslNegotiation === "direct") {
- options.ALPNProtocols = ["postgresql"];
- }
- const net2 = __require("net");
- if (net2.isIP && net2.isIP(host) === 0) {
- options.servername = host;
- }
- try {
- self.stream = stream.getSecureStream(options);
- } catch (err) {
- return self.emit("error", err);
- }
- self.attachListeners(self.stream);
- self.stream.on("error", reportStreamError);
- self.emit("sslconnect");
- }
- attachListeners(stream2) {
- parse(stream2, (msg) => {
- const eventName = msg.name === "error" ? "errorMessage" : msg.name;
- if (this._emitMessage) {
- this.emit("message", msg);
- }
- this.emit(eventName, msg);
- });
- }
- requestSsl() {
- this.stream.write(serialize.requestSsl());
- }
- startup(config) {
- this.stream.write(serialize.startup(config));
- }
- cancel(processID, secretKey) {
- this._send(serialize.cancel(processID, secretKey));
- }
- password(password) {
- this._send(serialize.password(password));
- }
- sendSASLInitialResponseMessage(mechanism, initialResponse) {
- this._send(serialize.sendSASLInitialResponseMessage(mechanism, initialResponse));
- }
- sendSCRAMClientFinalMessage(additionalData) {
- this._send(serialize.sendSCRAMClientFinalMessage(additionalData));
- }
- _send(buffer) {
- if (!this.stream.writable) {
- return false;
- }
- return this.stream.write(buffer);
- }
- query(text) {
- this._send(serialize.query(text));
- }
- // send parse message
- parse(query) {
- this._send(serialize.parse(query));
- }
- // send bind message
- bind(config) {
- this._send(serialize.bind(config));
- }
- // send execute message
- execute(config) {
- this._send(serialize.execute(config));
- }
- flush() {
- if (this.stream.writable) {
- this.stream.write(flushBuffer);
- }
- }
- sync() {
- this._ending = true;
- this._send(syncBuffer);
- }
- ref() {
- this.stream.ref();
- }
- unref() {
- this.stream.unref();
- }
- end() {
- this._ending = true;
- if (!this._connecting || !this.stream.writable) {
- this.stream.end();
- return;
- }
- return this.stream.write(endBuffer, () => {
- this.stream.end();
- });
- }
- close(msg) {
- this._send(serialize.close(msg));
- }
- describe(msg) {
- this._send(serialize.describe(msg));
- }
- sendCopyFromChunk(chunk) {
- this._send(serialize.copyData(chunk));
- }
- endCopyFrom() {
- this._send(serialize.copyDone());
- }
- sendCopyFail(msg) {
- this._send(serialize.copyFail(msg));
- }
- };
- module.exports = Connection2;
- }
-});
-
-// node_modules/.pnpm/split2@4.2.0/node_modules/split2/index.js
-var require_split2 = __commonJS({
- "node_modules/.pnpm/split2@4.2.0/node_modules/split2/index.js"(exports, module) {
- "use strict";
- var { Transform: Transform3 } = __require("stream");
- var { StringDecoder } = __require("string_decoder");
- var kLast = Symbol("last");
- var kDecoder = Symbol("decoder");
- function transform(chunk, enc, cb) {
- let list;
- if (this.overflow) {
- const buf = this[kDecoder].write(chunk);
- list = buf.split(this.matcher);
- if (list.length === 1) return cb();
- list.shift();
- this.overflow = false;
- } else {
- this[kLast] += this[kDecoder].write(chunk);
- list = this[kLast].split(this.matcher);
- }
- this[kLast] = list.pop();
- for (let i = 0; i < list.length; i++) {
- try {
- push(this, this.mapper(list[i]));
- } catch (error) {
- return cb(error);
- }
- }
- this.overflow = this[kLast].length > this.maxLength;
- if (this.overflow && !this.skipOverflow) {
- cb(new Error("maximum buffer reached"));
- return;
- }
- cb();
- }
- function flush(cb) {
- this[kLast] += this[kDecoder].end();
- if (this[kLast]) {
- try {
- push(this, this.mapper(this[kLast]));
- } catch (error) {
- return cb(error);
- }
- }
- cb();
- }
- function push(self, val) {
- if (val !== void 0) {
- self.push(val);
- }
- }
- function noop(incoming) {
- return incoming;
- }
- function split(matcher, mapper, options) {
- matcher = matcher || /\r?\n/;
- mapper = mapper || noop;
- options = options || {};
- switch (arguments.length) {
- case 1:
- if (typeof matcher === "function") {
- mapper = matcher;
- matcher = /\r?\n/;
- } else if (typeof matcher === "object" && !(matcher instanceof RegExp) && !matcher[Symbol.split]) {
- options = matcher;
- matcher = /\r?\n/;
- }
- break;
- case 2:
- if (typeof matcher === "function") {
- options = mapper;
- mapper = matcher;
- matcher = /\r?\n/;
- } else if (typeof mapper === "object") {
- options = mapper;
- mapper = noop;
- }
- }
- options = Object.assign({}, options);
- options.autoDestroy = true;
- options.transform = transform;
- options.flush = flush;
- options.readableObjectMode = true;
- const stream = new Transform3(options);
- stream[kLast] = "";
- stream[kDecoder] = new StringDecoder("utf8");
- stream.matcher = matcher;
- stream.mapper = mapper;
- stream.maxLength = options.maxLength;
- stream.skipOverflow = options.skipOverflow || false;
- stream.overflow = false;
- stream._destroy = function(err, cb) {
- this._writableState.errorEmitted = false;
- cb(err);
- };
- return stream;
- }
- module.exports = split;
- }
-});
-
-// node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/helper.js
-var require_helper = __commonJS({
- "node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/helper.js"(exports, module) {
- "use strict";
- var path35 = __require("path");
- var Stream = __require("stream").Stream;
- var split = require_split2();
- var util = __require("util");
- var defaultPort = 5432;
- var isWin = process.platform === "win32";
- var warnStream = process.stderr;
- var S_IRWXG = 56;
- var S_IRWXO = 7;
- var S_IFMT = 61440;
- var S_IFREG = 32768;
- function isRegFile(mode) {
- return (mode & S_IFMT) == S_IFREG;
- }
- var fieldNames = ["host", "port", "database", "user", "password"];
- var nrOfFields = fieldNames.length;
- var passKey = fieldNames[nrOfFields - 1];
- function warn() {
- var isWritable = warnStream instanceof Stream && true === warnStream.writable;
- if (isWritable) {
- var args = Array.prototype.slice.call(arguments).concat("\n");
- warnStream.write(util.format.apply(util, args));
- }
- }
- Object.defineProperty(module.exports, "isWin", {
- get: function() {
- return isWin;
- },
- set: function(val) {
- isWin = val;
- }
- });
- module.exports.warnTo = function(stream) {
- var old = warnStream;
- warnStream = stream;
- return old;
- };
- module.exports.getFileName = function(rawEnv) {
- var env = rawEnv || process.env;
- 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) {
- if (Object.prototype.hasOwnProperty.call(process.env, "PGPASSWORD")) {
- return false;
- }
- if (isWin) {
- return true;
- }
- fname = fname || "";
- if (!isRegFile(stats.mode)) {
- warn('WARNING: password file "%s" is not a plain file', fname);
- return false;
- }
- if (stats.mode & (S_IRWXG | S_IRWXO)) {
- warn('WARNING: password file "%s" has group or world access; permissions should be u=rw (0600) or less', fname);
- return false;
- }
- return true;
- };
- var matcher = module.exports.match = function(connInfo, entry) {
- return fieldNames.slice(0, -1).reduce(function(prev, field, idx) {
- if (idx == 1) {
- if (Number(connInfo[field] || defaultPort) === Number(entry[field])) {
- return prev && true;
- }
- }
- return prev && (entry[field] === "*" || entry[field] === connInfo[field]);
- }, true);
- };
- module.exports.getPassword = function(connInfo, stream, cb) {
- var pass;
- var lineStream = stream.pipe(split());
- function onLine(line) {
- var entry = parseLine(line);
- if (entry && isValidEntry(entry) && matcher(connInfo, entry)) {
- pass = entry[passKey];
- lineStream.end();
- }
- }
- var onEnd = function() {
- stream.destroy();
- cb(pass);
- };
- var onErr = function(err) {
- stream.destroy();
- warn("WARNING: error on reading file: %s", err);
- cb(void 0);
- };
- stream.on("error", onErr);
- lineStream.on("data", onLine).on("end", onEnd).on("error", onErr);
- };
- var parseLine = module.exports.parseLine = function(line) {
- if (line.length < 11 || line.match(/^\s+#/)) {
- return null;
- }
- var curChar = "";
- var prevChar = "";
- var fieldIdx = 0;
- var startIdx = 0;
- var endIdx = 0;
- var obj = {};
- var isLastField = false;
- var addToObj = function(idx, i0, i1) {
- var field = line.substring(i0, i1);
- if (!Object.hasOwnProperty.call(process.env, "PGPASS_NO_DEESCAPE")) {
- field = field.replace(/\\([:\\])/g, "$1");
- }
- obj[fieldNames[idx]] = field;
- };
- for (var i = 0; i < line.length - 1; i += 1) {
- curChar = line.charAt(i + 1);
- prevChar = line.charAt(i);
- isLastField = fieldIdx == nrOfFields - 1;
- if (isLastField) {
- addToObj(fieldIdx, startIdx);
- break;
- }
- if (i >= 0 && curChar == ":" && prevChar !== "\\") {
- addToObj(fieldIdx, startIdx, i + 1);
- startIdx = i + 2;
- fieldIdx += 1;
- }
- }
- obj = Object.keys(obj).length === nrOfFields ? obj : null;
- return obj;
- };
- var isValidEntry = module.exports.isValidEntry = function(entry) {
- var rules = {
- // host
- 0: function(x) {
- return x.length > 0;
- },
- // port
- 1: function(x) {
- if (x === "*") {
- return true;
- }
- x = Number(x);
- return isFinite(x) && x > 0 && x < 9007199254740992 && Math.floor(x) === x;
- },
- // database
- 2: function(x) {
- return x.length > 0;
- },
- // username
- 3: function(x) {
- return x.length > 0;
- },
- // password
- 4: function(x) {
- return x.length > 0;
- }
- };
- for (var idx = 0; idx < fieldNames.length; idx += 1) {
- var rule = rules[idx];
- var value = entry[fieldNames[idx]] || "";
- var res = rule(value);
- if (!res) {
- return false;
- }
- }
- return true;
- };
- }
-});
-
-// node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/index.js
-var require_lib = __commonJS({
- "node_modules/.pnpm/pgpass@1.0.5/node_modules/pgpass/lib/index.js"(exports, module) {
- "use strict";
- var path35 = __require("path");
- var fs33 = __require("fs");
- var helper = require_helper();
- module.exports = function(connInfo, cb) {
- var file = helper.getFileName();
- fs33.stat(file, function(err, stat) {
- if (err || !helper.usePgPass(stat, file)) {
- return cb(void 0);
- }
- var st = fs33.createReadStream(file);
- helper.getPassword(connInfo, st, cb);
- });
- };
- module.exports.warnTo = helper.warnTo;
- }
-});
-
-// 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 EventEmitter2 = __require("events").EventEmitter;
- var utils = require_utils();
- var nodeUtils = __require("util");
- var sasl = require_sasl();
- var TypeOverrides2 = require_type_overrides();
- var ConnectionParameters = require_connection_parameters();
- var Query2 = require_query();
- var defaults2 = require_defaults();
- var Connection2 = require_connection();
- var crypto37 = require_utils2();
- var activeQueryDeprecationNotice = nodeUtils.deprecate(
- () => {
- },
- "Client.activeQuery is deprecated and will be removed in pg@9.0"
- );
- var queryQueueDeprecationNotice = nodeUtils.deprecate(
- () => {
- },
- "Client.queryQueue is deprecated and will be removed in pg@9.0."
- );
- var pgPassDeprecationNotice = nodeUtils.deprecate(
- () => {
- },
- "pgpass support is deprecated and will be removed in pg@9.0. You can provide an async function as the password property to the Client/Pool constructor that returns a password instead. Within this function you can call the pgpass module in your own code."
- );
- var byoPromiseDeprecationNotice = nodeUtils.deprecate(
- () => {
- },
- "Passing a custom Promise implementation to the Client/Pool constructor is deprecated and will be removed in pg@9.0."
- );
- var queryQueueLengthDeprecationNotice = nodeUtils.deprecate(
- () => {
- },
- "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."
- );
- function coerceNumberOrDefault(value, defaultValue) {
- if (typeof value === "number") {
- return Number.isFinite(value) ? value : defaultValue;
- }
- if (typeof value === "string" && value.trim() !== "") {
- const n = Number(value);
- return Number.isFinite(n) ? n : defaultValue;
- }
- return defaultValue;
- }
- var Client2 = class extends EventEmitter2 {
- constructor(config) {
- super();
- this.connectionParameters = new ConnectionParameters(config);
- this.user = this.connectionParameters.user;
- this.database = this.connectionParameters.database;
- this.port = this.connectionParameters.port;
- this.host = this.connectionParameters.host;
- Object.defineProperty(this, "password", {
- configurable: true,
- enumerable: false,
- writable: true,
- value: this.connectionParameters.password
- });
- this.replication = this.connectionParameters.replication;
- const c = config || {};
- if (c.Promise) {
- byoPromiseDeprecationNotice();
- }
- this._Promise = c.Promise || global.Promise;
- this._types = new TypeOverrides2(c.types);
- this._ending = false;
- this._ended = false;
- this._connecting = false;
- this._connected = false;
- this._connectionError = false;
- this._queryable = true;
- this._activeQuery = null;
- this._txStatus = null;
- this.enableChannelBinding = Boolean(c.enableChannelBinding);
- this.scramMaxIterations = coerceNumberOrDefault(c.scramMaxIterations, sasl.DEFAULT_MAX_SCRAM_ITERATIONS);
- this.connection = c.connection || new Connection2({
- stream: c.stream,
- ssl: this.connectionParameters.ssl,
- sslNegotiation: this.connectionParameters.sslnegotiation,
- keepAlive: c.keepAlive || false,
- keepAliveInitialDelayMillis: c.keepAliveInitialDelayMillis || 0,
- encoding: this.connectionParameters.client_encoding || "utf8"
- });
- this._queryQueue = [];
- this.binary = c.binary || defaults2.binary;
- this.processID = null;
- this.secretKey = null;
- this.ssl = this.connectionParameters.ssl || false;
- this.sslNegotiation = this.connectionParameters.sslnegotiation || "postgres";
- if (this.ssl && this.ssl.key) {
- Object.defineProperty(this.ssl, "key", {
- enumerable: false
- });
- }
- this._connectionTimeoutMillis = c.connectionTimeoutMillis || 0;
- }
- get activeQuery() {
- activeQueryDeprecationNotice();
- return this._activeQuery;
- }
- set activeQuery(val) {
- activeQueryDeprecationNotice();
- this._activeQuery = val;
- }
- _getActiveQuery() {
- return this._activeQuery;
- }
- _errorAllQueries(err) {
- const enqueueError = (query) => {
- process.nextTick(() => {
- query.handleError(err, this.connection);
- });
- };
- const activeQuery = this._getActiveQuery();
- if (activeQuery) {
- enqueueError(activeQuery);
- this._activeQuery = null;
- }
- this._queryQueue.forEach(enqueueError);
- this._queryQueue.length = 0;
- }
- _connect(callback) {
- const self = this;
- const con = this.connection;
- this._connectionCallback = callback;
- if (this._connecting || this._connected) {
- const err = new Error("Client has already been connected. You cannot reuse a client.");
- process.nextTick(() => {
- callback(err);
- });
- return;
- }
- this._connecting = true;
- if (this._connectionTimeoutMillis > 0) {
- this.connectionTimeoutHandle = setTimeout(() => {
- con._ending = true;
- con.stream.destroy(new Error("timeout expired"));
- }, this._connectionTimeoutMillis);
- if (this.connectionTimeoutHandle.unref) {
- this.connectionTimeoutHandle.unref();
- }
- }
- if (this.host && this.host.indexOf("/") === 0) {
- con.connect(this.host + "/.s.PGSQL." + this.port);
- } else {
- con.connect(this.port, this.host);
- }
- con.on("connect", function() {
- if (self.ssl) {
- if (self.sslNegotiation !== "direct") {
- con.requestSsl();
- }
- } else {
- con.startup(self.getStartupConf());
- }
- });
- con.on("sslconnect", function() {
- con.startup(self.getStartupConf());
- });
- this._attachListeners(con);
- con.once("end", () => {
- const error = this._ending ? new Error("Connection terminated") : new Error("Connection terminated unexpectedly");
- clearTimeout(this.connectionTimeoutHandle);
- this._errorAllQueries(error);
- this._ended = true;
- if (!this._ending) {
- if (this._connecting && !this._connectionError) {
- if (this._connectionCallback) {
- this._connectionCallback(error);
- } else {
- this._handleErrorEvent(error);
- }
- } else if (!this._connectionError) {
- this._handleErrorEvent(error);
- }
- }
- process.nextTick(() => {
- this.emit("end");
- });
- });
- }
- connect(callback) {
- if (callback) {
- this._connect(callback);
- return;
- }
- return new this._Promise((resolve, reject) => {
- this._connect((error) => {
- if (error) {
- reject(error);
- } else {
- resolve(this);
- }
- });
- });
- }
- _attachListeners(con) {
- con.on("authenticationCleartextPassword", this._handleAuthCleartextPassword.bind(this));
- con.on("authenticationMD5Password", this._handleAuthMD5Password.bind(this));
- con.on("authenticationSASL", this._handleAuthSASL.bind(this));
- con.on("authenticationSASLContinue", this._handleAuthSASLContinue.bind(this));
- con.on("authenticationSASLFinal", this._handleAuthSASLFinal.bind(this));
- con.on("backendKeyData", this._handleBackendKeyData.bind(this));
- con.on("error", this._handleErrorEvent.bind(this));
- con.on("errorMessage", this._handleErrorMessage.bind(this));
- con.on("readyForQuery", this._handleReadyForQuery.bind(this));
- con.on("notice", this._handleNotice.bind(this));
- con.on("rowDescription", this._handleRowDescription.bind(this));
- con.on("dataRow", this._handleDataRow.bind(this));
- con.on("portalSuspended", this._handlePortalSuspended.bind(this));
- con.on("emptyQuery", this._handleEmptyQuery.bind(this));
- con.on("commandComplete", this._handleCommandComplete.bind(this));
- con.on("parseComplete", this._handleParseComplete.bind(this));
- con.on("copyInResponse", this._handleCopyInResponse.bind(this));
- con.on("copyData", this._handleCopyData.bind(this));
- con.on("notification", this._handleNotification.bind(this));
- }
- _getPassword(cb) {
- const con = this.connection;
- if (typeof this.password === "function") {
- this._Promise.resolve().then(() => this.password(this.connectionParameters)).then((pass) => {
- if (pass !== void 0) {
- if (typeof pass !== "string") {
- con.emit("error", new TypeError("Password must be a string"));
- return;
- }
- this.connectionParameters.password = this.password = pass;
- } else {
- this.connectionParameters.password = this.password = null;
- }
- cb();
- }).catch((err) => {
- con.emit("error", err);
- });
- } else if (this.password !== null) {
- cb();
- } else {
- try {
- const pgPass = require_lib();
- pgPass(this.connectionParameters, (pass) => {
- if (void 0 !== pass) {
- pgPassDeprecationNotice();
- this.connectionParameters.password = this.password = pass;
- }
- cb();
- });
- } catch (e) {
- this.emit("error", e);
- }
- }
- }
- _handleAuthCleartextPassword(msg) {
- this._getPassword(() => {
- this.connection.password(this.password);
- });
- }
- _handleAuthMD5Password(msg) {
- this._getPassword(async () => {
- try {
- const hashedPassword = await crypto37.postgresMd5PasswordHash(this.user, this.password, msg.salt);
- this.connection.password(hashedPassword);
- } catch (e) {
- this.emit("error", e);
- }
- });
- }
- _handleAuthSASL(msg) {
- this._getPassword(() => {
- try {
- this.saslSession = sasl.startSession(
- msg.mechanisms,
- this.enableChannelBinding && this.connection.stream,
- this.scramMaxIterations
- );
- this.connection.sendSASLInitialResponseMessage(this.saslSession.mechanism, this.saslSession.response);
- } catch (err) {
- this.connection.emit("error", err);
- }
- });
- }
- async _handleAuthSASLContinue(msg) {
- try {
- await sasl.continueSession(
- this.saslSession,
- this.password,
- msg.data,
- this.enableChannelBinding && this.connection.stream
- );
- this.connection.sendSCRAMClientFinalMessage(this.saslSession.response);
- } catch (err) {
- this.connection.emit("error", err);
- }
- }
- _handleAuthSASLFinal(msg) {
- try {
- sasl.finalizeSession(this.saslSession, msg.data);
- this.saslSession = null;
- } catch (err) {
- this.connection.emit("error", err);
- }
- }
- _handleBackendKeyData(msg) {
- this.processID = msg.processID;
- this.secretKey = msg.secretKey;
- }
- _handleReadyForQuery(msg) {
- if (this._connecting) {
- this._connecting = false;
- this._connected = true;
- clearTimeout(this.connectionTimeoutHandle);
- if (this._connectionCallback) {
- this._connectionCallback(null, this);
- this._connectionCallback = null;
- }
- this.emit("connect");
- }
- const activeQuery = this._getActiveQuery();
- this._activeQuery = null;
- this._txStatus = msg?.status ?? null;
- this.readyForQuery = true;
- if (activeQuery) {
- activeQuery.handleReadyForQuery(this.connection);
- }
- this._pulseQueryQueue();
- }
- // if we receive an error event or error message
- // during the connection process we handle it here
- _handleErrorWhileConnecting(err) {
- if (this._connectionError) {
- return;
- }
- this._connectionError = true;
- clearTimeout(this.connectionTimeoutHandle);
- if (this._connectionCallback) {
- return this._connectionCallback(err);
- }
- this.emit("error", err);
- }
- // if we're connected and we receive an error event from the connection
- // this means the socket is dead - do a hard abort of all queries and emit
- // the socket error on the client as well
- _handleErrorEvent(err) {
- if (this._connecting) {
- return this._handleErrorWhileConnecting(err);
- }
- this._queryable = false;
- this._errorAllQueries(err);
- this.emit("error", err);
- }
- // handle error messages from the postgres backend
- _handleErrorMessage(msg) {
- if (this._connecting) {
- return this._handleErrorWhileConnecting(msg);
- }
- const activeQuery = this._getActiveQuery();
- if (!activeQuery) {
- this._handleErrorEvent(msg);
- return;
- }
- this._activeQuery = null;
- activeQuery.handleError(msg, this.connection);
- }
- _handleRowDescription(msg) {
- const activeQuery = this._getActiveQuery();
- if (activeQuery == null) {
- const error = new Error("Received unexpected rowDescription message from backend.");
- this._handleErrorEvent(error);
- return;
- }
- activeQuery.handleRowDescription(msg);
- }
- _handleDataRow(msg) {
- const activeQuery = this._getActiveQuery();
- if (activeQuery == null) {
- const error = new Error("Received unexpected dataRow message from backend.");
- this._handleErrorEvent(error);
- return;
- }
- activeQuery.handleDataRow(msg);
- }
- _handlePortalSuspended(msg) {
- const activeQuery = this._getActiveQuery();
- if (activeQuery == null) {
- const error = new Error("Received unexpected portalSuspended message from backend.");
- this._handleErrorEvent(error);
- return;
- }
- activeQuery.handlePortalSuspended(this.connection);
- }
- _handleEmptyQuery(msg) {
- const activeQuery = this._getActiveQuery();
- if (activeQuery == null) {
- const error = new Error("Received unexpected emptyQuery message from backend.");
- this._handleErrorEvent(error);
- return;
- }
- activeQuery.handleEmptyQuery(this.connection);
- }
- _handleCommandComplete(msg) {
- const activeQuery = this._getActiveQuery();
- if (activeQuery == null) {
- const error = new Error("Received unexpected commandComplete message from backend.");
- this._handleErrorEvent(error);
- return;
- }
- activeQuery.handleCommandComplete(msg, this.connection);
- }
- _handleParseComplete() {
- const activeQuery = this._getActiveQuery();
- if (activeQuery == null) {
- const error = new Error("Received unexpected parseComplete message from backend.");
- this._handleErrorEvent(error);
- return;
- }
- if (activeQuery.name) {
- this.connection.parsedStatements[activeQuery.name] = activeQuery.text;
- }
- }
- _handleCopyInResponse(msg) {
- const activeQuery = this._getActiveQuery();
- if (activeQuery == null) {
- const error = new Error("Received unexpected copyInResponse message from backend.");
- this._handleErrorEvent(error);
- return;
- }
- activeQuery.handleCopyInResponse(this.connection);
- }
- _handleCopyData(msg) {
- const activeQuery = this._getActiveQuery();
- if (activeQuery == null) {
- const error = new Error("Received unexpected copyData message from backend.");
- this._handleErrorEvent(error);
- return;
- }
- activeQuery.handleCopyData(msg, this.connection);
- }
- _handleNotification(msg) {
- this.emit("notification", msg);
- }
- _handleNotice(msg) {
- this.emit("notice", msg);
- }
- getStartupConf() {
- const params = this.connectionParameters;
- const data = {
- user: params.user,
- database: params.database
- };
- const appName = params.application_name || params.fallback_application_name;
- if (appName) {
- data.application_name = appName;
- }
- if (params.replication) {
- data.replication = "" + params.replication;
- }
- if (params.statement_timeout) {
- data.statement_timeout = String(parseInt(params.statement_timeout, 10));
- }
- if (params.lock_timeout) {
- data.lock_timeout = String(parseInt(params.lock_timeout, 10));
- }
- if (params.idle_in_transaction_session_timeout) {
- data.idle_in_transaction_session_timeout = String(parseInt(params.idle_in_transaction_session_timeout, 10));
- }
- if (params.options) {
- data.options = params.options;
- }
- return data;
- }
- cancel(client, query) {
- if (client.activeQuery === query) {
- const con = this.connection;
- if (this.host && this.host.indexOf("/") === 0) {
- con.connect(this.host + "/.s.PGSQL." + this.port);
- } else {
- con.connect(this.port, this.host);
- }
- con.on("connect", function() {
- con.cancel(client.processID, client.secretKey);
- });
- } else if (client._queryQueue.indexOf(query) !== -1) {
- client._queryQueue.splice(client._queryQueue.indexOf(query), 1);
- }
- }
- setTypeParser(oid, format, parseFn) {
- return this._types.setTypeParser(oid, format, parseFn);
- }
- getTypeParser(oid, format) {
- return this._types.getTypeParser(oid, format);
- }
- // escapeIdentifier and escapeLiteral moved to utility functions & exported
- // on PG
- // re-exported here for backwards compatibility
- escapeIdentifier(str) {
- return utils.escapeIdentifier(str);
- }
- escapeLiteral(str) {
- return utils.escapeLiteral(str);
- }
- _pulseQueryQueue() {
- if (this.readyForQuery === true) {
- this._activeQuery = this._queryQueue.shift();
- const activeQuery = this._getActiveQuery();
- if (activeQuery) {
- this.readyForQuery = false;
- this.hasExecuted = true;
- const queryError = activeQuery.submit(this.connection);
- if (queryError) {
- process.nextTick(() => {
- activeQuery.handleError(queryError, this.connection);
- this.readyForQuery = true;
- this._pulseQueryQueue();
- });
- }
- } else if (this.hasExecuted) {
- this._activeQuery = null;
- this.emit("drain");
- }
- }
- }
- query(config, values, callback) {
- let query;
- let result;
- if (config == null) {
- throw new TypeError("Client was passed a null or undefined query");
- }
- if (typeof config.submit === "function") {
- result = query = config;
- if (!query.callback) {
- if (typeof values === "function") {
- query.callback = values;
- } else if (callback) {
- query.callback = callback;
- }
- }
- } else {
- query = new Query2(config, values, callback);
- if (!query.callback) {
- result = new this._Promise((resolve, reject) => {
- query.callback = (err, res) => err ? reject(err) : resolve(res);
- }).catch((err) => {
- Error.captureStackTrace(err);
- throw err;
- });
- } else if (typeof query.callback !== "function") {
- throw new TypeError("callback is not a function");
- }
- }
- const readTimeout = config.query_timeout || this.connectionParameters.query_timeout;
- if (readTimeout) {
- const queryCallback = query.callback || (() => {
- });
- const readTimeoutTimer = setTimeout(() => {
- const error = new Error("Query read timeout");
- process.nextTick(() => {
- query.handleError(error, this.connection);
- });
- queryCallback(error);
- query.callback = () => {
- };
- const index = this._queryQueue.indexOf(query);
- if (index > -1) {
- this._queryQueue.splice(index, 1);
- }
- this._pulseQueryQueue();
- }, readTimeout);
- query.callback = (err, res) => {
- clearTimeout(readTimeoutTimer);
- queryCallback(err, res);
- };
- }
- if (this.binary && !query.binary) {
- query.binary = true;
- }
- if (query._result && !query._result._types) {
- query._result._types = this._types;
- }
- if (!this._queryable) {
- process.nextTick(() => {
- query.handleError(new Error("Client has encountered a connection error and is not queryable"), this.connection);
- });
- return result;
- }
- if (this._ending) {
- process.nextTick(() => {
- query.handleError(new Error("Client was closed and is not queryable"), this.connection);
- });
- return result;
- }
- if (this._queryQueue.length > 0) {
- queryQueueLengthDeprecationNotice();
- }
- this._queryQueue.push(query);
- this._pulseQueryQueue();
- return result;
- }
- ref() {
- this.connection.ref();
- }
- unref() {
- this.connection.unref();
- }
- getTransactionStatus() {
- return this._txStatus;
- }
- end(cb) {
- this._ending = true;
- if (!this.connection._connecting || this._ended) {
- if (cb) {
- cb();
- return;
- } else {
- return this._Promise.resolve();
- }
- }
- if (this._getActiveQuery() || !this._queryable) {
- this.connection.stream.destroy();
- } else {
- this.connection.end();
- }
- if (cb) {
- this.connection.once("end", cb);
- } else {
- return new this._Promise((resolve) => {
- this.connection.once("end", resolve);
- });
- }
- }
- get queryQueue() {
- queryQueueDeprecationNotice();
- return this._queryQueue;
- }
- };
- Client2.Query = Query2;
- module.exports = Client2;
- }
-});
-
-// node_modules/.pnpm/pg-pool@3.14.0_pg@8.22.0/node_modules/pg-pool/index.js
-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 EventEmitter2 = __require("events").EventEmitter;
- var NOOP = function() {
- };
- var removeWhere = (list, predicate) => {
- const i = list.findIndex(predicate);
- return i === -1 ? void 0 : list.splice(i, 1)[0];
- };
- var IdleItem = class {
- constructor(client, idleListener, timeoutId) {
- this.client = client;
- this.idleListener = idleListener;
- this.timeoutId = timeoutId;
- }
- };
- var PendingItem = class {
- constructor(callback) {
- this.callback = callback;
- }
- };
- function throwOnDoubleRelease() {
- throw new Error("Release called on client which has already been released to the pool.");
- }
- function promisify(Promise2, callback) {
- if (callback) {
- return { callback, result: void 0 };
- }
- let rej;
- let res;
- const cb = function(err, client) {
- err ? rej(err) : res(client);
- };
- const result = new Promise2(function(resolve, reject) {
- res = resolve;
- rej = reject;
- }).catch((err) => {
- Error.captureStackTrace(err);
- throw err;
- });
- return { callback: cb, result };
- }
- function makeIdleListener(pool2, client) {
- return function idleListener(err) {
- err.client = client;
- client.removeListener("error", idleListener);
- client.on("error", () => {
- pool2.log("additional client error after disconnection due to error", err);
- });
- pool2._remove(client);
- pool2.emit("error", err, client);
- };
- }
- var Pool2 = class extends EventEmitter2 {
- constructor(options, Client2) {
- super();
- this.options = Object.assign({}, options);
- if (options != null && "password" in options) {
- Object.defineProperty(this.options, "password", {
- configurable: true,
- enumerable: false,
- writable: true,
- value: options.password
- });
- }
- if (options != null && options.ssl && options.ssl.key) {
- Object.defineProperty(this.options.ssl, "key", {
- enumerable: false
- });
- }
- this.options.max = this.options.max || this.options.poolSize || 10;
- this.options.min = this.options.min || 0;
- this.options.maxUses = this.options.maxUses || Infinity;
- this.options.allowExitOnIdle = this.options.allowExitOnIdle || false;
- this.options.maxLifetimeSeconds = this.options.maxLifetimeSeconds || 0;
- this.log = this.options.log || function() {
- };
- this.Client = this.options.Client || Client2 || require_lib2().Client;
- this.Promise = this.options.Promise || global.Promise;
- if (typeof this.options.idleTimeoutMillis === "undefined") {
- this.options.idleTimeoutMillis = 1e4;
- }
- this._clients = [];
- this._idle = [];
- this._expired = /* @__PURE__ */ new WeakSet();
- this._pendingQueue = [];
- this._endCallback = void 0;
- this.ending = false;
- this.ended = false;
- }
- _promiseTry(f) {
- const Promise2 = this.Promise;
- if (typeof Promise2.try === "function") {
- return Promise2.try(f);
- }
- return new Promise2((resolve) => resolve(f()));
- }
- _isFull() {
- return this._clients.length >= this.options.max;
- }
- _isAboveMin() {
- return this._clients.length > this.options.min;
- }
- _pulseQueue() {
- this.log("pulse queue");
- if (this.ended) {
- this.log("pulse queue ended");
- return;
- }
- if (this.ending) {
- this.log("pulse queue on ending");
- if (this._idle.length) {
- this._idle.slice().map((item) => {
- this._remove(item.client);
- });
- }
- if (!this._clients.length) {
- this.ended = true;
- this._endCallback();
- }
- return;
- }
- if (!this._pendingQueue.length) {
- this.log("no queued requests");
- return;
- }
- if (!this._idle.length && this._isFull()) {
- return;
- }
- const pendingItem = this._pendingQueue.shift();
- if (this._idle.length) {
- const idleItem = this._idle.pop();
- clearTimeout(idleItem.timeoutId);
- const client = idleItem.client;
- client.ref && client.ref();
- const idleListener = idleItem.idleListener;
- return this._acquireClient(client, pendingItem, idleListener, false);
- }
- if (!this._isFull()) {
- return this.newClient(pendingItem);
- }
- throw new Error("unexpected condition");
- }
- _remove(client, callback) {
- const removed = removeWhere(this._idle, (item) => item.client === client);
- if (removed !== void 0) {
- clearTimeout(removed.timeoutId);
- }
- this._clients = this._clients.filter((c) => c !== client);
- const context = this;
- client.end(() => {
- context.emit("remove", client);
- if (typeof callback === "function") {
- callback();
- }
- });
- }
- connect(cb) {
- if (this.ending) {
- const err = new Error("Cannot use a pool after calling end on the pool");
- return cb ? cb(err) : this.Promise.reject(err);
- }
- const response = promisify(this.Promise, cb);
- const result = response.result;
- if (this._isFull() || this._idle.length) {
- if (this._idle.length) {
- process.nextTick(() => this._pulseQueue());
- }
- if (!this.options.connectionTimeoutMillis) {
- this._pendingQueue.push(new PendingItem(response.callback));
- return result;
- }
- const queueCallback = (err, res, done) => {
- clearTimeout(tid);
- response.callback(err, res, done);
- };
- const pendingItem = new PendingItem(queueCallback);
- const tid = setTimeout(() => {
- removeWhere(this._pendingQueue, (i) => i.callback === queueCallback);
- pendingItem.timedOut = true;
- response.callback(new Error("timeout exceeded when trying to connect"));
- }, this.options.connectionTimeoutMillis);
- if (tid.unref) {
- tid.unref();
- }
- this._pendingQueue.push(pendingItem);
- return result;
- }
- this.newClient(new PendingItem(response.callback));
- return result;
- }
- newClient(pendingItem) {
- const client = new this.Client(this.options);
- this._clients.push(client);
- const idleListener = makeIdleListener(this, client);
- this.log("checking client timeout");
- let tid;
- let timeoutHit = false;
- if (this.options.connectionTimeoutMillis) {
- tid = setTimeout(() => {
- if (client.connection) {
- this.log("ending client due to timeout");
- timeoutHit = true;
- client.connection.stream.destroy();
- } else if (!client.isConnected()) {
- this.log("ending client due to timeout");
- timeoutHit = true;
- client.end();
- }
- }, this.options.connectionTimeoutMillis);
- }
- this.log("connecting new client");
- client.connect((err) => {
- if (tid) {
- clearTimeout(tid);
- }
- client.on("error", idleListener);
- if (err) {
- this.log("client failed to connect", err);
- this._clients = this._clients.filter((c) => c !== client);
- if (timeoutHit) {
- err = new Error("Connection terminated due to connection timeout", { cause: err });
- }
- this._pulseQueue();
- if (!pendingItem.timedOut) {
- pendingItem.callback(err, void 0, NOOP);
- }
- } else {
- this.log("new client connected");
- if (this.options.onConnect) {
- this._promiseTry(() => this.options.onConnect(client)).then(
- () => {
- this._afterConnect(client, pendingItem, idleListener);
- },
- (hookErr) => {
- this._clients = this._clients.filter((c) => c !== client);
- client.end(() => {
- this._pulseQueue();
- if (!pendingItem.timedOut) {
- pendingItem.callback(hookErr, void 0, NOOP);
- }
- });
- }
- );
- return;
- }
- return this._afterConnect(client, pendingItem, idleListener);
- }
- });
- }
- _afterConnect(client, pendingItem, idleListener) {
- if (this.options.maxLifetimeSeconds !== 0) {
- const maxLifetimeTimeout = setTimeout(() => {
- this.log("ending client due to expired lifetime");
- this._expired.add(client);
- const idleIndex = this._idle.findIndex((idleItem) => idleItem.client === client);
- if (idleIndex !== -1) {
- this._acquireClient(
- client,
- new PendingItem((err, client2, clientRelease) => clientRelease()),
- idleListener,
- false
- );
- }
- }, this.options.maxLifetimeSeconds * 1e3);
- maxLifetimeTimeout.unref();
- client.once("end", () => clearTimeout(maxLifetimeTimeout));
- }
- return this._acquireClient(client, pendingItem, idleListener, true);
- }
- // acquire a client for a pending work item
- _acquireClient(client, pendingItem, idleListener, isNew) {
- if (isNew) {
- this.emit("connect", client);
- }
- this.emit("acquire", client);
- client.release = this._releaseOnce(client, idleListener);
- client.removeListener("error", idleListener);
- if (!pendingItem.timedOut) {
- if (isNew && this.options.verify) {
- this.options.verify(client, (err) => {
- if (err) {
- client.release(err);
- return pendingItem.callback(err, void 0, NOOP);
- }
- pendingItem.callback(void 0, client, client.release);
- });
- } else {
- pendingItem.callback(void 0, client, client.release);
- }
- } else {
- if (isNew && this.options.verify) {
- this.options.verify(client, client.release);
- } else {
- client.release();
- }
- }
- }
- // returns a function that wraps _release and throws if called more than once
- _releaseOnce(client, idleListener) {
- let released = false;
- return (err) => {
- if (released) {
- throwOnDoubleRelease();
- }
- released = true;
- this._release(client, idleListener, err);
- };
- }
- // release a client back to the poll, include an error
- // to remove it from the pool
- _release(client, idleListener, err) {
- client.on("error", idleListener);
- client._poolUseCount = (client._poolUseCount || 0) + 1;
- this.emit("release", err, client);
- if (err || this.ending || !client._queryable || client._ending || client._poolUseCount >= this.options.maxUses) {
- if (client._poolUseCount >= this.options.maxUses) {
- this.log("remove expended client");
- }
- return this._remove(client, this._pulseQueue.bind(this));
- }
- const isExpired = this._expired.has(client);
- if (isExpired) {
- this.log("remove expired client");
- this._expired.delete(client);
- return this._remove(client, this._pulseQueue.bind(this));
- }
- let tid;
- if (this.options.idleTimeoutMillis && this._isAboveMin()) {
- tid = setTimeout(() => {
- if (this._isAboveMin()) {
- this.log("remove idle client");
- this._remove(client, this._pulseQueue.bind(this));
- }
- }, this.options.idleTimeoutMillis);
- if (this.options.allowExitOnIdle) {
- tid.unref();
- }
- }
- if (this.options.allowExitOnIdle) {
- client.unref();
- }
- this._idle.push(new IdleItem(client, idleListener, tid));
- this._pulseQueue();
- }
- query(text, values, cb) {
- if (typeof text === "function") {
- const response2 = promisify(this.Promise, text);
- setImmediate(function() {
- return response2.callback(new Error("Passing a function as the first parameter to pool.query is not supported"));
- });
- return response2.result;
- }
- if (typeof values === "function") {
- cb = values;
- values = void 0;
- }
- const response = promisify(this.Promise, cb);
- cb = response.callback;
- this.connect((err, client) => {
- if (err) {
- return cb(err);
- }
- let clientReleased = false;
- const onError = (err2) => {
- if (clientReleased) {
- return;
- }
- clientReleased = true;
- client.release(err2);
- cb(err2);
- };
- client.once("error", onError);
- this.log("dispatching query");
- try {
- client.query(text, values, (err2, res) => {
- this.log("query dispatched");
- client.removeListener("error", onError);
- if (clientReleased) {
- return;
- }
- clientReleased = true;
- client.release(err2);
- if (err2) {
- return cb(err2);
- }
- return cb(void 0, res);
- });
- } catch (err2) {
- client.release(err2);
- return cb(err2);
- }
- });
- return response.result;
- }
- end(cb) {
- this.log("ending");
- if (this.ending) {
- const err = new Error("Called end on pool more than once");
- return cb ? cb(err) : this.Promise.reject(err);
- }
- this.ending = true;
- const promised = promisify(this.Promise, cb);
- this._endCallback = promised.callback;
- this._pulseQueue();
- return promised.result;
- }
- get waitingCount() {
- return this._pendingQueue.length;
- }
- get idleCount() {
- return this._idle.length;
- }
- get expiredCount() {
- return this._clients.reduce((acc, client) => acc + (this._expired.has(client) ? 1 : 0), 0);
- }
- get totalCount() {
- return this._clients.length;
- }
- };
- module.exports = Pool2;
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/native/query.js
-var require_query2 = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/native/query.js"(exports, module) {
- "use strict";
- var EventEmitter2 = __require("events").EventEmitter;
- var util = __require("util");
- var utils = require_utils();
- var NativeQuery = module.exports = function(config, values, callback) {
- EventEmitter2.call(this);
- config = utils.normalizeQueryConfig(config, values, callback);
- this.text = config.text;
- this.values = config.values;
- this.name = config.name;
- this.queryMode = config.queryMode;
- this.callback = config.callback;
- this.state = "new";
- this._arrayMode = config.rowMode === "array";
- this._emitRowEvents = false;
- this.on(
- "newListener",
- function(event) {
- if (event === "row") this._emitRowEvents = true;
- }.bind(this)
- );
- };
- util.inherits(NativeQuery, EventEmitter2);
- var errorFieldMap = {
- sqlState: "code",
- statementPosition: "position",
- messagePrimary: "message",
- context: "where",
- schemaName: "schema",
- tableName: "table",
- columnName: "column",
- dataTypeName: "dataType",
- constraintName: "constraint",
- sourceFile: "file",
- sourceLine: "line",
- sourceFunction: "routine"
- };
- NativeQuery.prototype.handleError = function(err) {
- const fields = this.native.pq.resultErrorFields();
- if (fields) {
- for (const key in fields) {
- const normalizedFieldName = errorFieldMap[key] || key;
- err[normalizedFieldName] = fields[key];
- }
- }
- if (this.callback) {
- this.callback(err);
- } else {
- this.emit("error", err);
- }
- this.state = "error";
- };
- NativeQuery.prototype.then = function(onSuccess, onFailure) {
- return this._getPromise().then(onSuccess, onFailure);
- };
- NativeQuery.prototype.catch = function(callback) {
- return this._getPromise().catch(callback);
- };
- NativeQuery.prototype._getPromise = function() {
- if (this._promise) return this._promise;
- this._promise = new Promise(
- function(resolve, reject) {
- this._once("end", resolve);
- this._once("error", reject);
- }.bind(this)
- );
- return this._promise;
- };
- NativeQuery.prototype.submit = function(client) {
- this.state = "running";
- const self = this;
- this.native = client.native;
- client.native.arrayMode = this._arrayMode;
- let after = function(err, rows, results) {
- client.native.arrayMode = false;
- setImmediate(function() {
- self.emit("_done");
- });
- if (err) {
- return self.handleError(err);
- }
- if (self._emitRowEvents) {
- if (results.length > 1) {
- rows.forEach((rowOfRows, i) => {
- rowOfRows.forEach((row) => {
- self.emit("row", row, results[i]);
- });
- });
- } else {
- rows.forEach(function(row) {
- self.emit("row", row, results);
- });
- }
- }
- self.state = "end";
- self.emit("end", results);
- if (self.callback) {
- self.callback(null, results);
- }
- };
- if (process.domain) {
- after = process.domain.bind(after);
- }
- if (this.name) {
- if (this.name.length > 63) {
- console.error("Warning! Postgres only supports 63 characters for query names.");
- console.error("You supplied %s (%s)", this.name, this.name.length);
- console.error("This can cause conflicts and silent errors executing queries");
- }
- const values = (this.values || []).map(utils.prepareValue);
- if (client.namedQueries[this.name]) {
- if (this.text && client.namedQueries[this.name] !== this.text) {
- const err = new Error(`Prepared statements must be unique - '${this.name}' was used for a different statement`);
- return after(err);
- }
- return client.native.execute(this.name, values, after);
- }
- return client.native.prepare(this.name, this.text, values.length, function(err) {
- if (err) return after(err);
- client.namedQueries[self.name] = self.text;
- return self.native.execute(self.name, values, after);
- });
- } else if (this.values) {
- if (!Array.isArray(this.values)) {
- const err = new Error("Query values must be an array");
- return after(err);
- }
- const vals = this.values.map(utils.prepareValue);
- client.native.query(this.text, vals, after);
- } else if (this.queryMode === "extended") {
- client.native.query(this.text, [], after);
- } else {
- client.native.query(this.text, after);
- }
- };
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/native/client.js
-var require_client2 = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/native/client.js"(exports, module) {
- var nodeUtils = __require("util");
- var Native;
- try {
- Native = __require("pg-native");
- } catch (e) {
- throw e;
- }
- var TypeOverrides2 = require_type_overrides();
- var EventEmitter2 = __require("events").EventEmitter;
- var util = __require("util");
- var ConnectionParameters = require_connection_parameters();
- var NativeQuery = require_query2();
- var queryQueueLengthDeprecationNotice = nodeUtils.deprecate(
- () => {
- },
- "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) {
- EventEmitter2.call(this);
- config = config || {};
- this._Promise = config.Promise || global.Promise;
- this._types = new TypeOverrides2(config.types);
- this.native = new Native({
- types: this._types
- });
- this._queryQueue = [];
- this._ending = false;
- this._connecting = false;
- this._connected = false;
- this._queryable = true;
- const cp = this.connectionParameters = new ConnectionParameters(config);
- if (config.nativeConnectionString) cp.nativeConnectionString = config.nativeConnectionString;
- this.user = cp.user;
- Object.defineProperty(this, "password", {
- configurable: true,
- enumerable: false,
- writable: true,
- value: cp.password
- });
- this.database = cp.database;
- this.host = cp.host;
- this.port = cp.port;
- this.namedQueries = {};
- };
- Client2.Query = NativeQuery;
- util.inherits(Client2, EventEmitter2);
- Client2.prototype._errorAllQueries = function(err) {
- const enqueueError = (query) => {
- process.nextTick(() => {
- query.native = this.native;
- query.handleError(err);
- });
- };
- if (this._hasActiveQuery()) {
- enqueueError(this._activeQuery);
- this._activeQuery = null;
- }
- this._queryQueue.forEach(enqueueError);
- this._queryQueue.length = 0;
- };
- Client2.prototype._connect = function(cb) {
- const self = this;
- if (this._connecting) {
- process.nextTick(() => cb(new Error("Client has already been connected. You cannot reuse a client.")));
- return;
- }
- this._connecting = true;
- this.connectionParameters.getLibpqConnectionString(function(err, conString) {
- if (self.connectionParameters.nativeConnectionString) conString = self.connectionParameters.nativeConnectionString;
- if (err) return cb(err);
- self.native.connect(conString, function(err2) {
- if (err2) {
- self.native.end();
- return cb(err2);
- }
- self._connected = true;
- self.native.on("error", function(err3) {
- self._queryable = false;
- self._errorAllQueries(err3);
- self.emit("error", err3);
- });
- self.native.on("notification", function(msg) {
- self.emit("notification", {
- channel: msg.relname,
- payload: msg.extra
- });
- });
- self.emit("connect");
- self._pulseQueryQueue(true);
- cb(null, this);
- });
- });
- };
- Client2.prototype.connect = function(callback) {
- if (callback) {
- this._connect(callback);
- return;
- }
- return new this._Promise((resolve, reject) => {
- this._connect((error) => {
- if (error) {
- reject(error);
- } else {
- resolve(this);
- }
- });
- });
- };
- Client2.prototype.query = function(config, values, callback) {
- let query;
- let result;
- let readTimeout;
- let readTimeoutTimer;
- let queryCallback;
- if (config === null || config === void 0) {
- throw new TypeError("Client was passed a null or undefined query");
- } else if (typeof config.submit === "function") {
- readTimeout = config.query_timeout || this.connectionParameters.query_timeout;
- result = query = config;
- if (typeof values === "function") {
- config.callback = values;
- }
- } else {
- readTimeout = config.query_timeout || this.connectionParameters.query_timeout;
- query = new NativeQuery(config, values, callback);
- if (!query.callback) {
- let resolveOut, rejectOut;
- result = new this._Promise((resolve, reject) => {
- resolveOut = resolve;
- rejectOut = reject;
- }).catch((err) => {
- Error.captureStackTrace(err);
- throw err;
- });
- query.callback = (err, res) => err ? rejectOut(err) : resolveOut(res);
- }
- }
- if (readTimeout) {
- queryCallback = query.callback || (() => {
- });
- readTimeoutTimer = setTimeout(() => {
- const error = new Error("Query read timeout");
- process.nextTick(() => {
- query.handleError(error, this.connection);
- });
- queryCallback(error);
- query.callback = () => {
- };
- const index = this._queryQueue.indexOf(query);
- if (index > -1) {
- this._queryQueue.splice(index, 1);
- }
- this._pulseQueryQueue();
- }, readTimeout);
- query.callback = (err, res) => {
- clearTimeout(readTimeoutTimer);
- queryCallback(err, res);
- };
- }
- if (!this._queryable) {
- query.native = this.native;
- process.nextTick(() => {
- query.handleError(new Error("Client has encountered a connection error and is not queryable"));
- });
- return result;
- }
- if (this._ending) {
- query.native = this.native;
- process.nextTick(() => {
- query.handleError(new Error("Client was closed and is not queryable"));
- });
- return result;
- }
- if (this._queryQueue.length > 0) {
- queryQueueLengthDeprecationNotice();
- }
- this._queryQueue.push(query);
- this._pulseQueryQueue();
- return result;
- };
- Client2.prototype.end = function(cb) {
- const self = this;
- this._ending = true;
- if (this._connecting && !this._connected) {
- this.once("connect", () => {
- this.end(() => {
- });
- });
- }
- let result;
- if (!cb) {
- result = new this._Promise(function(resolve, reject) {
- cb = (err) => err ? reject(err) : resolve();
- });
- }
- this.native.end(function() {
- self._connected = false;
- self._errorAllQueries(new Error("Connection terminated"));
- process.nextTick(() => {
- self.emit("end");
- if (cb) cb();
- });
- });
- return result;
- };
- Client2.prototype._hasActiveQuery = function() {
- return this._activeQuery && this._activeQuery.state !== "error" && this._activeQuery.state !== "end";
- };
- Client2.prototype._pulseQueryQueue = function(initialConnection) {
- if (!this._connected) {
- return;
- }
- if (this._hasActiveQuery()) {
- return;
- }
- const query = this._queryQueue.shift();
- if (!query) {
- if (!initialConnection) {
- this.emit("drain");
- }
- return;
- }
- this._activeQuery = query;
- query.submit(this);
- const self = this;
- query.once("_done", function() {
- self._pulseQueryQueue();
- });
- };
- Client2.prototype.cancel = function(query) {
- if (this._activeQuery === query) {
- this.native.cancel(function() {
- });
- } else if (this._queryQueue.indexOf(query) !== -1) {
- this._queryQueue.splice(this._queryQueue.indexOf(query), 1);
- }
- };
- Client2.prototype.ref = function() {
- };
- Client2.prototype.unref = function() {
- };
- Client2.prototype.setTypeParser = function(oid, format, parseFn) {
- return this._types.setTypeParser(oid, format, parseFn);
- };
- Client2.prototype.getTypeParser = function(oid, format) {
- return this._types.getTypeParser(oid, format);
- };
- Client2.prototype.isConnected = function() {
- return this._connected;
- };
- Client2.prototype.getTransactionStatus = function() {
- return this.native.getTransactionStatus();
- };
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/native/index.js
-var require_native = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/native/index.js"(exports, module) {
- "use strict";
- module.exports = require_client2();
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/index.js
-var require_lib2 = __commonJS({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/lib/index.js"(exports, module) {
- "use strict";
- var Client2 = require_client();
- var defaults2 = require_defaults();
- var Connection2 = require_connection();
- var Result2 = require_result();
- var utils = require_utils();
- var Pool2 = require_pg_pool();
- var TypeOverrides2 = require_type_overrides();
- var { DatabaseError: DatabaseError2 } = require_dist();
- var { escapeIdentifier: escapeIdentifier2, escapeLiteral: escapeLiteral2 } = require_utils();
- var poolFactory = (Client3) => {
- return class BoundPool extends Pool2 {
- constructor(options) {
- super(options, Client3);
- }
- };
- };
- var PG = function(clientConstructor2) {
- this.defaults = defaults2;
- this.Client = clientConstructor2;
- this.Query = this.Client.Query;
- this.Pool = poolFactory(this.Client);
- this._pools = [];
- this.Connection = Connection2;
- this.types = require_pg_types();
- this.DatabaseError = DatabaseError2;
- this.TypeOverrides = TypeOverrides2;
- this.escapeIdentifier = escapeIdentifier2;
- this.escapeLiteral = escapeLiteral2;
- this.Result = Result2;
- this.utils = utils;
- };
- var clientConstructor = Client2;
- var forceNative = false;
- try {
- forceNative = !!process.env.NODE_PG_FORCE_NATIVE;
- } catch {
- }
- if (forceNative) {
- clientConstructor = require_native();
- }
- module.exports = new PG(clientConstructor);
- Object.defineProperty(module.exports, "native", {
- configurable: true,
- enumerable: false,
- get() {
- let native = null;
- try {
- native = new PG(require_native());
- } catch (err) {
- if (err.code !== "MODULE_NOT_FOUND") {
- throw err;
- }
- }
- Object.defineProperty(module.exports, "native", {
- value: native
- });
- return native;
- }
- });
- }
-});
-
-// node_modules/.pnpm/pg@8.22.0/node_modules/pg/esm/index.mjs
-var esm_exports = {};
-__export(esm_exports, {
- Client: () => Client,
- Connection: () => Connection,
- DatabaseError: () => DatabaseError,
- Pool: () => Pool,
- Query: () => Query,
- Result: () => Result,
- TypeOverrides: () => TypeOverrides,
- default: () => esm_default,
- defaults: () => defaults,
- escapeIdentifier: () => escapeIdentifier,
- escapeLiteral: () => escapeLiteral,
- types: () => types
-});
-var import_lib, Client, Pool, Connection, types, Query, DatabaseError, escapeIdentifier, escapeLiteral, Result, TypeOverrides, defaults, esm_default;
-var init_esm = __esm({
- "node_modules/.pnpm/pg@8.22.0/node_modules/pg/esm/index.mjs"() {
- import_lib = __toESM(require_lib2(), 1);
- Client = import_lib.default.Client;
- Pool = import_lib.default.Pool;
- Connection = import_lib.default.Connection;
- types = import_lib.default.types;
- Query = import_lib.default.Query;
- DatabaseError = import_lib.default.DatabaseError;
- escapeIdentifier = import_lib.default.escapeIdentifier;
- escapeLiteral = import_lib.default.escapeLiteral;
- Result = import_lib.default.Result;
- TypeOverrides = import_lib.default.TypeOverrides;
- defaults = import_lib.default.defaults;
- esm_default = import_lib.default;
- }
-});
-
-// experience-service-pg.mjs
-var experience_service_pg_exports = {};
-__export(experience_service_pg_exports, {
- createPgExperienceService: () => createPgExperienceService
-});
-import crypto35 from "node:crypto";
-async function createPgExperienceService(options = {}) {
- const connectionString = options.connectionString;
- if (!connectionString) {
- throw new Error("createPgExperienceService \u9700\u8981 connectionString\uFF08\u5EFA\u8BAE\u7528 EXPERIENCE_PG_URL\uFF09");
- }
- const now = options.now ?? (() => Date.now());
- const embed = options.embed ?? null;
- const embeddingDim = Math.max(1, Number(options.embeddingDim ?? 1536));
- const halfLifeMs = Math.max(
- 6e4,
- Number(options.recencyHalfLifeMs ?? 30 * 24 * 60 * 60 * 1e3)
- );
- const { default: pg2 } = await Promise.resolve().then(() => (init_esm(), esm_exports));
- const pool2 = new pg2.Pool({ connectionString, max: Number(options.poolMax ?? 10) });
- await pool2.query("CREATE EXTENSION IF NOT EXISTS vector");
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_experience (
- id UUID PRIMARY KEY,
- scope TEXT NOT NULL DEFAULT 'global',
- kind TEXT NOT NULL DEFAULT 'lesson',
- title TEXT NOT NULL,
- body TEXT NOT NULL,
- tags JSONB NOT NULL DEFAULT '[]'::jsonb,
- source_session_id TEXT,
- source_user_id TEXT,
- use_count BIGINT NOT NULL DEFAULT 0,
- embedding vector(${embeddingDim}),
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL
- )
- `);
- await pool2.query(
- "CREATE INDEX IF NOT EXISTS idx_h5_experience_scope_updated ON h5_experience (scope, updated_at DESC)"
- );
- await pool2.query(
- "CREATE INDEX IF NOT EXISTS idx_h5_experience_embedding ON h5_experience USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)"
- ).catch(() => {
- });
- function normalizeTags2(tags) {
- if (!Array.isArray(tags)) return [];
- return [...new Set(tags.map((t) => String(t).trim()).filter(Boolean))];
- }
- function toVectorLiteral(vec) {
- return `[${vec.map((n) => Number(n)).join(",")}]`;
- }
- function rowToExperience(row) {
- return {
- id: row.id,
- scope: row.scope,
- kind: row.kind,
- title: row.title,
- body: row.body,
- tags: Array.isArray(row.tags) ? row.tags : [],
- sourceSessionId: row.source_session_id ?? null,
- sourceUserId: row.source_user_id ?? null,
- useCount: Number(row.use_count ?? 0),
- createdAt: Number(row.created_at),
- updatedAt: Number(row.updated_at)
- };
- }
- async function record(input) {
- const title = String(input?.title ?? "").trim();
- 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 = crypto35.randomUUID();
- const ts = now();
- const scope = String(input?.scope ?? "global").trim() || "global";
- const kind = String(input?.kind ?? "lesson").trim() || "lesson";
- const tags = normalizeTags2(input?.tags);
- let embedding = null;
- if (embed) {
- try {
- const vec = await embed(`${title}
-${body}`);
- if (Array.isArray(vec) && vec.length === embeddingDim) embedding = toVectorLiteral(vec);
- } catch {
- embedding = null;
- }
- }
- await pool2.query(
- `INSERT INTO h5_experience
- (id, scope, kind, title, body, tags, source_session_id, source_user_id,
- use_count, embedding, created_at, updated_at)
- VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,0,$9::vector,$10,$11)`,
- [
- id,
- scope,
- kind,
- title,
- body,
- JSON.stringify(tags),
- input?.sourceSessionId ?? null,
- input?.sourceUserId ?? null,
- embedding,
- ts,
- ts
- ]
- );
- return rowToExperience({
- id,
- scope,
- kind,
- title,
- body,
- tags,
- source_session_id: input?.sourceSessionId ?? null,
- source_user_id: input?.sourceUserId ?? null,
- use_count: 0,
- created_at: ts,
- updated_at: ts
- });
- }
- async function search(query, { scope = "global", limit = 5 } = {}) {
- const text = String(query ?? "").trim();
- if (!text) return [];
- const max = Math.max(1, limit);
- if (embed) {
- try {
- const vec = await embed(text);
- if (Array.isArray(vec) && vec.length === embeddingDim) {
- const { rows: rows2 } = await pool2.query(
- `SELECT id, scope, kind, title, body, tags, source_session_id,
- source_user_id, use_count, created_at, updated_at
- FROM h5_experience
- WHERE scope = $1 AND embedding IS NOT NULL
- ORDER BY embedding <=> $2::vector, updated_at DESC
- LIMIT $3`,
- [scope, toVectorLiteral(vec), max]
- );
- if (rows2.length > 0) {
- await bumpUseCount(rows2.map((r) => r.id));
- return rows2.map(rowToExperience);
- }
- }
- } catch {
- }
- }
- const terms = tokenize(text);
- if (terms.length === 0) return [];
- const likeClauses = terms.map((_, i) => `(title ILIKE $${i + 2} OR body ILIKE $${i + 2})`);
- const params = [scope, ...terms.map((t) => `%${t}%`), max];
- const { rows } = await pool2.query(
- `SELECT id, scope, kind, title, body, tags, source_session_id,
- source_user_id, use_count, created_at, updated_at
- FROM h5_experience
- WHERE scope = $1 AND (${likeClauses.join(" OR ")})
- ORDER BY updated_at DESC
- LIMIT $${terms.length + 2}`,
- params
- );
- if (rows.length > 0) await bumpUseCount(rows.map((r) => r.id));
- return rows.map(rowToExperience);
- }
- async function bumpUseCount(ids) {
- if (!ids.length) return;
- await pool2.query("UPDATE h5_experience SET use_count = use_count + 1 WHERE id = ANY($1::uuid[])", [ids]).catch(() => {
- });
- }
- function tokenize(query) {
- return [
- ...new Set(
- String(query ?? "").toLowerCase().split(/[^\p{L}\p{N}]+/u).map((t) => t.trim()).filter((t) => t.length >= 2)
- )
- ];
- }
- async function reflect() {
- return { ok: false, reason: "not_implemented" };
- }
- async function close() {
- await pool2.end();
- }
- return { record, search, reflect, close };
-}
-function experienceError2(message, code) {
- return Object.assign(new Error(message), { code });
-}
-var init_experience_service_pg = __esm({
- "experience-service-pg.mjs"() {
- "use strict";
- }
-});
-
-// server.mjs
-import express2 from "express";
-import crypto36 from "node:crypto";
-import fs32 from "node:fs";
-import fsPromises4 from "node:fs/promises";
-import { createProxyMiddleware } from "http-proxy-middleware";
-import path34 from "node:path";
-import { fileURLToPath as fileURLToPath6 } from "node:url";
-
-// auth.mjs
-import crypto from "node:crypto";
-var AUTH_COOKIE = "tkmind_h5_session";
-function parseCookies(header = "") {
- return Object.fromEntries(
- header.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
- const separator = part.indexOf("=");
- if (separator < 0) return [part, ""];
- return [
- decodeURIComponent(part.slice(0, separator)),
- decodeURIComponent(part.slice(separator + 1))
- ];
- })
- );
-}
-function safeEqual(left, right) {
- const leftBuffer = Buffer.from(left);
- const rightBuffer = Buffer.from(right);
- return leftBuffer.length === rightBuffer.length && crypto.timingSafeEqual(leftBuffer, rightBuffer);
-}
-function createAuthManager({
- password,
- ttlMs = 7 * 24 * 60 * 60 * 1e3,
- maxFailures = 5,
- failureWindowMs = 5 * 60 * 1e3
-}) {
- if (!password) {
- throw new Error("H5_ACCESS_PASSWORD is required");
- }
- const sessions = /* @__PURE__ */ new Map();
- const failures = /* @__PURE__ */ new Map();
- const prune = (now = Date.now()) => {
- for (const [token, expiresAt] of sessions) {
- if (expiresAt <= now) sessions.delete(token);
- }
- for (const [ip, state] of failures) {
- if (state.resetAt <= now) failures.delete(ip);
- }
- };
- const login = (candidate, ip = "unknown", now = Date.now()) => {
- prune(now);
- const failure = failures.get(ip);
- if (failure && failure.count >= maxFailures && failure.resetAt > now) {
- return { ok: false, retryAfterMs: failure.resetAt - now };
- }
- if (!safeEqual(candidate, password)) {
- const current = failure && failure.resetAt > now ? failure : { count: 0, resetAt: now + failureWindowMs };
- current.count += 1;
- failures.set(ip, current);
- return { ok: false, retryAfterMs: 0 };
- }
- failures.delete(ip);
- const token = crypto.randomBytes(32).toString("base64url");
- sessions.set(token, now + ttlMs);
- return { ok: true, token };
- };
- const verify = (token, now = Date.now()) => {
- if (!token) return false;
- prune(now);
- const expiresAt = sessions.get(token);
- if (!expiresAt || expiresAt <= now) {
- sessions.delete(token);
- return false;
- }
- sessions.set(token, now + ttlMs);
- return true;
- };
- const revoke = (token) => {
- if (token) sessions.delete(token);
- };
- return { login, verify, revoke };
-}
-function sessionCookie(token, secure) {
- const parts = [
- `${AUTH_COOKIE}=${encodeURIComponent(token)}`,
- "Path=/",
- "HttpOnly",
- "SameSite=Lax",
- "Max-Age=604800"
- ];
- if (secure) parts.push("Secure");
- return parts.join("; ");
-}
-function clearSessionCookie(secure) {
- const parts = [
- `${AUTH_COOKIE}=`,
- "Path=/",
- "HttpOnly",
- "SameSite=Lax",
- "Max-Age=0"
- ];
- if (secure) parts.push("Secure");
- return parts.join("; ");
-}
-
-// db.mjs
-import fs2 from "node:fs";
-import mysql from "mysql2/promise";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-
-// mindspace.mjs
-import crypto2 from "node:crypto";
-
-// mindspace-config.mjs
-var PUBLIC_PAGE_LIMIT_KEY = "public_page_limit";
-function asPositiveInteger(value, fallback) {
- const parsed = Number(value);
- if (!Number.isFinite(parsed) || parsed < 1) return fallback;
- return Math.floor(parsed);
-}
-async function ensureConfigTable(pool2) {
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS mindspace_config (
- \`key\` VARCHAR(64) PRIMARY KEY,
- value TEXT NOT NULL,
- description VARCHAR(255) NULL,
- updated_at BIGINT NOT NULL
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
-}
-function defaultMindSpaceConfig(env = process.env) {
- return {
- publicPageLimit: asPositiveInteger(env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5, 5)
- };
-}
-async function readConfigRows(pool2) {
- const [rows] = await pool2.query(
- "SELECT `key`, value FROM mindspace_config"
- );
- return rows;
-}
-async function ensureMindSpaceConfig(pool2, { env = process.env, seedDefault = true } = {}) {
- await ensureConfigTable(pool2);
- if (!seedDefault) return;
- const [rows] = await pool2.query("SELECT COUNT(*) AS count FROM mindspace_config");
- if (Number(rows[0]?.count ?? 0) > 0) return;
- const now = Date.now();
- const defaults2 = defaultMindSpaceConfig(env);
- await pool2.query(
- `INSERT INTO mindspace_config (\`key\`, value, description, updated_at)
- VALUES (?, ?, ?, ?)`,
- [PUBLIC_PAGE_LIMIT_KEY, String(defaults2.publicPageLimit), "\u516C\u5F00\u9875\u9762\u6570\u91CF\u4E0A\u9650", now]
- );
-}
-async function loadMindSpaceConfig(pool2, { env = process.env } = {}) {
- const config = defaultMindSpaceConfig(env);
- try {
- const rows = await readConfigRows(pool2);
- for (const row of rows) {
- if (row.key === PUBLIC_PAGE_LIMIT_KEY) {
- config.publicPageLimit = asPositiveInteger(row.value, config.publicPageLimit);
- }
- }
- } catch (error) {
- if (error?.code === "ER_NO_SUCH_TABLE") return config;
- throw error;
- }
- return config;
-}
-
-// mindspace.mjs
-var DEFAULT_SPACE_QUOTA_BYTES = 5 * 1024 * 1024;
-var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
-var SYSTEM_CATEGORIES = Object.freeze([
- {
- code: "oa",
- name: "OA \u5DE5\u4F5C\u533A",
- visibilityPolicy: "private",
- aiAccessPolicy: "selected_assets",
- publishPolicy: "derivative_only",
- sortOrder: 10
- },
- {
- code: "public",
- name: "\u516C\u5F00\u533A",
- visibilityPolicy: "public_candidate",
- aiAccessPolicy: "selected_assets",
- publishPolicy: "security_scan_required",
- sortOrder: 20
- },
- {
- code: "draft",
- name: "\u9875\u9762\u8349\u7A3F",
- visibilityPolicy: "private",
- aiAccessPolicy: "job_output",
- publishPolicy: "security_scan_required",
- sortOrder: 40
- },
- {
- code: "archive",
- name: "\u5F52\u6863\u533A",
- visibilityPolicy: "private",
- aiAccessPolicy: "none",
- publishPolicy: "forbidden",
- sortOrder: 50
- }
-]);
-function asNumber(value) {
- return Number(value ?? 0);
-}
-async function withOptionalTimeout(promise, timeoutMs, fallback) {
- let timeout;
- try {
- return await Promise.race([
- promise,
- new Promise((resolve) => {
- timeout = setTimeout(() => resolve(fallback), timeoutMs);
- })
- ]);
- } finally {
- if (timeout) clearTimeout(timeout);
- }
-}
-function categoryResponse(row) {
- return {
- id: row.id,
- code: row.category_code,
- name: row.category_name,
- visibilityPolicy: row.visibility_policy,
- aiAccessPolicy: row.ai_access_policy,
- publishPolicy: row.publish_policy,
- isSystem: Boolean(row.is_system),
- sortOrder: asNumber(row.sort_order),
- itemCount: asNumber(row.item_count)
- };
-}
-async function initializeDefaultSpace(db, userId, {
- quotaBytes = DEFAULT_SPACE_QUOTA_BYTES,
- spaceName = "\u6211\u7684\u7A7A\u95F4",
- now = Date.now(),
- idFactory = () => crypto2.randomUUID()
-} = {}) {
- const spaceId = idFactory();
- await db.query(
- `INSERT INTO h5_user_spaces
- (id, user_id, space_name, quota_bytes, used_bytes, reserved_bytes, status, created_at, updated_at)
- VALUES (?, ?, ?, ?, 0, 0, 'active', ?, ?)
- ON DUPLICATE KEY UPDATE user_id = user_id`,
- [spaceId, userId, spaceName, quotaBytes, now, now]
- );
- const [spaces] = await db.query(
- `SELECT id FROM h5_user_spaces WHERE user_id = ? LIMIT 1`,
- [userId]
- );
- const resolvedSpaceId = spaces[0]?.id;
- if (!resolvedSpaceId) {
- throw new Error("\u7528\u6237\u7A7A\u95F4\u521D\u59CB\u5316\u5931\u8D25");
- }
- for (const category of SYSTEM_CATEGORIES) {
- await db.query(
- `INSERT INTO h5_space_categories
- (id, user_id, space_id, category_code, category_name, visibility_policy,
- ai_access_policy, publish_policy, is_system, sort_order, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- category_name = VALUES(category_name),
- visibility_policy = VALUES(visibility_policy),
- ai_access_policy = VALUES(ai_access_policy),
- publish_policy = VALUES(publish_policy),
- is_system = 1,
- sort_order = VALUES(sort_order),
- updated_at = VALUES(updated_at)`,
- [
- idFactory(),
- userId,
- resolvedSpaceId,
- category.code,
- category.name,
- category.visibilityPolicy,
- category.aiAccessPolicy,
- category.publishPolicy,
- category.sortOrder,
- now,
- now
- ]
- );
- }
- return resolvedSpaceId;
-}
-async function ensureDefaultSpaces(pool2, options = {}) {
- const [users] = await pool2.query(
- `SELECT u.id
- FROM h5_users u
- LEFT JOIN h5_user_spaces s ON s.user_id = u.id
- WHERE u.role = 'user' AND s.id IS NULL`
- );
- for (const user of users) {
- await initializeDefaultSpace(pool2, user.id, options);
- }
- return users.length;
-}
-function createMindSpaceService(pool2, options = {}) {
- const maxFileBytes = Number(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES);
- const aiDailyLimit = Number(options.aiDailyLimit ?? 10);
- const publicPageLimitFallback = Number(options.publicPageLimit ?? 5);
- const monthlyViewLimit = Number(options.monthlyViewLimit ?? 1e3);
- const scheduleService2 = options.scheduleService ?? null;
- const resolvePublicPageLimit = async () => {
- try {
- const config = await withOptionalTimeout(
- loadMindSpaceConfig(pool2),
- 1e3,
- { publicPageLimit: publicPageLimitFallback }
- );
- return Number(config.publicPageLimit ?? publicPageLimitFallback);
- } catch {
- return publicPageLimitFallback;
- }
- };
- const getSpace = async (userId) => {
- const [spaces] = await pool2.query(
- `SELECT id, user_id, space_name, quota_bytes, used_bytes, reserved_bytes, status,
- created_at, updated_at
- FROM h5_user_spaces
- WHERE user_id = ?
- LIMIT 1`,
- [userId]
- );
- const row = spaces[0];
- if (!row) return null;
- const [categories] = await pool2.query(
- `SELECT c.id, c.category_code, c.category_name, c.visibility_policy,
- c.ai_access_policy, c.publish_policy, c.is_system, c.sort_order,
- CASE
- WHEN c.category_code = 'draft' THEN COALESCE(pc.item_count, 0)
- WHEN c.category_code IN ('oa', 'public') THEN COALESCE(pc.item_count, 0) + COALESCE(ac.item_count, 0)
- ELSE COALESCE(ac.item_count, 0)
- END AS item_count
- FROM h5_space_categories c
- LEFT JOIN (
- SELECT category_id, user_id, COUNT(*) AS item_count
- FROM h5_assets
- WHERE user_id = ? AND status <> 'deleted'
- GROUP BY category_id, user_id
- ) ac ON ac.category_id = c.id AND ac.user_id = c.user_id
- LEFT JOIN (
- SELECT category_id, user_id, COUNT(*) AS item_count
- FROM h5_page_records
- WHERE user_id = ? AND status <> 'deleted'
- GROUP BY category_id, user_id
- ) pc ON pc.category_id = c.id AND pc.user_id = c.user_id
- WHERE c.user_id = ? AND c.space_id = ?
- ORDER BY c.sort_order, c.category_name`,
- [userId, userId, userId, row.id]
- );
- const quotaBytes = asNumber(row.quota_bytes);
- const usedBytes = asNumber(row.used_bytes);
- const reservedBytes = asNumber(row.reserved_bytes);
- const [publicationUsage] = await pool2.query(
- `SELECT COUNT(DISTINCT CASE WHEN status = 'online' THEN page_id END) AS public_page_used,
- COALESCE(SUM(CASE WHEN published_at >= ? THEN view_count ELSE 0 END), 0) AS monthly_view_used
- FROM h5_publish_records WHERE user_id = ?`,
- [Date.now() - 30 * 24 * 60 * 60 * 1e3, userId]
- );
- const publicPageLimit = await resolvePublicPageLimit();
- let schedule = null;
- if (scheduleService2) {
- try {
- const [todayTodoItems, upcomingItems, upcomingReminders, digestSubscriptions] = await withOptionalTimeout(
- Promise.all([
- typeof scheduleService2.listTodayTodoItems === "function" ? scheduleService2.listTodayTodoItems({ userId }) : Promise.resolve([]),
- typeof scheduleService2.listUpcomingItems === "function" ? scheduleService2.listUpcomingItems({ userId }) : Promise.resolve([]),
- typeof scheduleService2.listUpcomingReminders === "function" ? scheduleService2.listUpcomingReminders({ userId }) : Promise.resolve([]),
- typeof scheduleService2.listDigestSubscriptions === "function" ? scheduleService2.listDigestSubscriptions({
- userId,
- digestType: "todo_day",
- status: ["active", "locked"],
- limit: 10
- }) : Promise.resolve([])
- ]),
- 1e3,
- [[], [], [], []]
- );
- schedule = { todayTodoItems, upcomingItems, upcomingReminders, digestSubscriptions };
- } catch {
- schedule = null;
- }
- }
- return {
- id: row.id,
- userId: row.user_id,
- name: row.space_name,
- status: row.status,
- quota: {
- quotaBytes,
- usedBytes,
- reservedBytes,
- availableBytes: Math.max(0, quotaBytes - usedBytes - reservedBytes),
- maxFileBytes,
- publicPageLimit,
- publicPageUsed: asNumber(publicationUsage[0]?.public_page_used),
- aiDailyLimit,
- aiDailyUsed: 0,
- monthlyViewLimit,
- monthlyViewUsed: asNumber(publicationUsage[0]?.monthly_view_used)
- },
- categories: categories.map(categoryResponse),
- createdAt: asNumber(row.created_at),
- updatedAt: asNumber(row.updated_at),
- schedule
- };
- };
- const getQuota = async (userId) => {
- const space = await getSpace(userId);
- return space?.quota ?? null;
- };
- const listCategories = async (userId) => {
- const space = await getSpace(userId);
- return space?.categories ?? null;
- };
- return {
- getSpace,
- getQuota,
- listCategories
- };
-}
-
-// db.mjs
-var __dirname = path.dirname(fileURLToPath(import.meta.url));
-function isDatabaseConfigured() {
- return Boolean(
- process.env.DATABASE_URL || process.env.MYSQL_HOST && process.env.MYSQL_DATABASE
- );
-}
-function resolvePoolOptions() {
- const connectionLimit = Math.max(1, Number(process.env.MYSQL_POOL_SIZE ?? 50));
- const queueLimit = Math.max(0, Number(process.env.MYSQL_POOL_QUEUE_LIMIT ?? 0));
- return { waitForConnections: true, connectionLimit, queueLimit };
-}
-function createDbPool() {
- if (!isDatabaseConfigured()) {
- throw new Error("MySQL \u672A\u914D\u7F6E\uFF0C\u8BF7\u8BBE\u7F6E DATABASE_URL \u6216 MYSQL_* \u73AF\u5883\u53D8\u91CF");
- }
- const poolOptions = resolvePoolOptions();
- if (process.env.DATABASE_URL) {
- return mysql.createPool({ uri: process.env.DATABASE_URL, ...poolOptions });
- }
- return mysql.createPool({
- host: process.env.MYSQL_HOST ?? "localhost",
- port: Number(process.env.MYSQL_PORT ?? 3306),
- user: process.env.MYSQL_USER ?? "boot",
- password: process.env.MYSQL_PASSWORD ?? "",
- database: process.env.MYSQL_DATABASE ?? "tkmind",
- ...poolOptions
- });
-}
-async function columnExists(pool2, table, column) {
- const [rows] = await pool2.query(
- `SELECT 1 FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
- LIMIT 1`,
- [table, column]
- );
- return rows.length > 0;
-}
-async function indexExists(pool2, table, index) {
- const [rows] = await pool2.query(
- `SELECT 1 FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?
- LIMIT 1`,
- [table, index]
- );
- return rows.length > 0;
-}
-async function foreignKeyDeleteRule(pool2, table, constraint) {
- const [rows] = await pool2.query(
- `SELECT DELETE_RULE
- FROM information_schema.REFERENTIAL_CONSTRAINTS
- WHERE CONSTRAINT_SCHEMA = DATABASE()
- AND TABLE_NAME = ?
- AND CONSTRAINT_NAME = ?
- LIMIT 1`,
- [table, constraint]
- );
- return rows[0]?.DELETE_RULE ?? null;
-}
-async function ensureForeignKeyDeleteRule(pool2, { table, constraint, column, referencedTable, referencedColumn = "id", deleteRule }) {
- const currentRule = await foreignKeyDeleteRule(pool2, table, constraint);
- if (currentRule === deleteRule) return;
- if (currentRule) {
- await pool2.query(`ALTER TABLE \`${table}\` DROP FOREIGN KEY \`${constraint}\``);
- }
- await pool2.query(
- `ALTER TABLE \`${table}\`
- ADD CONSTRAINT \`${constraint}\`
- FOREIGN KEY (\`${column}\`) REFERENCES \`${referencedTable}\`(\`${referencedColumn}\`)
- ON DELETE ${deleteRule}`
- );
-}
-async function migrateSchema(pool2) {
- const renames = [
- ["h5_user_sessions", "goose_session_id", "agent_session_id"],
- ["h5_session_billing_state", "goose_session_id", "agent_session_id"],
- ["h5_usage_records", "goose_session_id", "agent_session_id"]
- ];
- for (const [table, oldCol, newCol] of renames) {
- if (await columnExists(pool2, table, oldCol)) {
- await pool2.query(
- `ALTER TABLE \`${table}\` CHANGE \`${oldCol}\` \`${newCol}\` VARCHAR(128) NOT NULL`
- );
- }
- }
- const userColumns = [
- ["slug", "VARCHAR(64) NULL AFTER username"],
- ["email", "VARCHAR(255) NULL AFTER slug"],
- [
- "password_algorithm",
- "VARCHAR(32) NOT NULL DEFAULT 'pbkdf2-sha512' AFTER password_hash"
- ],
- ["plan_type", "VARCHAR(32) NOT NULL DEFAULT 'free' AFTER status"]
- ];
- for (const [column, definition] of userColumns) {
- if (!await columnExists(pool2, "h5_users", column)) {
- await pool2.query(`ALTER TABLE h5_users ADD COLUMN \`${column}\` ${definition}`);
- }
- }
- await pool2.query(`UPDATE h5_users SET slug = username WHERE slug IS NULL OR slug = ''`);
- if (!await indexExists(pool2, "h5_users", "uq_h5_users_slug")) {
- await pool2.query(`ALTER TABLE h5_users ADD UNIQUE KEY uq_h5_users_slug (slug)`);
- }
- if (!await indexExists(pool2, "h5_users", "uq_h5_users_email")) {
- await pool2.query(`ALTER TABLE h5_users ADD UNIQUE KEY uq_h5_users_email (email)`);
- }
- if (!await indexExists(pool2, "h5_usage_records", "uniq_h5_usage_request_id")) {
- await pool2.query(
- `ALTER TABLE h5_usage_records ADD UNIQUE KEY uniq_h5_usage_request_id (request_id)`
- );
- }
- const assetForeignKeys = [
- {
- table: "h5_assets",
- constraint: "fk_h5_asset_category",
- column: "category_id",
- referencedTable: "h5_space_categories",
- deleteRule: "CASCADE"
- },
- {
- table: "h5_assets",
- constraint: "fk_h5_asset_parent",
- column: "parent_id",
- referencedTable: "h5_assets",
- deleteRule: "SET NULL"
- },
- {
- table: "h5_asset_versions",
- constraint: "fk_h5_asset_version_user",
- column: "created_by",
- referencedTable: "h5_users",
- deleteRule: "CASCADE"
- },
- {
- table: "h5_upload_sessions",
- constraint: "fk_h5_upload_category",
- column: "category_id",
- referencedTable: "h5_space_categories",
- deleteRule: "CASCADE"
- },
- {
- table: "h5_page_versions",
- constraint: "fk_h5_page_version_content",
- column: "content_asset_id",
- referencedTable: "h5_assets",
- deleteRule: "CASCADE"
- }
- ];
- for (const foreignKey of assetForeignKeys) {
- await ensureForeignKeyDeleteRule(pool2, foreignKey);
- }
- await pool2.query(
- `ALTER TABLE h5_assets
- MODIFY source_type ENUM('upload', 'chat', 'agent', 'template', 'generated', 'workspace')
- NOT NULL DEFAULT 'upload'`
- );
- const llmColumns = [
- ["provider_kind", "ENUM('builtin','custom') NOT NULL DEFAULT 'builtin' AFTER provider_id"],
- ["api_url", "VARCHAR(512) NULL AFTER provider_kind"],
- ["base_path", "VARCHAR(256) NULL AFTER api_url"],
- ["models_json", "TEXT NULL AFTER base_path"],
- ["goosed_provider_id", "VARCHAR(64) NULL AFTER models_json"],
- ["engine", "VARCHAR(32) NOT NULL DEFAULT 'openai' AFTER goosed_provider_id"],
- ["relay_provider", "VARCHAR(64) NULL AFTER engine"],
- ["is_vision_selected", "TINYINT(1) NOT NULL DEFAULT 0 AFTER is_selected"]
- ];
- for (const [column, definition] of llmColumns) {
- if (!await columnExists(pool2, "h5_llm_provider_keys", column)) {
- await pool2.query(`ALTER TABLE h5_llm_provider_keys ADD COLUMN \`${column}\` ${definition}`);
- }
- }
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_llm_executor_bindings (
- id CHAR(36) PRIMARY KEY,
- executor ENUM('goose', 'aider', 'openhands') NOT NULL,
- purpose VARCHAR(32) NOT NULL DEFAULT 'default',
- provider_key_id CHAR(36) NULL,
- model VARCHAR(128) NOT NULL,
- enabled TINYINT(1) NOT NULL DEFAULT 1,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_llm_executor_binding (executor, purpose),
- KEY idx_h5_llm_executor_provider (provider_key_id),
- CONSTRAINT fk_h5_llm_executor_provider FOREIGN KEY (provider_key_id) REFERENCES h5_llm_provider_keys(id) ON DELETE SET NULL
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_experience (
- id CHAR(36) PRIMARY KEY,
- scope VARCHAR(64) NOT NULL DEFAULT 'global',
- kind VARCHAR(32) NOT NULL DEFAULT 'lesson',
- title VARCHAR(255) NOT NULL,
- body MEDIUMTEXT NOT NULL,
- tags_json JSON NULL,
- source_session_id VARCHAR(128) NULL,
- source_user_id CHAR(36) NULL,
- use_count BIGINT NOT NULL DEFAULT 0,
- embedding JSON NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_h5_experience_scope_updated (scope, updated_at),
- FULLTEXT KEY ftx_h5_experience (title, body)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_conversation_messages (
- id CHAR(64) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- agent_session_id VARCHAR(128) NOT NULL,
- message_key VARCHAR(128) NOT NULL,
- sequence_no INT NOT NULL DEFAULT 0,
- role VARCHAR(32) NOT NULL,
- text MEDIUMTEXT NOT NULL,
- raw_json LONGTEXT NULL,
- analyzed_at BIGINT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_conversation_message (agent_session_id, message_key),
- KEY idx_h5_conversation_user_created (user_id, created_at),
- KEY idx_h5_conversation_user_analyzed (user_id, analyzed_at),
- CONSTRAINT fk_h5_conversation_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_conversation_session FOREIGN KEY (agent_session_id) REFERENCES h5_user_sessions(agent_session_id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_agent_runs (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- agent_session_id VARCHAR(128) NULL,
- request_id VARCHAR(128) NOT NULL,
- status ENUM('queued', 'running', 'retryable', 'succeeded', 'failed') NOT NULL DEFAULT 'queued',
- attempts INT NOT NULL DEFAULT 0,
- user_message_json LONGTEXT NOT NULL,
- error_message TEXT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- started_at BIGINT NULL,
- completed_at BIGINT NULL,
- UNIQUE KEY uq_h5_agent_run_request (user_id, request_id),
- KEY idx_h5_agent_run_user_status (user_id, status, updated_at),
- KEY idx_h5_agent_run_session (agent_session_id, updated_at),
- CONSTRAINT fk_h5_agent_run_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_agent_run_session FOREIGN KEY (agent_session_id) REFERENCES h5_user_sessions(agent_session_id) ON DELETE SET NULL
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_agent_run_events (
- id CHAR(36) PRIMARY KEY,
- run_id CHAR(36) NOT NULL,
- event_type VARCHAR(64) NOT NULL,
- data_json JSON NULL,
- created_at BIGINT NOT NULL,
- KEY idx_h5_agent_run_event_run (run_id, created_at),
- 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,
- user_id CHAR(36) NOT NULL,
- label ENUM('preference', 'habit', 'interest', 'goal', 'fact', 'experience', 'knowledge') NOT NULL DEFAULT 'fact',
- memory_hash CHAR(64) NOT NULL,
- memory_text TEXT NOT NULL,
- evidence_message_id CHAR(64) NULL,
- source_session_id VARCHAR(128) NULL,
- confidence DECIMAL(4,3) NOT NULL DEFAULT 0.600,
- status ENUM('active', 'archived', 'deleted') NOT NULL DEFAULT 'active',
- raw_json JSON NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_h5_user_memory_hash (user_id, memory_hash),
- KEY idx_h5_user_memory_user_label (user_id, label, updated_at),
- KEY idx_h5_user_memory_session (source_session_id),
- CONSTRAINT fk_h5_user_memory_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_h5_user_memory_evidence FOREIGN KEY (evidence_message_id) REFERENCES h5_conversation_messages(id) ON DELETE SET NULL,
- CONSTRAINT fk_h5_user_memory_session FOREIGN KEY (source_session_id) REFERENCES h5_user_sessions(agent_session_id) ON DELETE SET NULL
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- const publishColumns = [
- ["user_confirmed_at", "BIGINT NULL AFTER expires_at"],
- ["plaza_view_count", "BIGINT NOT NULL DEFAULT 0"],
- ["plaza_like_count", "BIGINT NOT NULL DEFAULT 0"]
- ];
- for (const [column, definition] of publishColumns) {
- if (!await columnExists(pool2, "h5_publish_records", column)) {
- await pool2.query(`ALTER TABLE h5_publish_records ADD COLUMN \`${column}\` ${definition}`);
- }
- }
- if (!await indexExists(pool2, "h5_publish_records", "idx_h5_publish_auto_private")) {
- await pool2.query(
- `ALTER TABLE h5_publish_records
- ADD KEY idx_h5_publish_auto_private (access_mode, status, user_confirmed_at, expires_at)`
- );
- }
- const plazaUserColumns = [
- ["plaza_post_count", "INT UNSIGNED NOT NULL DEFAULT 0"],
- ["plaza_follower_count", "INT UNSIGNED NOT NULL DEFAULT 0"],
- ["plaza_following_count", "INT UNSIGNED NOT NULL DEFAULT 0"],
- ["plaza_verified", "TINYINT(1) NOT NULL DEFAULT 0"],
- ["plaza_post_banned", "TINYINT(1) NOT NULL DEFAULT 0"],
- ["plaza_comment_banned", "TINYINT(1) NOT NULL DEFAULT 0"],
- [
- "ops_role",
- "ENUM('none','reviewer','editor','ops_admin') NOT NULL DEFAULT 'none'"
- ]
- ];
- for (const [column, definition] of plazaUserColumns) {
- if (!await columnExists(pool2, "h5_users", column)) {
- await pool2.query(`ALTER TABLE h5_users ADD COLUMN \`${column}\` ${definition}`);
- }
- }
- if (!await columnExists(pool2, "h5_users", "signup_source")) {
- await pool2.query(
- `ALTER TABLE h5_users ADD COLUMN signup_source VARCHAR(32) NULL DEFAULT 'password' AFTER workspace_root`
- );
- }
- if (!await columnExists(pool2, "h5_users", "low_balance_gift_eligible")) {
- await pool2.query(
- `ALTER TABLE h5_users ADD COLUMN low_balance_gift_eligible TINYINT(1) NOT NULL DEFAULT 0 AFTER signup_source`
- );
- }
- if (!await columnExists(pool2, "h5_users", "low_balance_gift_granted_at")) {
- await pool2.query(
- `ALTER TABLE h5_users ADD COLUMN low_balance_gift_granted_at BIGINT NULL AFTER low_balance_gift_eligible`
- );
- }
- if (!await columnExists(pool2, "h5_user_sessions", "goosed_node")) {
- await pool2.query(
- `ALTER TABLE h5_user_sessions ADD COLUMN goosed_node TINYINT UNSIGNED NOT NULL DEFAULT 0`
- );
- }
- if (!await columnExists(pool2, "h5_user_sessions", "goosed_target")) {
- await pool2.query(
- `ALTER TABLE h5_user_sessions ADD COLUMN goosed_target VARCHAR(255) NULL AFTER goosed_node`
- );
- }
- if (!await columnExists(pool2, "h5_user_sessions", "origin")) {
- await pool2.query(
- `ALTER TABLE h5_user_sessions ADD COLUMN origin ENUM('h5', 'wechat') NOT NULL DEFAULT 'h5' AFTER user_id`
- );
- }
- if (!await columnExists(pool2, "h5_session_snapshots", "display_title")) {
- await pool2.query(
- `ALTER TABLE h5_session_snapshots ADD COLUMN display_title VARCHAR(512) NOT NULL DEFAULT '' AFTER name`
- );
- }
- const oauthStateColumns = [
- ["intent", "VARCHAR(16) NOT NULL DEFAULT 'login' AFTER utm_campaign"],
- ["bind_user_id", "CHAR(36) NULL AFTER intent"],
- ["auth_mode", "VARCHAR(8) NOT NULL DEFAULT 'mp' AFTER bind_user_id"],
- ["status", "VARCHAR(16) NOT NULL DEFAULT 'pending' AFTER auth_mode"],
- ["result_kind", "VARCHAR(32) NULL AFTER status"],
- ["result_token", "VARCHAR(512) NULL AFTER result_kind"],
- ["result_message", "VARCHAR(255) NULL AFTER result_token"]
- ];
- for (const [column, definition] of oauthStateColumns) {
- if (!await columnExists(pool2, "h5_wechat_oauth_states", column)) {
- await pool2.query(
- `ALTER TABLE h5_wechat_oauth_states ADD COLUMN \`${column}\` ${definition}`
- );
- }
- }
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_wechat_pending_binds (
- token VARCHAR(64) PRIMARY KEY,
- app_id VARCHAR(32) NOT NULL,
- openid VARCHAR(64) NOT NULL,
- unionid VARCHAR(64) NULL,
- nickname VARCHAR(128) NULL,
- avatar_url VARCHAR(512) NULL,
- return_to VARCHAR(512) NULL,
- utm_source VARCHAR(64) NULL,
- utm_medium VARCHAR(64) NULL,
- utm_campaign VARCHAR(64) NULL,
- expires_at BIGINT NOT NULL,
- created_at BIGINT NOT NULL,
- KEY idx_wechat_pending_expires (expires_at)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_wechat_mp_messages (
- app_id VARCHAR(32) NOT NULL,
- openid VARCHAR(64) NOT NULL,
- msg_id VARCHAR(128) NOT NULL,
- status ENUM('processing', 'done', 'failed') NOT NULL DEFAULT 'processing',
- agent_session_id VARCHAR(128) NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- PRIMARY KEY (app_id, openid, msg_id),
- KEY idx_wechat_mp_messages_updated (updated_at),
- KEY idx_wechat_mp_messages_session (agent_session_id)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_wechat_mp_message_details (
- id CHAR(36) PRIMARY KEY,
- app_id VARCHAR(32) NOT NULL,
- openid VARCHAR(64) NOT NULL,
- user_id CHAR(36) NULL,
- msg_id VARCHAR(128) NULL,
- msg_type VARCHAR(32) NOT NULL,
- display_text TEXT NULL,
- agent_text TEXT NULL,
- media_id VARCHAR(256) NULL,
- media_url TEXT NULL,
- media_public_url TEXT NULL,
- media_format VARCHAR(64) NULL,
- location_lat DECIMAL(10,7) NULL,
- location_lng DECIMAL(10,7) NULL,
- location_label VARCHAR(255) NULL,
- link_url TEXT NULL,
- link_title VARCHAR(255) NULL,
- raw_xml_hash CHAR(40) NULL,
- raw_json JSON NULL,
- created_at BIGINT NOT NULL,
- KEY idx_wechat_mp_message_details_user (app_id, openid, created_at),
- KEY idx_wechat_mp_message_details_msg (msg_id),
- KEY idx_wechat_mp_message_details_type (msg_type, created_at),
- CONSTRAINT fk_wechat_mp_message_details_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE SET NULL
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_schedule_items (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- kind ENUM('task', 'event') NOT NULL,
- title VARCHAR(255) NOT NULL,
- description TEXT NULL,
- status ENUM('active', 'completed', 'cancelled', 'deleted') NOT NULL DEFAULT 'active',
- start_at BIGINT NULL,
- end_at BIGINT NULL,
- due_at BIGINT NULL,
- all_day TINYINT(1) NOT NULL DEFAULT 0,
- timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
- location VARCHAR(255) NULL,
- source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent',
- source_session_id VARCHAR(128) NULL,
- source_message_id VARCHAR(128) NULL,
- source_text TEXT NULL,
- metadata_json JSON NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- deleted_at BIGINT NULL,
- KEY idx_schedule_user_time (user_id, status, start_at, due_at),
- KEY idx_schedule_user_updated (user_id, updated_at),
- CONSTRAINT fk_schedule_item_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_schedule_reminders (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- item_id CHAR(36) NOT NULL,
- remind_at BIGINT NOT NULL,
- offset_minutes INT NULL,
- channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat',
- status ENUM('pending', 'locked', 'sent', 'failed', 'cancelled') NOT NULL DEFAULT 'pending',
- attempts INT NOT NULL DEFAULT 0,
- last_error VARCHAR(500) NULL,
- locked_until BIGINT NULL,
- sent_at BIGINT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_schedule_item_remind_at (item_id, remind_at, channel),
- KEY idx_reminder_due (status, remind_at),
- KEY idx_reminder_user (user_id, status, remind_at),
- CONSTRAINT fk_schedule_reminder_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
- CONSTRAINT fk_schedule_reminder_item FOREIGN KEY (item_id) REFERENCES h5_schedule_items(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_schedule_digest_subscriptions (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- digest_type ENUM('todo_day') NOT NULL DEFAULT 'todo_day',
- hour TINYINT UNSIGNED NOT NULL,
- minute TINYINT UNSIGNED NOT NULL DEFAULT 0,
- timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
- channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat',
- status ENUM('active', 'locked', 'failed', 'cancelled') NOT NULL DEFAULT 'active',
- next_run_at BIGINT NOT NULL,
- last_run_at BIGINT NULL,
- attempts INT NOT NULL DEFAULT 0,
- locked_until BIGINT NULL,
- last_error VARCHAR(500) NULL,
- source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent',
- source_session_id VARCHAR(128) NULL,
- source_message_id VARCHAR(128) NULL,
- source_text TEXT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_schedule_digest_user_type_channel (user_id, digest_type, channel),
- KEY idx_schedule_digest_due (status, next_run_at),
- CONSTRAINT fk_schedule_digest_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_balance_alert_subscriptions (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- threshold_cents BIGINT NOT NULL,
- channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat',
- status ENUM('active', 'locked', 'failed', 'cancelled') NOT NULL DEFAULT 'active',
- next_run_at BIGINT NOT NULL,
- last_run_at BIGINT NULL,
- last_notified_balance_cents BIGINT NULL,
- attempts INT NOT NULL DEFAULT 0,
- locked_until BIGINT NULL,
- last_error VARCHAR(500) NULL,
- source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent',
- source_session_id VARCHAR(128) NULL,
- source_message_id VARCHAR(128) NULL,
- source_text TEXT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- UNIQUE KEY uq_balance_alert_user_channel (user_id, channel),
- KEY idx_balance_alert_due (status, next_run_at),
- KEY idx_balance_alert_user (user_id, status, next_run_at),
- CONSTRAINT fk_balance_alert_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_schedule_delivery_logs (
- id CHAR(36) PRIMARY KEY,
- reminder_id CHAR(36) NULL,
- subscription_id CHAR(36) NULL,
- user_id CHAR(36) NOT NULL,
- channel ENUM('wechat', 'in_app') NOT NULL,
- status ENUM('success', 'failed') NOT NULL,
- provider_message_id VARCHAR(128) NULL,
- error_code VARCHAR(64) NULL,
- error_message VARCHAR(500) NULL,
- created_at BIGINT NOT NULL,
- KEY idx_delivery_reminder (reminder_id, created_at),
- KEY idx_delivery_subscription (subscription_id, created_at),
- KEY idx_delivery_user (user_id, created_at),
- CONSTRAINT fk_schedule_delivery_reminder FOREIGN KEY (reminder_id) REFERENCES h5_schedule_reminders(id) ON DELETE CASCADE,
- CONSTRAINT fk_schedule_delivery_subscription FOREIGN KEY (subscription_id) REFERENCES h5_schedule_digest_subscriptions(id) ON DELETE CASCADE,
- CONSTRAINT fk_schedule_delivery_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_user_notifications (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- channel ENUM('web', 'wechat') NOT NULL DEFAULT 'web',
- notification_type VARCHAR(64) NOT NULL,
- title VARCHAR(255) NOT NULL,
- body TEXT NOT NULL,
- data_json JSON NULL,
- status ENUM('unread', 'read') NOT NULL DEFAULT 'unread',
- read_at BIGINT NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_user_notifications_user_status_created (user_id, status, created_at),
- KEY idx_user_notifications_user_created (user_id, created_at),
- CONSTRAINT fk_user_notifications_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(
- `ALTER TABLE h5_payment_orders
- MODIFY pay_mode ENUM('native', 'h5', 'jsapi') NOT NULL DEFAULT 'native'`
- );
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_session_snapshots (
- agent_session_id VARCHAR(128) NOT NULL PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- name VARCHAR(512) NOT NULL DEFAULT '',
- display_title VARCHAR(512) NOT NULL DEFAULT '',
- working_dir VARCHAR(1024) NOT NULL DEFAULT '',
- created_at_str VARCHAR(64) NOT NULL DEFAULT '',
- updated_at_str VARCHAR(64) NOT NULL DEFAULT '',
- user_set_name TINYINT(1) NOT NULL DEFAULT 0,
- recipe_json TEXT NULL,
- synced_msg_count INT NOT NULL DEFAULT 0,
- source_updated_at VARCHAR(64) NOT NULL DEFAULT '',
- messages_json LONGTEXT NOT NULL,
- synced_at BIGINT NOT NULL,
- KEY idx_h5_snapshots_user (user_id),
- KEY idx_h5_snapshots_synced (synced_at),
- CONSTRAINT fk_h5_snapshot_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_subscriptions (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- plan_type VARCHAR(32) NOT NULL DEFAULT 'free',
- status ENUM('active', 'expired', 'cancelled') NOT NULL DEFAULT 'active',
- period_tokens_limit BIGINT NOT NULL DEFAULT 0,
- period_tokens_used BIGINT NOT NULL DEFAULT 0,
- period_start BIGINT NOT NULL,
- period_end BIGINT NOT NULL,
- expires_at BIGINT NOT NULL,
- overage_rate DECIMAL(4,2) NOT NULL DEFAULT 1.00,
- operator_id CHAR(36) NULL,
- note VARCHAR(512) NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_h5_sub_user_status (user_id, status),
- KEY idx_h5_sub_expires (expires_at, status),
- CONSTRAINT fk_h5_sub_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await pool2.query(`
- INSERT INTO h5_subscriptions
- (id, user_id, plan_type, status,
- period_tokens_limit, period_tokens_used,
- period_start, period_end, expires_at, overage_rate,
- operator_id, note, created_at, updated_at)
- SELECT
- UUID(),
- u.id,
- 'free',
- 'active',
- 150000, 0,
- UNIX_TIMESTAMP() * 1000,
- (UNIX_TIMESTAMP() + 30 * 86400) * 1000,
- (UNIX_TIMESTAMP() + 30 * 86400) * 1000,
- 1.00,
- NULL,
- '\u7CFB\u7EDF\u8FC1\u79FB\u8865\u5EFA\u514D\u8D39\u5957\u9910',
- UNIX_TIMESTAMP() * 1000,
- UNIX_TIMESTAMP() * 1000
- FROM h5_users u
- WHERE NOT EXISTS (
- SELECT 1 FROM h5_subscriptions s
- WHERE s.user_id = u.id AND s.status = 'active'
- )
- `);
- await pool2.query(`
- CREATE TABLE IF NOT EXISTS h5_user_feedback (
- id CHAR(36) PRIMARY KEY,
- user_id CHAR(36) NOT NULL,
- type ENUM('bug', 'feature', 'other') NOT NULL,
- title VARCHAR(120) NOT NULL,
- description TEXT NOT NULL,
- contact VARCHAR(120) NULL,
- status ENUM('pending', 'reviewing', 'resolved', 'closed') NOT NULL DEFAULT 'pending',
- images_json JSON NULL,
- context_json JSON NULL,
- created_at BIGINT NOT NULL,
- updated_at BIGINT NOT NULL,
- KEY idx_h5_user_feedback_user_created (user_id, created_at),
- KEY idx_h5_user_feedback_status_created (status, created_at),
- CONSTRAINT fk_h5_user_feedback_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
-}
-async function initSchema(pool2) {
- const schemaPath = path.join(__dirname, "schema.sql");
- const sql = fs2.readFileSync(schemaPath, "utf8").split("\n").filter((line) => !line.trim().startsWith("--")).join("\n");
- for (const statement of sql.split(";")) {
- const trimmed = statement.trim();
- if (trimmed) {
- await pool2.query(trimmed);
- }
- }
- await migrateSchema(pool2);
- await ensureDefaultSpaces(pool2, {
- quotaBytes: Number(process.env.MINDSPACE_FREE_QUOTA_BYTES ?? 5 * 1024 * 1024)
- });
-}
-
-// 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();
-}
-function safeJsonParse(value, fallback = null) {
- try {
- return JSON.parse(value);
- } catch {
- return fallback;
- }
-}
-function serializeMessage(message) {
- return JSON.stringify(message ?? {});
-}
-function positiveInteger(value, fallback) {
- const n = Number(value);
- if (!Number.isFinite(n) || n <= 0) return fallback;
- return Math.floor(n);
-}
-function timeoutError(ms) {
- const err = new Error(`agent run timed out after ${ms}ms`);
- err.code = "AGENT_RUN_TIMEOUT";
- return err;
-}
-function envFlag(value, fallback = false) {
- const raw = String(value ?? "").trim().toLowerCase();
- if (!raw) return fallback;
- return ["1", "true", "yes", "on"].includes(raw);
-}
-function normalizeAgentRunToolMode(value) {
- const normalized = String(value ?? "chat").trim().toLowerCase();
- if (!normalized || normalized === "chat") return "chat";
- if (CODE_TOOL_MODES.has(normalized)) return "code";
- throw new Error(`\u4E0D\u652F\u6301\u7684 tool_mode: ${value}`);
-}
-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 } : {};
- metadata[RUN_METADATA_KEY] = {
- ...metadata[RUN_METADATA_KEY] && typeof metadata[RUN_METADATA_KEY] === "object" ? metadata[RUN_METADATA_KEY] : {},
- toolMode: normalizeAgentRunToolMode(toolMode),
- ...taskType ? { taskType } : {}
- };
- return { ...message, metadata };
-}
-function getRunOptionsFromMessage(userMessage) {
- const metadata = userMessage?.metadata;
- const runMetadata = metadata?.[RUN_METADATA_KEY] ?? metadata?.agentRun ?? {};
- let toolMode = "chat";
- try {
- toolMode = normalizeAgentRunToolMode(runMetadata?.toolMode ?? metadata?.toolMode ?? "chat");
- } catch {
- toolMode = "chat";
- }
- return {
- toolMode,
- taskType: normalizeTaskType(runMetadata?.taskType ?? metadata?.taskType),
- validation: normalizeToolGatewayValidation(runMetadata?.validation ?? metadata?.toolGatewayValidation)
- };
-}
-function projectRun(row) {
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- sessionId: row.agent_session_id ?? null,
- requestId: row.request_id,
- status: row.status,
- attempts: Number(row.attempts ?? 0),
- error: row.error_message ?? null,
- createdAt: Number(row.created_at ?? 0),
- updatedAt: Number(row.updated_at ?? 0),
- startedAt: row.started_at == null ? null : Number(row.started_at),
- completedAt: row.completed_at == null ? null : Number(row.completed_at)
- };
-}
-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(
- process.env.MEMIND_AGENT_RUN_QUEUE_CONCURRENCY,
- DEFAULT_MAX_CONCURRENT_RUNS
- ),
- 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();
- const queuedDispatches = [];
- const queuedDispatchSet = /* @__PURE__ */ new Set();
- function enqueueRun(runId) {
- if (!runId || inFlight.has(runId) || queuedDispatchSet.has(runId)) return false;
- queuedDispatchSet.add(runId);
- queuedDispatches.push(runId);
- return true;
- }
- function nextQueuedRunId() {
- const runId = queuedDispatches.shift();
- if (runId) queuedDispatchSet.delete(runId);
- return runId ?? null;
- }
- async function appendEvent(runId, type, data = null) {
- await pool2.query(
- `INSERT INTO h5_agent_run_events (id, run_id, event_type, data_json, created_at)
- VALUES (?, ?, ?, ?, ?)`,
- [
- crypto3.randomUUID(),
- runId,
- type,
- data == null ? null : JSON.stringify(data),
- nowMs()
- ]
- );
- }
- async function getRunById(runId) {
- const [rows] = await pool2.query(
- `SELECT * FROM h5_agent_runs WHERE id = ? LIMIT 1`,
- [runId]
- );
- return rows[0] ?? null;
- }
- async function getRunForUser(userId, runId) {
- const [rows] = await pool2.query(
- `SELECT * FROM h5_agent_runs WHERE id = ? AND user_id = ? LIMIT 1`,
- [runId, userId]
- );
- return projectRun(rows[0] ?? null);
- }
- async function getRunByRequest(userId, requestId) {
- const [rows] = await pool2.query(
- `SELECT * FROM h5_agent_runs WHERE user_id = ? AND request_id = ? LIMIT 1`,
- [userId, requestId]
- );
- return rows[0] ?? null;
- }
- async function createRun(userId, {
- sessionId = null,
- requestId,
- userMessage,
- toolMode = "chat",
- taskType = null
- }) {
- const normalizedRequestId = String(requestId ?? "").trim();
- if (!normalizedRequestId) {
- throw new Error("\u7F3A\u5C11 request_id");
- }
- const normalizedToolMode = normalizeAgentRunToolMode(toolMode);
- const normalizedTaskType = normalizeTaskType(taskType);
- const existing = await getRunByRequest(userId, normalizedRequestId);
- if (existing) {
- if (autoDispatch && !TERMINAL_STATUSES.has(existing.status)) dispatchRun(existing.id);
- return projectRun(existing);
- }
- const runId = crypto3.randomUUID();
- const createdAt = nowMs();
- const runMessage = withRunMetadata(userMessage, {
- toolMode: normalizedToolMode,
- taskType: normalizedTaskType
- });
- await pool2.query(
- `INSERT INTO h5_agent_runs
- (id, user_id, agent_session_id, request_id, status, attempts,
- user_message_json, error_message, created_at, updated_at, started_at, completed_at)
- VALUES (?, ?, ?, ?, 'queued', 0, ?, NULL, ?, ?, NULL, NULL)`,
- [
- runId,
- userId,
- sessionId || null,
- normalizedRequestId,
- serializeMessage(runMessage),
- createdAt,
- createdAt
- ]
- );
- await appendEvent(runId, "queued", {
- sessionId: sessionId || null,
- toolMode: normalizedToolMode,
- taskType: normalizedTaskType
- });
- if (autoDispatch) dispatchRun(runId);
- return projectRun(await getRunById(runId));
- }
- async function markRun(runId, status, fields = {}) {
- const updates = ["status = ?", "updated_at = ?"];
- const values = [status, nowMs()];
- for (const [key, value] of Object.entries(fields)) {
- updates.push(`${key} = ?`);
- values.push(value);
- }
- values.push(runId);
- await pool2.query(
- `UPDATE h5_agent_runs SET ${updates.join(", ")} WHERE id = ?`,
- values
- );
- 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) => {
- timer = setTimeout(() => reject(timeoutError(runTimeoutMs)), runTimeoutMs);
- });
- try {
- return await Promise.race([task(), timeout]);
- } finally {
- if (timer) clearTimeout(timer);
- }
- }
- 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 = {};
- if (runOptions.toolMode === "code" && userAuth2?.getCodeAgentSessionPolicy) {
- sessionOptions.sessionPolicy = await userAuth2.getCodeAgentSessionPolicy(row.user_id);
- }
- const session = await tkmindProxy2.startSessionForUser(row.user_id, sessionOptions);
- sessionId = session.id;
- await pool2.query(
- `UPDATE h5_agent_runs SET agent_session_id = ?, updated_at = ? WHERE id = ?`,
- [sessionId, nowMs(), runId]
- );
- await appendEvent(runId, "session_started", {
- sessionId,
- toolMode: runOptions.toolMode,
- taskType: runOptions.taskType
- });
- }
- await tkmindProxy2.submitSessionReplyForUser(
- row.user_id,
- sessionId,
- row.request_id,
- userMessage,
- { toolMode: runOptions.toolMode }
- );
- return { sessionId };
- }
- async function processRun(runId) {
- const row = await getRunById(runId);
- if (!row || TERMINAL_STATUSES.has(row.status)) return;
- const nextAttempt = Number(row.attempts ?? 0) + 1;
- const [claim] = await pool2.query(
- `UPDATE h5_agent_runs
- SET status = 'running', attempts = ?, started_at = COALESCE(started_at, ?), updated_at = ?, error_message = NULL
- WHERE id = ? AND status IN ('queued', 'retryable')`,
- [nextAttempt, nowMs(), nowMs(), runId]
- );
- 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", {
- agent_session_id: sessionId,
- completed_at: nowMs()
- });
- } catch (err) {
- const message = err instanceof Error ? err.message : String(err);
- const timedOut = err?.code === "AGENT_RUN_TIMEOUT";
- if (timedOut) {
- await appendEvent(runId, "timeout", { timeoutMs: runTimeoutMs });
- }
- const retryable = !timedOut && err?.retryable !== false && nextAttempt < retryDelaysMs.length;
- await markRun(runId, retryable ? "retryable" : "failed", {
- error_message: message,
- completed_at: retryable ? null : nowMs()
- });
- if (retryable && autoDispatch) {
- setTimeout(() => dispatchRun(runId), retryDelaysMs[nextAttempt - 1]);
- }
- } finally {
- stopHeartbeat();
- }
- }
- function drainQueue() {
- while (inFlight.size < maxConcurrentRuns) {
- const runId = nextQueuedRunId();
- if (!runId) break;
- inFlight.add(runId);
- void processRun(runId).finally(() => {
- inFlight.delete(runId);
- drainQueue();
- });
- }
- }
- function dispatchRun(runId) {
- if (!enqueueRun(runId)) return;
- drainQueue();
- }
- async function getQueueStatus() {
- const [rows] = await pool2.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 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,
- 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()
- };
- }
- const take = Math.min(dispatchLimit, available);
- const [rows] = await pool2.query(
- `SELECT id
- FROM h5_agent_runs
- WHERE status IN ('queued', 'retryable')
- ORDER BY updated_at ASC
- LIMIT ?`,
- [take]
- );
- for (const row of rows) {
- dispatchRun(row.id);
- }
- return {
- considered: rows.length,
- dispatched: rows.length,
- staleRecovery,
- queue: await getQueueStatus()
- };
- }
- return {
- createRun,
- getRunForUser,
- dispatchRun,
- getQueueStatus,
- 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 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 = 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 {
- const sessionId = String(request.body?.session_id ?? "").trim() || null;
- const requestId = String(request.body?.request_id ?? "").trim();
- const userMessage = request.body?.user_message ?? null;
- const rawToolMode = request.body?.tool_mode ?? request.body?.toolMode ?? "chat";
- const taskType = String(request.body?.task_type ?? request.body?.taskType ?? "").trim() || null;
- if (!requestId) {
- response.status(400).json({ message: "\u7F3A\u5C11 request_id" });
- return;
- }
- if (!userMessage) {
- response.status(400).json({ message: "\u7F3A\u5C11 user_message" });
- return;
- }
- let toolMode = "chat";
- try {
- toolMode = normalizeAgentRunToolMode(rawToolMode);
- } catch (err) {
- response.status(400).json({
- message: err instanceof Error ? err.message : "\u4E0D\u652F\u6301\u7684 tool_mode"
- });
- return;
- }
- if (toolMode === "code" && !codeRunsEnabled) {
- 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) {
- response.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" });
- return;
- }
- }
- const run = await agentRunGateway2.createRun(request.currentUser.id, {
- sessionId,
- requestId,
- userMessage,
- toolMode,
- taskType
- });
- response.status(202).json({ run });
- } catch (err) {
- response.status(500).json({
- message: err instanceof Error ? err.message : "\u521B\u5EFA\u4EFB\u52A1\u5931\u8D25"
- });
- }
- };
-}
-function createGetAgentRunHandler({ agentRunGateway: agentRunGateway2 }) {
- return async function getAgentRun(request, response) {
- try {
- const run = await agentRunGateway2.getRunForUser(
- request.currentUser.id,
- request.params.runId
- );
- if (!run) {
- response.status(404).json({ message: "\u4EFB\u52A1\u4E0D\u5B58\u5728" });
- return;
- }
- if (run.status !== "succeeded" && run.status !== "failed") {
- agentRunGateway2.dispatchRun(run.id);
- }
- response.json({ run });
- } catch (err) {
- response.status(500).json({
- message: err instanceof Error ? err.message : "\u8BFB\u53D6\u4EFB\u52A1\u5931\u8D25"
- });
- }
- };
-}
-function createAgentRunEventsHandler({
- agentRunGateway: agentRunGateway2,
- pollIntervalMs = 1e3,
- keepaliveIntervalMs = 2e4
-}) {
- return async function getAgentRunEvents(request, response) {
- const userId = request.currentUser.id;
- const runId = request.params.runId;
- let closed = false;
- let lastPayload = null;
- let pollTimer = null;
- let keepaliveTimer = null;
- const cleanup = () => {
- closed = true;
- if (pollTimer) clearInterval(pollTimer);
- if (keepaliveTimer) clearInterval(keepaliveTimer);
- pollTimer = null;
- keepaliveTimer = null;
- };
- const sendEvent = (event, data) => {
- response.write(`event: ${event}
-`);
- response.write(`data: ${JSON.stringify(data)}
-
-`);
- };
- const publish = async (prefetchedRun = null) => {
- if (closed) return;
- try {
- const run = prefetchedRun ?? await agentRunGateway2.getRunForUser(userId, runId);
- if (!run) {
- sendEvent("error", { message: "\u4EFB\u52A1\u4E0D\u5B58\u5728" });
- cleanup();
- response.end();
- return;
- }
- const nextPayload = JSON.stringify(run);
- if (nextPayload !== lastPayload) {
- lastPayload = nextPayload;
- sendEvent("run", { run });
- }
- if (run.status !== "succeeded" && run.status !== "failed") {
- agentRunGateway2.dispatchRun(run.id);
- return;
- }
- cleanup();
- response.end();
- } catch (err) {
- sendEvent("error", {
- message: err instanceof Error ? err.message : "\u8BFB\u53D6\u4EFB\u52A1\u5931\u8D25"
- });
- cleanup();
- response.end();
- }
- };
- const firstRun = await agentRunGateway2.getRunForUser(userId, runId);
- if (!firstRun) {
- response.status(404).json({ message: "\u4EFB\u52A1\u4E0D\u5B58\u5728" });
- return;
- }
- response.status(200);
- response.setHeader("Content-Type", "text/event-stream");
- response.setHeader("Cache-Control", "no-cache");
- response.setHeader("Connection", "keep-alive");
- request.on("close", cleanup);
- keepaliveTimer = setInterval(() => {
- if (!closed) response.write(": keepalive\n\n");
- }, keepaliveIntervalMs);
- pollTimer = setInterval(() => {
- void publish();
- }, pollIntervalMs);
- await publish(firstRun);
- };
-}
-
-// tkmind-proxy.mjs
-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";
-
-// sse-billing.mjs
-import { Transform } from "node:stream";
-function parseSseChunk(chunk) {
- const events = [];
- const blocks = chunk.split("\n\n");
- for (const block of blocks) {
- if (!block.trim()) continue;
- let data;
- for (const line of block.split("\n")) {
- if (line.startsWith("data:")) {
- data = line.slice(5).trim();
- }
- }
- if (!data) continue;
- try {
- events.push(JSON.parse(data));
- } catch {
- }
- }
- return events;
-}
-function finishBillingKey(event) {
- const requestId = event.request_id ?? event.chat_request_id;
- if (requestId) return String(requestId);
- const state = event.token_state ?? {};
- const input = state.accumulatedInputTokens ?? state.accumulated_input_tokens ?? state.inputTokens ?? 0;
- const output = state.accumulatedOutputTokens ?? state.accumulated_output_tokens ?? state.outputTokens ?? 0;
- return `${input}:${output}`;
-}
-function createSseBillingTransform({ onFinish }) {
- let buffer = "";
- const billedFinishKeys = /* @__PURE__ */ new Set();
- const billFinishOnce = (event) => {
- if (event?.type !== "Finish" || !event.token_state) return;
- const key = finishBillingKey(event);
- if (billedFinishKeys.has(key)) return;
- billedFinishKeys.add(key);
- void onFinish(event).catch(() => {
- });
- };
- return new Transform({
- transform(chunk, _encoding, callback) {
- buffer += chunk.toString("utf8");
- const parts = buffer.split("\n\n");
- buffer = parts.pop() ?? "";
- for (const block of parts) {
- if (!block.trim()) continue;
- let dataLine;
- for (const line of block.split("\n")) {
- if (line.startsWith("data:")) dataLine = line.slice(5).trim();
- }
- if (!dataLine) continue;
- try {
- billFinishOnce(JSON.parse(dataLine));
- } catch {
- }
- }
- callback(null, chunk);
- },
- flush(callback) {
- if (buffer.trim()) {
- for (const event of parseSseChunk(`${buffer}
-
-`)) {
- billFinishOnce(event);
- }
- }
- callback();
- }
- });
-}
-function appendBalanceEvent(payload) {
- const data = typeof payload === "number" ? { balanceCents: payload } : payload && typeof payload === "object" ? payload : { balanceCents: 0 };
- return `event: balance
-data: ${JSON.stringify(data)}
-
-`;
-}
-
-// capabilities.mjs
-import path3 from "node:path";
-import { fileURLToPath as fileURLToPath2 } from "node:url";
-
-// policies.mjs
-var POLICY_CATALOG = [
- {
- key: "goose_mode",
- label: "TKMind \u6A21\u5F0F",
- description: "H5 \u7EC8\u7AEF\u7528\u6237\u4E0D\u5728\u804A\u5929\u91CC\u70B9\u300C\u5141\u8BB8/\u62D2\u7EDD\u300D\uFF1B\u53EF\u7528\u5DE5\u5177\u7531\u300C\u80FD\u529B\u6743\u9650\u300D\u51B3\u5B9A\u3002chat \u4E0D\u8C03\u7528\u5DE5\u5177\uFF0Cauto \u5728\u6388\u6743\u8303\u56F4\u5185\u81EA\u52A8\u6267\u884C\u3002",
- type: "select",
- options: [
- { value: "chat", label: "\u4EC5\u804A\u5929 (chat)" },
- { value: "auto", label: "\u81EA\u52A8\u6267\u884C (auto)" }
- ],
- defaultValue: "chat",
- category: "goose",
- risk: "high"
- },
- {
- key: "workspace_access",
- label: "\u5DE5\u4F5C\u533A\u8BBF\u95EE",
- description: "readonly \u65F6\u7981\u6B62 shell \u4E0E\u6587\u4EF6\u5199\u5165\uFF0C\u4EC5\u5141\u8BB8\u6D4F\u89C8\u4E0E\u5206\u6790",
- type: "select",
- options: [
- { value: "readwrite", label: "\u8BFB\u5199" },
- { value: "readonly", label: "\u53EA\u8BFB" }
- ],
- defaultValue: "readwrite",
- category: "workspace",
- risk: "medium"
- },
- {
- key: "network_egress",
- label: "\u7F51\u7EDC\u51FA\u7AD9",
- description: "deny \u65F6\u7981\u7528 shell\u3001\u7535\u8111\u63A7\u5236\u3001\u6C99\u7BB1\u811A\u672C\u7B49\u53EF\u80FD\u8BBF\u95EE\u7F51\u7EDC\u7684\u6269\u5C55",
- type: "select",
- options: [
- { value: "deny", label: "\u7981\u6B62\uFF08\u63A8\u8350\uFF09" },
- { value: "allow", label: "\u5141\u8BB8\uFF08\u9700\u540C\u65F6\u5F00\u542F\u5BF9\u5E94\u80FD\u529B\uFF09" }
- ],
- defaultValue: "deny",
- category: "network",
- risk: "high"
- },
- {
- key: "api_lockdown",
- label: "API \u4EE3\u7406\u9501\u5B9A",
- description: "\u5F00\u542F\u540E\u4EC5\u5141\u8BB8\u804A\u5929\u76F8\u5173 API\uFF0C\u62E6\u622A\u6539\u6A21\u5F0F\u3001\u52A0\u6269\u5C55\u3001\u5199\u914D\u7F6E\u7B49\u5371\u9669\u8BF7\u6C42",
- type: "boolean",
- defaultValue: true,
- category: "proxy",
- risk: "medium"
- },
- {
- key: "code_delegate_executor",
- label: "\u4EE3\u7801\u59D4\u6258\u6267\u884C\u5668",
- description: "\u63A7\u5236 Goose \u5728\u591A\u6587\u4EF6\u7F16\u7801\u3001\u4FEE\u590D\u4E0E\u91CD\u6784\u4EFB\u52A1\u4E2D\u4F18\u5148\u59D4\u6258\u7ED9\u8C01\u3002auto \u7531 Goose \u7ED3\u5408\u5F53\u524D\u53EF\u7528\u6269\u5C55\u81EA\u884C\u5224\u65AD\uFF1Baider / openhands \u5219\u4F18\u5148\u4F7F\u7528\u6307\u5B9A\u6267\u884C\u5668\u3002",
- type: "select",
- options: [
- { value: "auto", label: "\u81EA\u52A8\u9009\u62E9 (auto)" },
- { value: "aider", label: "\u4F18\u5148 Aider" },
- { value: "openhands", label: "\u4F18\u5148 OpenHands" }
- ],
- defaultValue: "auto",
- category: "routing",
- risk: "medium"
- },
- {
- key: "code_task_routing",
- label: "\u4EE3\u7801\u4EFB\u52A1\u8DEF\u7531\u7B56\u7565",
- description: "\u5B9A\u4E49\u4EE3\u7801\u4EFB\u52A1\u5982\u4F55\u5728 Aider \u4E0E OpenHands \u4E4B\u95F4\u5206\u6D41\u3002balanced \u7531 Goose \u6309\u4EFB\u52A1\u590D\u6742\u5EA6\u5224\u65AD\uFF1Bsplit \u5EFA\u8BAE\u5C0F\u6539\u52A8\u8D70 Aider\u3001\u590D\u6742\u4EFB\u52A1\u8D70 OpenHands\uFF1Bforce_* \u5219\u5F3A\u5236\u4F18\u5148\u5355\u4E00\u8DEF\u5F84\u3002",
- type: "select",
- options: [
- { value: "balanced", label: "\u5E73\u8861\u8DEF\u7531 (balanced)" },
- { value: "split", label: "\u5C0F\u6539\u52A8 Aider\uFF0C\u590D\u6742\u4EFB\u52A1 OpenHands" },
- { value: "force_aider", label: "\u5C3D\u91CF\u7EDF\u4E00\u8D70 Aider" },
- { value: "force_openhands", label: "\u5C3D\u91CF\u7EDF\u4E00\u8D70 OpenHands" }
- ],
- defaultValue: "balanced",
- category: "routing",
- risk: "medium"
- }
-];
-var DEFAULT_USER_POLICIES = Object.fromEntries(
- POLICY_CATALOG.map((item) => [item.key, item.defaultValue])
-);
-var POLICY_KEYS = new Set(POLICY_CATALOG.map((item) => item.key));
-var GOOSE_MODES = /* @__PURE__ */ new Set(["auto", "approve", "smart_approve", "chat"]);
-var WORKSPACE_ACCESS = /* @__PURE__ */ new Set(["readwrite", "readonly"]);
-var NETWORK_EGRESS = /* @__PURE__ */ new Set(["allow", "deny"]);
-var CODE_DELEGATE_EXECUTORS = /* @__PURE__ */ new Set(["auto", "aider", "openhands"]);
-var CODE_TASK_ROUTINGS = /* @__PURE__ */ new Set(["balanced", "split", "force_aider", "force_openhands"]);
-function policyKeys() {
- return [...POLICY_KEYS];
-}
-function isValidPolicyKey(key) {
- return POLICY_KEYS.has(key);
-}
-function normalizePolicyPatch(patch) {
- const normalized = {};
- for (const [key, raw] of Object.entries(patch ?? {})) {
- if (!isValidPolicyKey(key)) continue;
- const def = POLICY_CATALOG.find((item) => item.key === key);
- if (!def) continue;
- if (def.type === "boolean") {
- normalized[key] = raw === true || raw === "true" || raw === 1 || raw === "1";
- continue;
- }
- const value = String(raw ?? "").trim();
- if (def.key === "goose_mode" && GOOSE_MODES.has(value)) normalized[key] = value;
- if (def.key === "workspace_access" && WORKSPACE_ACCESS.has(value)) normalized[key] = value;
- if (def.key === "network_egress" && NETWORK_EGRESS.has(value)) normalized[key] = value;
- if (def.key === "code_delegate_executor" && CODE_DELEGATE_EXECUTORS.has(value)) {
- normalized[key] = value;
- }
- if (def.key === "code_task_routing" && CODE_TASK_ROUTINGS.has(value)) {
- normalized[key] = value;
- }
- }
- return normalized;
-}
-function resolvePolicies(rolePolicies, userOverrides) {
- const resolved = { ...DEFAULT_USER_POLICIES };
- for (const [key, value] of Object.entries(rolePolicies ?? {})) {
- if (isValidPolicyKey(key)) resolved[key] = value;
- }
- for (const [key, value] of Object.entries(userOverrides ?? {})) {
- if (isValidPolicyKey(key)) resolved[key] = value;
- }
- if (resolved.goose_mode === "approve" || resolved.goose_mode === "smart_approve") {
- resolved.goose_mode = "auto";
- }
- return resolved;
-}
-function hasExecutableTools(capabilities) {
- return Boolean(
- capabilities?.static_publish || capabilities?.shell || capabilities?.filesystem || capabilities?.code_browse || capabilities?.subagent || capabilities?.code_sandbox || capabilities?.computer || capabilities?.charts || capabilities?.aider || capabilities?.openhands || capabilities?.apps || capabilities?.todo || capabilities?.skills || capabilities?.chat_recall
- );
-}
-function resolveAgentGooseMode(capabilities, policies) {
- const requested = policies?.goose_mode ?? DEFAULT_USER_POLICIES.goose_mode;
- if (!hasExecutableTools(capabilities)) {
- return "chat";
- }
- if (requested === "chat" || requested === "approve" || requested === "smart_approve") {
- return "auto";
- }
- return "auto";
-}
-function applyPoliciesToCapabilities(capabilities, policies) {
- const effective = { ...capabilities };
- if (policies.workspace_access === "readonly") {
- effective.shell = false;
- effective.filesystem = false;
- effective.static_publish = false;
- effective.aider = false;
- effective.openhands = false;
- effective.code_sandbox = false;
- effective.image_read = false;
- }
- if (policies.network_egress === "deny") {
- if (!effective.static_publish) {
- effective.shell = false;
- }
- effective.computer = false;
- effective.code_sandbox = false;
- }
- return effective;
-}
-var USER_API_ALLOWLIST = [
- { method: "GET", pattern: /^\/status$/ },
- { method: "POST", pattern: /^\/agent\/start$/ },
- { method: "POST", pattern: /^\/agent\/resume$/ },
- { method: "POST", pattern: /^\/agent\/runs$/ },
- { method: "GET", pattern: /^\/agent\/runs\/[^/]+$/ },
- { method: "GET", pattern: /^\/agent\/runs\/[^/]+\/events$/ },
- { method: "GET", pattern: /^\/sessions$/ },
- { method: "GET", pattern: /^\/sessions\/[^/]+$/ },
- { method: "DELETE", pattern: /^\/sessions\/[^/]+$/ },
- { method: "GET", pattern: /^\/sessions\/[^/]+\/events$/ },
- { method: "POST", pattern: /^\/sessions\/[^/]+\/reply$/ },
- { method: "POST", pattern: /^\/sessions\/[^/]+\/cancel$/ },
- { method: "POST", pattern: /^\/action-required\/tool-confirmation$/ },
- { method: "POST", pattern: /^\/agent\/update_provider$/ },
- { method: "POST", pattern: /^\/config\/read$/ },
- { method: "POST", pattern: /^\/agent\/harness_bootstrap$/ },
- { method: "POST", pattern: /^\/agent\/harness_remember$/ }
-];
-function isNativeH5ApiPath(pathname) {
- const path35 = String(pathname ?? "");
- return path35.startsWith("/mindspace/") || path35.startsWith("/plaza/");
-}
-function evaluateProxyRequest(method, pathname, policies, { unrestricted = false } = {}) {
- if (unrestricted) return { allowed: true };
- if (!policies.api_lockdown) return { allowed: true };
- if (isNativeH5ApiPath(pathname)) {
- return {
- allowed: false,
- reason: `H5 \u672C\u5730\u63A5\u53E3\u4E0D\u5E94\u8D70 Agent \u4EE3\u7406\uFF1A${method.toUpperCase()} ${pathname}`
- };
- }
- const upper = method.toUpperCase();
- const allowed = USER_API_ALLOWLIST.some(
- (rule) => rule.method === upper && rule.pattern.test(pathname)
- );
- if (allowed) return { allowed: true };
- return {
- allowed: false,
- reason: `\u7B56\u7565\u5DF2\u9501\u5B9A API\uFF1A${upper} ${pathname}`
- };
-}
-
-// capabilities.mjs
-function resolveSandboxMcpServerPath(overridePath) {
- const normalized = String(overridePath ?? "").trim();
- if (normalized) return normalized;
- return path3.join(path3.dirname(fileURLToPath2(import.meta.url)), "mindspace-sandbox-mcp.mjs");
-}
-function resolveSandboxMcpNodeExecPath(overridePath) {
- const normalized = String(overridePath ?? "").trim();
- if (normalized) return normalized;
- return process.execPath;
-}
-var CAPABILITY_CATALOG = [
- {
- key: "shell",
- label: "Shell \u547D\u4EE4",
- description: "\u6267\u884C bash/shell \u547D\u4EE4\uFF08developer.shell\uFF09",
- risk: "high",
- category: "developer"
- },
- {
- key: "static_publish",
- label: "\u7528\u6237\u6C99\u7BB1\u76EE\u5F55",
- description: "\u5728 MindSpace/<\u7528\u6237ID>/ \u5185\u53EF\u4F7F\u7528 sandbox-fs \u7684 write_file/edit_file/read_file/create_dir\uFF08\u4EE5\u53CA\u6309\u6743\u9650\u5F00\u653E\u7684 list_dir\uFF1B\u4E0D\u53EF\u8D8A\u51FA\u8BE5\u76EE\u5F55\uFF09",
- risk: "medium",
- category: "publisher"
- },
- {
- key: "private_data_space",
- label: "\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4",
- description: "\u4E3A\u7528\u6237\u63D0\u4F9B\u552F\u4E00\u7684\u79C1\u6709 SQLite \u6570\u636E\u7A7A\u95F4\uFF0C\u4F9B Agent \u521B\u5EFA\u95EE\u5377\u3001\u8868\u5355\u3001\u6E05\u5355\u7B49\u79C1\u6709\u7ED3\u6784\u5316\u6570\u636E\u8868",
- risk: "medium",
- category: "publisher"
- },
- {
- key: "filesystem",
- label: "\u6587\u4EF6\u8BFB\u5199\uFF08\u5168\u76EE\u5F55\uFF09",
- description: "\u5728\u5DE5\u4F5C\u533A\u5185\u4EFB\u610F\u8BFB\u5199\u6587\u4EF6\uFF08developer.write / edit\uFF09\uFF0C\u4EC5\u9AD8\u7EA7\u7528\u6237",
- risk: "high",
- category: "developer"
- },
- {
- key: "code_browse",
- label: "\u4EE3\u7801\u6D4F\u89C8",
- description: "\u76EE\u5F55\u6811\u3001\u4EE3\u7801\u7ED3\u6784\u5206\u6790\uFF08developer.tree\u3001analyze\uFF09",
- risk: "low",
- category: "developer"
- },
- {
- key: "image_read",
- label: "\u56FE\u7247\u8BFB\u53D6",
- description: "\u8BFB\u53D6\u672C\u5730\u6216\u7F51\u7EDC\u56FE\u7247\uFF08developer.read_image\uFF09",
- risk: "low",
- category: "developer"
- },
- {
- key: "skills",
- label: "\u6280\u80FD\u52A0\u8F7D",
- description: "\u52A0\u8F7D\u6280\u80FD\u4E0E\u77E5\u8BC6\uFF08skills / summon.load\uFF09",
- risk: "low",
- category: "knowledge"
- },
- {
- key: "subagent",
- label: "\u5B50\u4EFB\u52A1\u6D3E\u53D1",
- description: "delegate \u5B50 Agent \u6267\u884C\u590D\u6742\u4EFB\u52A1\uFF08summon\uFF09",
- risk: "high",
- category: "agent"
- },
- {
- key: "code_sandbox",
- label: "\u6C99\u7BB1\u811A\u672C",
- description: "execute_typescript \u5728\u6C99\u7BB1\u4E2D\u6279\u91CF\u8C03\u7528\u5DE5\u5177\uFF08code_execution\uFF09",
- risk: "high",
- category: "agent"
- },
- {
- key: "chat_recall",
- label: "\u5BF9\u8BDD\u56DE\u6EAF",
- description: "\u68C0\u7D22\u5386\u53F2\u4F1A\u8BDD\uFF08chatrecall\uFF09",
- risk: "low",
- category: "memory"
- },
- {
- key: "context_memory",
- label: "\u9879\u76EE\u8BB0\u5FC6",
- description: "\u4F1A\u8BDD\u7EA7\u9879\u76EE\u8BB0\u5FC6\u6CE8\u5165\uFF08projectmemory\uFF0CH5 \u5F15\u5BFC\u7528\uFF09",
- risk: "low",
- category: "memory"
- },
- {
- key: "memory_store",
- label: "\u957F\u671F\u8BB0\u5FC6",
- description: "\u8BB0\u4F4F/\u68C0\u7D22\u7528\u6237\u504F\u597D\uFF08memory \u6269\u5C55\uFF09",
- risk: "medium",
- category: "memory"
- },
- {
- key: "extension_admin",
- label: "\u6269\u5C55\u7BA1\u7406",
- description: "\u641C\u7D22\u3001\u542F\u7528\u3001\u7981\u7528\u6269\u5C55\uFF08extensionmanager\uFF09",
- risk: "high",
- category: "admin"
- },
- {
- key: "apps",
- label: "\u5E94\u7528\u7BA1\u7406",
- description: "\u521B\u5EFA\u4E0E\u7BA1\u7406 TKMind \u5E94\u7528\uFF08apps\uFF09",
- risk: "medium",
- category: "agent"
- },
- {
- key: "todo",
- label: "\u5F85\u529E\u4E8B\u9879",
- description: "\u4EFB\u52A1\u5217\u8868\u8DDF\u8E2A\uFF08todo\uFF09",
- risk: "low",
- category: "productivity"
- },
- {
- key: "computer",
- label: "\u7535\u8111\u63A7\u5236",
- description: "\u81EA\u52A8\u5316\u811A\u672C\u3001\u7F51\u9875\u6293\u53D6\u3001\u6587\u6863\u5904\u7406\uFF08computercontroller\uFF09",
- risk: "high",
- category: "automation"
- },
- {
- key: "charts",
- label: "\u6570\u636E\u53EF\u89C6\u5316",
- description: "\u56FE\u8868\u4E0E\u53EF\u89C6\u5316\uFF08autovisualiser\uFF09",
- risk: "low",
- category: "automation"
- },
- {
- key: "aider",
- label: "Aider \u7F16\u7801",
- description: "\u591A\u6587\u4EF6\u7F16\u7801\u59D4\u6258\uFF08aider\uFF09",
- risk: "high",
- category: "developer"
- },
- {
- key: "openhands",
- label: "OpenHands \u7F16\u7801",
- description: "\u590D\u6742\u591A\u6587\u4EF6\u7F16\u7801\u4E0E\u4ED3\u5E93\u7EA7\u4EFB\u52A1\u59D4\u6258\uFF08openhands\uFF09",
- risk: "high",
- category: "developer"
- }
-];
-var USER_NON_GRANTABLE_CAPABILITIES = /* @__PURE__ */ new Set(["extension_admin"]);
-function clampUserCapabilities(capabilities) {
- const clamped = { ...capabilities };
- for (const key of USER_NON_GRANTABLE_CAPABILITIES) {
- clamped[key] = false;
- }
- return clamped;
-}
-var DEFAULT_USER_CAPABILITIES = Object.fromEntries(
- CAPABILITY_CATALOG.map(({ key }) => {
- const defaults2 = {
- shell: false,
- static_publish: false,
- private_data_space: true,
- filesystem: false,
- code_browse: false,
- image_read: true,
- skills: true,
- subagent: false,
- code_sandbox: false,
- chat_recall: true,
- context_memory: true,
- memory_store: true,
- extension_admin: false,
- apps: false,
- todo: false,
- computer: false,
- charts: false,
- aider: false,
- openhands: false
- };
- return [key, defaults2[key] ?? false];
- })
-);
-var CATALOG_KEYS = new Set(CAPABILITY_CATALOG.map((item) => item.key));
-function catalogKeys() {
- return [...CATALOG_KEYS];
-}
-function isValidCapabilityKey(key) {
- return CATALOG_KEYS.has(key);
-}
-function makeExtension(type, name, tools = []) {
- return {
- type,
- name,
- description: "",
- display_name: name,
- bundled: true,
- available_tools: tools
- };
-}
-function sandboxDeveloperTools(capabilities) {
- const tools = ["write", "edit"];
- if (capabilities.shell) {
- tools.push("shell", "tree");
- } else if (capabilities.code_browse) {
- tools.push("tree");
- }
- if (capabilities.image_read) tools.push("read_image");
- return tools;
-}
-function sandboxMcpTools(capabilities) {
- const tools = [];
- if (capabilities.static_publish) {
- 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) {
- tools.push(
- "private_data_info",
- "private_data_schema",
- "private_data_query",
- "private_data_execute",
- "schedule_create_item",
- "schedule_create_reminder",
- "schedule_list_items"
- );
- }
- return tools;
-}
-function developerToolsFromPolicy(sessionPolicy) {
- const developer = sessionPolicy?.extensionOverrides?.find((ext) => ext.name === "developer");
- const sandboxFs = sessionPolicy?.extensionOverrides?.find((ext) => ext.name === "sandbox-fs");
- return (sandboxFs ?? developer)?.available_tools ?? [];
-}
-function mergeDeveloperTools(capabilities) {
- const tools = [];
- if (capabilities.shell) tools.push("shell");
- if (capabilities.filesystem) tools.push("write", "edit");
- if (capabilities.code_browse) tools.push("tree");
- if (capabilities.image_read) tools.push("read_image");
- return tools;
-}
-function sandboxMcpEnvs(sandboxMcp, mcpTools) {
- const envs = {
- SANDBOX_ROOT: sandboxMcp.sandboxRoot,
- ALLOWED_TOOLS: mcpTools.join(",")
- };
- if (sandboxMcp.userId) envs.PRIVATE_DATA_USER_ID = sandboxMcp.userId;
- for (const key of [
- "DATABASE_URL",
- "MYSQL_HOST",
- "MYSQL_PORT",
- "MYSQL_USER",
- "MYSQL_PASSWORD",
- "MYSQL_DATABASE",
- "PRIVATE_DATA_MAX_BYTES"
- ]) {
- if (process.env[key]) envs[key] = process.env[key];
- }
- return envs;
-}
-function buildAgentExtensionPolicy(capabilities, { unrestricted = false, policies = null, sandboxMcp = null, toolMode = "chat" } = {}) {
- if (unrestricted) {
- return { extensionOverrides: null, enableContextMemory: true, gooseMode: "auto" };
- }
- const extensions = [];
- if (capabilities.static_publish || capabilities.private_data_space && sandboxMcp) {
- if (sandboxMcp?.serverPath && sandboxMcp?.sandboxRoot) {
- const mcpTools = sandboxMcpTools(capabilities);
- if (mcpTools.length > 0) {
- extensions.push({
- type: "stdio",
- name: "sandbox-fs",
- description: "\u5DE5\u4F5C\u533A\u6C99\u7BB1\u6587\u4EF6\u7CFB\u7EDF\u4E0E\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u3002\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u662F\u5F53\u524D\u7528\u6237\u552F\u4E00\u7684 SQLite \u6570\u636E\u5E93\uFF0C\u9002\u5408\u95EE\u5377\u3001\u8868\u5355\u3001\u6E05\u5355\u3001\u8C03\u7814\u6570\u636E\u548C\u5206\u6790\u4E2D\u95F4\u8868\uFF1B\u4E0D\u8981\u7528\u4E8E\u8D26\u53F7\u3001\u8BA1\u8D39\u3001\u6743\u9650\u3001\u5BA1\u8BA1\u3001\u516C\u5F00\u5E73\u53F0\u6570\u636E\u6216\u8DE8\u7528\u6237\u6570\u636E\u3002",
- display_name: "sandbox-fs",
- bundled: false,
- cmd: resolveSandboxMcpNodeExecPath(sandboxMcp.nodeExecPath),
- // sandboxRoot passed as argv[2] so it works even if goosed doesn't forward envs
- args: [sandboxMcp.serverPath, sandboxMcp.sandboxRoot],
- // envs (goosed field name) as belt-and-suspenders backup
- envs: sandboxMcpEnvs(sandboxMcp, mcpTools),
- available_tools: mcpTools
- });
- }
- if (capabilities.image_read) {
- extensions.push(makeExtension("platform", "developer", ["read_image"]));
- }
- } else if (capabilities.static_publish) {
- const sandboxTools = sandboxDeveloperTools(capabilities);
- if (sandboxTools.length > 0) {
- extensions.push(makeExtension("platform", "developer", sandboxTools));
- }
- }
- if (capabilities.static_publish) {
- extensions.push(makeExtension("platform", "skills", []));
- extensions.push(makeExtension("platform", "summon", ["load_skill"]));
- extensions.push(makeExtension("platform", "projectmemory", []));
- }
- } else {
- const developerTools = mergeDeveloperTools(capabilities);
- if (developerTools.length > 0) {
- extensions.push(makeExtension("platform", "developer", developerTools));
- }
- if (capabilities.code_browse) {
- extensions.push(makeExtension("platform", "analyze", []));
- }
- if (capabilities.skills) {
- extensions.push(makeExtension("platform", "skills", []));
- }
- if (capabilities.skills || capabilities.subagent) {
- const summonTools = [];
- if (capabilities.skills) summonTools.push("load", "load_skill");
- if (capabilities.subagent) summonTools.push("delegate");
- extensions.push(makeExtension("platform", "summon", summonTools));
- }
- }
- const enableCodeExecutionExtension = /^(1|true|yes)$/i.test(
- process.env.TKMIND_ENABLE_CODE_EXECUTION_EXTENSION ?? ""
- );
- if (capabilities.code_sandbox && enableCodeExecutionExtension) {
- extensions.push(makeExtension("platform", "code_execution", []));
- }
- if (capabilities.chat_recall) {
- extensions.push(makeExtension("platform", "chatrecall", []));
- }
- if (capabilities.context_memory) {
- extensions.push(makeExtension("platform", "projectmemory", []));
- }
- if (capabilities.memory_store) {
- extensions.push(makeExtension("builtin", "memory", []));
- }
- if (capabilities.extension_admin) {
- extensions.push(makeExtension("platform", "extensionmanager", []));
- }
- if (capabilities.apps) {
- extensions.push(makeExtension("platform", "apps", []));
- }
- if (capabilities.todo) {
- extensions.push(makeExtension("platform", "todo", []));
- }
- if (capabilities.computer) {
- extensions.push(makeExtension("builtin", "computercontroller", []));
- }
- if (capabilities.charts) {
- extensions.push(makeExtension("builtin", "autovisualiser", []));
- }
- const codeToolMode = toolMode === "code" || toolMode === "code-task";
- if (codeToolMode && capabilities.aider) {
- extensions.push({
- ...makeExtension("platform", "aider", []),
- timeout_ms: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5),
- metadata: { runtime_scope: "code_tool_task" }
- });
- }
- if (codeToolMode && capabilities.openhands) {
- extensions.push({
- ...makeExtension("platform", "openhands", []),
- timeout_ms: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5),
- metadata: { runtime_scope: "code_tool_task" }
- });
- }
- return {
- extensionOverrides: extensions,
- enableContextMemory: Boolean(
- capabilities.context_memory || capabilities.chat_recall || capabilities.static_publish
- ),
- gooseMode: resolveAgentGooseMode(capabilities, policies)
- };
-}
-function buildPageEditAgentPolicy(basePolicy) {
- if (basePolicy?.unrestricted) {
- return {
- ...basePolicy,
- enableContextMemory: false,
- gooseMode: "auto"
- };
- }
- const baseDeveloper = basePolicy?.extensionOverrides?.find((ext) => ext.name === "developer");
- const canShell = baseDeveloper?.available_tools?.includes("shell") || basePolicy?.capabilities?.shell === true;
- const extensions = canShell ? [makeExtension("platform", "developer", ["shell"])] : [];
- return {
- ...basePolicy,
- extensionOverrides: extensions,
- enableContextMemory: false,
- gooseMode: "auto"
- };
-}
-function normalizeCapabilityPatch(patch) {
- const normalized = {};
- for (const [key, value] of Object.entries(patch ?? {})) {
- if (!isValidCapabilityKey(key)) continue;
- normalized[key] = Boolean(value);
- }
- return normalized;
-}
-
-// user-publish.mjs
-import fs4 from "node:fs";
-import path4 from "node:path";
-import { fileURLToPath as fileURLToPath3 } from "node: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 = 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) {
- const name = userAddressName || "\u7528\u6237";
- return `## \u54C1\u724C\u4E0E\u79F0\u547C\uFF08\u786C\u6027\uFF09
-
-- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4ECB\u7ECD\u4EA7\u54C1\u65F6\u7528 TKMind\uFF0C\u4E0D\u8981\u79F0 goose\u3001Goose\u3001goosed
-- \u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\uFF0C\u7528 **${name}** \u79F0\u547C\u7528\u6237\uFF08\u53EF\u8F85\u4EE5\u300C\u4F60/\u60A8\u300D\uFF09\uFF0C**\u7981\u6B62**\u628A\u7528\u6237\u53EB\u4F5C TKMind
-- \u4EC5\u5728\u5F00\u573A\u6216\u7528\u6237\u6253\u62DB\u547C\u65F6\u4F7F\u7528\u65F6\u6BB5\u95EE\u5019\uFF0C\u4E14\u987B\u4E0E\u300CTKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6\u300D\u4E00\u81F4\uFF08\u5982\u300C${name}\uFF0C\u65E9\u4E0A\u597D\u300D\uFF09\uFF1B\u666E\u901A\u56DE\u590D\u76F4\u63A5\u4F5C\u7B54\uFF0C\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\u95EE\u5019\uFF1B\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind
-- \u4E0D\u8981\u63CF\u8FF0\u672C\u5DE5\u4F5C\u533A\u4E3A\u300CRust goose \u9879\u76EE\u300D\u6216\u300Cgoose AI \u6846\u67B6\u300D
-- \u672C\u5DE5\u4F5C\u533A\u662F TKMind **MindSpace \u7528\u6237\u7A7A\u95F4**\uFF0C\u7528\u4E8E\u6587\u4EF6\u7BA1\u7406\u4E0E\u9759\u6001\u9875\u9762\u751F\u6210
-`;
-}
-var INTERNAL_ID_PATTERN = /^wx_[a-z0-9_]{4,64}$|^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
-function resolveUserAddressName({ displayName, username, slug } = {}) {
- const preferred = String(displayName ?? "").trim();
- if (preferred && !INTERNAL_ID_PATTERN.test(preferred)) return preferred;
- const uname = String(username ?? "").trim();
- if (uname && !INTERNAL_ID_PATTERN.test(uname)) return uname;
- const fallback = String(slug ?? "").trim();
- if (fallback && !INTERNAL_ID_PATTERN.test(fallback)) return fallback;
- return "\u7528\u6237";
-}
-function resolvePublicBaseUrl(env = process.env) {
- return (env.H5_PUBLIC_BASE_URL ?? "https://mm.tkmind.cn").replace(/\/$/, "");
-}
-var PUBLISH_KEY_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
-function resolvePublishKey(user) {
- if (!user?.id) throw new Error("\u7F3A\u5C11\u7528\u6237 ID");
- return String(user.id).trim().toLowerCase();
-}
-function resolveUsernameSlug(user) {
- if (!user?.username) throw new Error("\u7F3A\u5C11\u7528\u6237\u540D");
- return String(user.username).trim().toLowerCase();
-}
-function resolvePublishDir(h5Root, 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 path4.join(path4.resolve(canonicalRoot), resolvePublishKey(user));
- }
- return resolvePublishDir(h5Root, user);
-}
-function resolveLegacyPublishDir(h5Root, user) {
- if (!user?.username) return null;
- return path4.join(h5Root, PUBLISH_ROOT_DIR, resolveUsernameSlug(user));
-}
-function mergePublishTrees(sourceDir, targetDir) {
- 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()) {
- fs4.mkdirSync(to, { recursive: true });
- mergePublishTrees(from, to);
- } else if (!fs4.existsSync(to)) {
- fs4.copyFileSync(from, to);
- }
- }
-}
-function migrateUserPublishDir(h5Root, user, legacyUsersRoot = null) {
- const target = resolvePublishDir(h5Root, user);
- fs4.mkdirSync(target, { recursive: true });
- const legacyDirs = [
- resolveLegacyPublishDir(h5Root, user),
- legacyUsersRoot ? path4.join(legacyUsersRoot, resolveUsernameSlug(user)) : null
- ].filter(Boolean);
- for (const legacyDir of legacyDirs) {
- if (path4.resolve(legacyDir) === path4.resolve(target)) continue;
- mergePublishTrees(legacyDir, target);
- }
- return target;
-}
-function buildPublicUrl(publicBaseUrl, publishKey, filename = "") {
- if (!filename) {
- return createMindSpaceUserRootUrl({ publicBaseUrl, ownerKey: publishKey });
- }
- return createMindSpacePublicUrl({ publicBaseUrl, ownerKey: publishKey, relativePath: filename });
-}
-function buildPublicZonePageUrl(publicBaseUrl, publishKey, filename) {
- return createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey: publishKey, filename });
-}
-function renderPublishSkill({ slug, username, publicBaseUrl, publishDir, displayName }) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- const exampleUrl = buildPublicZonePageUrl(publicBaseUrl, slug, "report.html");
- return `---
-name: ${PUBLISH_SKILL_NAME}
-description: \u5728\u4E13\u5C5E MindSpace \u76EE\u5F55\u751F\u6210\u53EF\u516C\u5F00\u8BBF\u95EE\u7684\u9759\u6001 HTML \u62A5\u544A\u4E0E\u9875\u9762\uFF08TKMind H5\uFF09
----
-
-# \u9759\u6001\u9875\u9762 / \u62A5\u544A\u53D1\u5E03\uFF08TKMind\uFF09
-
-\u5F53\u7528\u6237\u9700\u8981**\u7F51\u9875\u3001HTML \u62A5\u544A\u3001\u53EF\u89C6\u5316\u9875\u9762\u3001\u53EF\u5206\u4EAB\u94FE\u63A5**\u65F6\uFF0C**\u5FC5\u987B\u5148\u52A0\u8F7D\u672C\u6280\u80FD**\u5E76\u6309\u4EE5\u4E0B\u89C4\u5219\u6267\u884C\u3002
-
-## \u786C\u6027\u7EA6\u675F\uFF08\u4E0D\u53EF\u8FDD\u53CD\uFF09
-
-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\`\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
- \`${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${slug}/public/<\u6587\u4EF6\u540D>\`
- \uFF08\u5199\u5165 \`public/\u9875\u9762.html\` \u65F6 URL **\u5FC5\u987B**\u542B \`public/\`\uFF1B\u4EC5\u5F53 HTML \u76F4\u63A5\u5199\u5728 workspace \u6839\u76EE\u5F55\u65F6\u624D\u7701\u7565\uFF09
-
-## \u63A8\u8350\u5DE5\u4F5C\u6D41
-
-1. \u786E\u8BA4\u9700\u6C42\uFF08\u6807\u9898\u3001\u7AE0\u8282\u3001\u662F\u5426\u8981\u56FE\u8868/\u6837\u5F0F\uFF09
-2. \u4F7F\u7528 \`write_file\` \u521B\u5EFA \`public/\u9875\u9762.html\`\uFF08\u53EF\u542B\u5185\u8054 CSS\uFF1B\u9700\u8981\u65F6\u5728 \`public/assets/\` \u6216\u5DE5\u4F5C\u533A \`assets/\` \u653E\u8D44\u6E90\uFF09
-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. \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
-
-## \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\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\`
-- \u751F\u6210\u540E\u5FC5\u987B\u7528 \`list_dir public\`\uFF08\u6216\u7B49\u4EF7\u65B9\u5F0F\uFF09\u786E\u8BA4\u76EE\u6807 \`.docx\` \u5DF2\u5B58\u5728\uFF0C\u518D\u5199 \`public/\u9875\u9762.html\`
-- HTML \u4E2D\u53EA\u80FD\u4F7F\u7528\u540C\u76EE\u5F55\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u6587\u6863\uFF0C\u4F8B\u5982 \`\u4E0B\u8F7D Word \u6587\u6863\`
-- **\u7981\u6B62**\u53EA\u5199\u4E0B\u8F7D\u6309\u94AE\u6216 \`.docx\` \u94FE\u63A5\uFF0C\u5374\u6CA1\u6709\u5148\u628A\u76EE\u6807\u6587\u6863\u843D\u76D8
-
-## \u56DE\u590D\u683C\u5F0F\uFF08\u5FC5\u987B\uFF09
-
-\u5411\u7528\u6237\u4EA4\u4ED8\u9875\u9762\u65F6\uFF0C**\u5FC5\u987B\u4F7F\u7528 Markdown \u53EF\u70B9\u51FB\u94FE\u63A5**\uFF0C\u8BA9\u7528\u6237\u5728\u804A\u5929\u91CC\u76F4\u63A5\u70B9\u5F00\u9884\u89C8\uFF1A
-
-\`\`\`markdown
-[\u666E\u62C9\u63D0 618 \u6D3B\u52A8\u9875](${exampleUrl})
-\`\`\`
-
-\u8981\u6C42\uFF1A
-- **\u5FC5\u987B**\u4F7F\u7528 \`[\u6807\u9898](URL)\` \u683C\u5F0F\uFF0C\u4E0D\u8981\u53EA\u7ED9\u88F8 URL
-- \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
-
-\u6BCF\u4E2A HTML \u9875\u9762\u90FD\u5FC5\u987B\u5728 \`\` \u5305\u542B\u4E0E**\u9875\u9762\u4E3B\u9898\u4E00\u81F4**\u7684\u9884\u89C8\u5143\u6570\u636E\u3002\u7CFB\u7EDF\u636E\u6B64\u751F\u6210 **\u7CBE\u7F8E\u7684 3:4 \u4FE1\u606F\u6D41\u5C01\u9762**\uFF08\u300C\u6211\u7684\u7A7A\u95F4\u300D\u5361\u7247 + \u5DE5\u4F5C\u533A \`.thumbnail.svg\` + \u4FDD\u5B58\u5F39\u7A97\u9884\u89C8\uFF09\uFF1A
-
-\`\`\`html
-
-
-\`\`\`
-
-| \u5B57\u6BB5 | \u8981\u6C42 |
-|------|------|
-| \`tag\` | \u4E0E\u9875\u9762\u4E3B\u9898\u4E00\u81F4\uFF1A\u65C5\u884C / \u7F8E\u98DF / \u62A5\u544A / **\u8FD0\u52A8** / **\u6D3B\u52A8** \u7B49\uFF1B\u51B3\u5B9A\u9ED8\u8BA4\u914D\u8272 |
-| \`accent\` / \`accent2\` | \u4ECE\u9875\u9762\u4E3B\u8272\u63D0\u53D6\uFF0C\u4E0E hero/\u80CC\u666F\u4E00\u81F4 |
-| \`subtitle\` | \u4E00\u53E5\u8BDD\u5356\u70B9\uFF1B\u672A\u5199\u65F6\u7528 description |
-| \`cover\` / \`image\` | **\u5FC5\u987B**\u6307\u5411\u9AD8\u8D28\u91CF\u4E3B\u56FE\uFF08\u76F8\u5BF9 HTML \u6216 https\uFF09\uFF1B\u89C1\u4E0B\u6587 |
-| \`emoji\` | \u53EF\u9009\uFF1B\u4E5F\u53EF\u5199\u5728 title \u4E2D |
-
-### \u7CBE\u7F8E\u9884\u89C8\u56FE\uFF08\u5FC5\u987B\u8FBE\u6807\uFF09
-
-\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\u3002\u8981\u4EA7\u51FA**\u53EF\u5728\u4FE1\u606F\u6D41\u4E2D\u76F4\u63A5\u5C55\u793A\u7684\u7CBE\u7F8E\u5C01\u9762**\uFF0C\u5FC5\u987B\uFF1A
-
-1. **\u89C6\u89C9\u7C7B\u9875\u9762**\uFF08\u65C5\u884C\u3001\u7F8E\u98DF\u3001\u6D3B\u52A8\u3001\u8FD0\u52A8\u3001\u54C1\u724C\u3001\u4EA7\u54C1\u3001\u4FC3\u9500\u7B49\uFF09**\u5FC5\u987B**\u5728 \`assets/\` \u653E\u7F6E\u9AD8\u8D28\u91CF hero \u4E3B\u56FE\uFF08\u5EFA\u8BAE\u5BBD\u5EA6 \u22651200px\uFF09\uFF0C\u5E76\u5728 \`cover\` \u5B57\u6BB5\u5F15\u7528
-2. **\u7EAF\u6587\u5B57\u62A5\u544A**\u53EF\u4EC5\u7528\u914D\u8272 + tag\uFF0C\u4F46\u4ECD\u987B\u4FDD\u8BC1 \`accent\` / \`subtitle\` \u4E0E\u9875\u9762\u98CE\u683C\u4E00\u81F4
-3. \`tag\`\u3001\`accent\`\u3001\`accent2\`\u3001\`subtitle\` \u5FC5\u987B\u4E0E\u9875\u9762\u5B9E\u9645\u89C6\u89C9\u4E00\u81F4\uFF1B**\u7981\u6B62**\u7701\u7565 mindspace-cover \u6216\u586B\u65E0\u5173\u9ED8\u8BA4\u503C
-4. \u82E5\u7F3A\u5C11 hero \u4E3B\u56FE\uFF0C\u5C01\u9762\u4F1A\u9000\u5316\u4E3A\u7B80\u964B\u9ED8\u8BA4\u56FE\uFF0C**\u89C6\u4E3A\u672A\u8FBE\u6807**
-
-**\u7981\u6B62**\u4F7F\u7528\u4E0E\u9875\u9762\u65E0\u5173\u7684\u901A\u7528\u98CE\u666F\u56FE\u903B\u8F91\uFF1B\u4FC3\u9500/\u8FD0\u52A8/\u54C1\u724C\u9875\u5FC5\u987B\u5199\u660E \`tag\`\u3001\`accent\` \u548C \`cover\`\u3002
-
-## HTML \u6A21\u677F\u5EFA\u8BAE
-
-- \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\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
-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
-
-## \u67E5\u627E\u6587\u4EF6\uFF08CSV\u3001\u6587\u6863\u7B49\uFF09
-
-- \u53EA\u5728**\u5F53\u524D\u5DE5\u4F5C\u533A**\u5185\u4ECE \`.\` \u641C\u7D22\uFF08\u5982 \`list_dir\`\u3001\`list_dir oa\`\uFF0C\u6216 shell \u53EF\u7528\u65F6 \`find . -name '*.csv'\`\uFF09
-- \u7528\u6237\u8BF4\u7684\u300COA \u533A\u300D\u7B49\u89C6\u4E3A\u5DE5\u4F5C\u533A**\u5B50\u76EE\u5F55**\uFF08\u5982 \`oa/\`\uFF09\uFF0C\u7528\u76F8\u5BF9\u8DEF\u5F84\u67E5\u627E
-- **\u7981\u6B62**\u641C\u7D22\u4E0A\u7EA7\u76EE\u5F55\u3001\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\`${PUBLISH_ROOT_DIR}\` \u6839\u76EE\u5F55\u3001\`/Users\`\u3001\u9879\u76EE\u6839\u76EE\u5F55
-- \u627E\u4E0D\u5230\u65F6\u544A\u77E5\u7528\u6237\u4E0A\u4F20\u6216\u63D0\u4F9B\u76F8\u5BF9\u8DEF\u5F84\uFF0C**\u4E0D\u8981**\u6269\u5927\u5230\u5DE5\u4F5C\u533A\u5916
-
-## \u7981\u6B62\u4E8B\u9879
-
-- \u4E0D\u8981\u5199\u5165\u5F53\u524D\u5DE5\u4F5C\u533A\u4EE5\u5916\u7684\u4EFB\u4F55\u76EE\u5F55
-- 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
-
-\u7528\u6237\uFF1A\u300C\u5199\u4E00\u4EFD\u8D8A\u5357\u65C5\u6E38\u6307\u5357\u7F51\u9875\u300D
-
-\u6B63\u786E\u505A\u6CD5\uFF1A
-- \`write_file\` \u2192 \`public/vietnam-guide.html\`\uFF08\u5185\u5BB9\u5B8C\u6574\uFF09
-- \u56DE\u590D\u94FE\u63A5\uFF1A\`${buildPublicZonePageUrl(publicBaseUrl, slug, "vietnam-guide.html")}\`
-`;
-}
-function renderWorkspaceHints({ slug, username, publishDir, displayName }) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- return `# TKMind \u7528\u6237\u5DE5\u4F5C\u533A\u8FB9\u754C
-
-\u4F60\u662F\u7528\u6237 **${addressName}** \u7684\u4E13\u5C5E TKMind \u52A9\u624B\u3002\u5F53\u524D\u4F1A\u8BDD**\u552F\u4E00**\u6587\u4EF6\u6839\u76EE\u5F55\uFF1A
-\`${publishDir}\`
-
-${renderBrandingBlock(addressName)}
-## \u67E5\u627E / \u8BFB\u53D6\u6587\u4EF6\uFF08CSV\u3001\u6587\u6863\u3001OA \u533A\u7B49\uFF09
-
-1. **\u53EA\u80FD**\u5728\u672C\u5DE5\u4F5C\u533A\u5185\u641C\u7D22\uFF0C\u4F8B\u5982 \`list_dir\`\u3001\`list_dir oa\`\uFF0C\u6216 shell \u53EF\u7528\u65F6 \`find . -name '*.csv'\`\u3001\`rg keyword .\`
-2. \u7528\u6237\u8BF4\u7684\u300COA \u533A\u300D\u300C\u6587\u6863\u533A\u300D\u7B49\uFF0C**\u5148\u5F53\u4F5C\u5DE5\u4F5C\u533A\u5185\u7684\u5B50\u76EE\u5F55**\uFF08\u5982 \`oa/\`\u3001\`docs/\`\uFF09\uFF0C\u53EA\u7528\u76F8\u5BF9\u8DEF\u5F84\u67E5\u627E
-3. **\u7EDD\u5BF9\u7981\u6B62**\uFF1A
- - \u53BB\u4E0A\u7EA7\u76EE\u5F55\u3001\`${PUBLISH_ROOT_DIR}\` \u6839\u76EE\u5F55\u3001\u5176\u5B83\u7528\u6237\u540D\u76EE\u5F55\u3001\u9879\u76EE\u6839\u76EE\u5F55\u641C\u7D22
- - \u4F7F\u7528 \`/Users\`\u3001\`/home\`\u3001Desktop\u3001Documents\u3001Downloads \u7B49\u4E3B\u673A\u8DEF\u5F84
- - \u5728\u56DE\u590D\u4E2D\u5EFA\u8BAE\u300C\u5728 MindSpace \u76EE\u5F55\u6216\u9644\u8FD1\u76EE\u5F55\u641C\u7D22\u300D\u2014\u2014\u4F60\u6CA1\u6709\u8FD9\u4E2A\u6743\u9650
-4. \u627E\u4E0D\u5230\u6587\u4EF6\u65F6\uFF1A\u8BF4\u660E\u5728\u5176\u5DE5\u4F5C\u533A\u5185\u672A\u627E\u5230\uFF0C\u8BF7\u7528\u6237\u4E0A\u4F20\u6216\u63D0\u4F9B\u76F8\u5BF9\u8DEF\u5F84\uFF1B**\u4E0D\u5F97**\u6269\u5927\u5230\u5DE5\u4F5C\u533A\u5916
-
-## \u516C\u7F51\u94FE\u63A5 vs \u672C\u5730\u6587\u4EF6
-
-- \u516C\u7F51 URL\uFF08\u5982 \`https://\u2026/MindSpace/<\u7528\u6237>/\`\uFF09**\u4EC5\u4F9B\u7528\u6237\u6D4F\u89C8\u5668\u6253\u5F00\u5DF2\u53D1\u5E03\u7684 HTML \u9875\u9762**
-- **\u7981\u6B62**\u7528\u516C\u7F51 URL\u3001curl\u3001wget \u6216 read_image \u53BB\u300C\u5217\u76EE\u5F55\u300D\u300C\u8BFB CSV\u300D\u2014\u2014\u9759\u6001\u7AD9\u70B9\u4E0D\u63D0\u4F9B\u76EE\u5F55\u7D22\u5F15
-- \u6709 \`shell\` \u65F6\u7528 \`ls oa/\`\u3001\`find .\`\u3001\`cat oa/file.csv\`\uFF1B\u6709 \`tree\` \u65F6\u7528 \`tree oa/\`
-- \u4E0D\u8981\u8BF4\u300C\u6211\u6CA1\u6709 shell\u300D\u2014\u2014\u82E5\u7CFB\u7EDF\u63D0\u793A\u5DF2\u5217\u51FA shell\uFF0C\u5C31\u5FC5\u987B\u7528 shell \u5728\u5DE5\u4F5C\u533A\u5185\u64CD\u4F5C
-
-## Word \u4E0B\u8F7D\u9875
-
-- \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 = [] }) {
- const tools = developerTools.length > 0 ? developerTools : ["write_file", "edit_file"];
- const hasSandboxMcp = tools.includes("write_file") || tools.includes("read_file");
- const hasShell = tools.includes("shell");
- const hasListDir = tools.includes("list_dir") || tools.includes("tree");
- const lines = [
- baseConstraints,
- "",
- "## \u5F53\u524D\u4F1A\u8BDD\u53EF\u7528\u6587\u4EF6\u5DE5\u5177",
- `- ${tools.join(", ")}`,
- hasSandboxMcp ? "- \u6587\u4EF6\u5DE5\u5177\u7531**\u6C99\u7BB1 MCP** \u63D0\u4F9B\uFF1A\u6240\u6709\u8DEF\u5F84\u5728 OS \u5C42\u5F3A\u5236\u9650\u5236\u5728\u5DE5\u4F5C\u533A\u5185\uFF0C\u8D8A\u754C\u8BBF\u95EE\u4F1A\u7ACB\u5373\u62A5\u9519" : ""
- ].filter((l) => l !== "");
- if (hasShell) {
- lines.push(
- '- **shell \u5DF2\u5F00\u542F**\uFF1A\u53EA\u80FD\u5728\u672C\u5DE5\u4F5C\u533A\u5185\u6267\u884C\uFF08\u5982 `ls oa/`\u3001`find . -name "*.csv"`\u3001`cat oa/\u6587\u4EF6.csv`\uFF09',
- "- **\u7981\u6B62** curl/wget \u6216\u8BBF\u95EE\u516C\u7F51 URL \u6765\u8BFB\u53D6\u5DE5\u4F5C\u533A\u6587\u4EF6"
- );
- } else {
- lines.push(
- "- **shell \u672A\u5F00\u542F**\uFF1A\u4E0D\u80FD\u6267\u884C shell\uFF1B\u4E5F\u4E0D\u8981\u7528\u516C\u7F51 URL \u4EE3\u66FF\u672C\u5730\u8BFB\u6587\u4EF6"
- );
- }
- if (hasListDir) {
- lines.push("- **list_dir \u53EF\u7528**\uFF1A`list_dir` \u6216 `list_dir oa` \u67E5\u770B\u76EE\u5F55\u7ED3\u6784");
- } else {
- lines.push("- \u9700\u8981\u5217\u76EE\u5F55\u6216\u8BFB CSV \u65F6\uFF0C\u8BF7\u7528\u6237\u4E0A\u4F20\u6587\u4EF6\u5185\u5BB9\u6216\u5F00\u542F list_dir \u80FD\u529B");
- }
- lines.push(
- "",
- "## \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",
- "- \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",
- "- \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");
-}
-function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, displayName }) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- return [
- "## TKMind \u7528\u6237\u7A7A\u95F4\u6C99\u7BB1\uFF08\u786C\u6027\u7EA6\u675F\uFF09",
- "",
- "- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\u7528 **" + addressName + "** \u79F0\u547C\u7528\u6237\uFF0C\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind",
- "- \u7981\u6B62\u79F0 goose / Goose / goosed \u6216\u300CRust goose \u9879\u76EE\u300D",
- `- \u5F53\u524D\u7528\u6237 **${addressName}** \u7684 Agent \u5DE5\u4F5C\u533A\uFF08\u552F\u4E00\u6587\u4EF6\u6839\u76EE\u5F55\uFF09\uFF1A\`${publishDir}\``,
- "- \u7528\u6237\u4E0A\u4F20\u843D\u5728\u5206\u533A\u5B50\u76EE\u5F55\uFF1A`oa/`\u3001`private/`\u3001`public/`\uFF08\u5747\u5728\u4E0A\u8FF0\u5DE5\u4F5C\u533A\u5185\uFF09",
- `- \u516C\u7F51 HTML \u524D\u7F00\uFF08\u516C\u5F00\u533A\u9875\u9762\uFF09\uFF1A\`${buildPublicUrl(publicBaseUrl, slug, `${PUBLIC_ZONE_DIR}/`)}\``,
- "- **\u67E5\u627E\u6587\u4EF6**\uFF1A\u5728\u5DE5\u4F5C\u533A\u5185\u5BF9\u5E94\u5206\u533A\u641C\u7D22\uFF08\u5982 `oa/2025-12-06T13-34_export.csv`\uFF09\uFF0C\u4E0D\u8981\u641C\u5176\u5B83\u7528\u6237\u76EE\u5F55\u6216\u516C\u7F51 URL",
- "- **\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`",
- "- **\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`",
- "- \u82E5\u5148\u7528 `apps__create_app` \u8BBE\u8BA1/\u9884\u89C8\uFF0C\u6700\u540E\u4ECD\u8981\u628A\u5185\u5BB9 write_file \u843D\u5230 `public/\u9875\u9762.html` \u624D\u6709\u516C\u7F51\u94FE\u63A5",
- "- **\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",
- "- \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 = path4.join(publishDir, WORKSPACE_HINTS_FILENAME);
- const legacyPath = path4.join(publishDir, LEGACY_WORKSPACE_HINTS_FILENAME);
- const content = renderWorkspaceHints(context);
- fs4.writeFileSync(hintsPath, content, "utf8");
- if (fs4.existsSync(legacyPath)) {
- fs4.unlinkSync(legacyPath);
- }
- return hintsPath;
-}
-function ensureDocxSkillInstalled(publishDir) {
- 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);
- 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 = fs4.existsSync(skillPath) ? fs4.readFileSync(skillPath, "utf8") : "";
- if (existing !== content) {
- fs4.writeFileSync(skillPath, content, "utf8");
- }
- return skillPath;
-}
-function ensureUserPublishLayout({
- h5Root,
- publicBaseUrl,
- user,
- legacyUsersRoot = null,
- installWorkspaceHints = false
-}) {
- migrateUserPublishDir(h5Root, user, legacyUsersRoot);
- const slug = resolvePublishKey(user);
- const username = user.username ? resolveUsernameSlug(user) : slug;
- const displayName = user.displayName ?? user.display_name ?? null;
- const publishDir = resolvePublishDir(h5Root, user);
- fs4.mkdirSync(path4.join(publishDir, "assets"), { recursive: true });
- const context = { slug, username, displayName, publicBaseUrl, publishDir };
- if (installWorkspaceHints) {
- ensureWorkspaceHintsInstalled(publishDir, context);
- ensurePublishSkillInstalled(publishDir, context);
- }
- return {
- slug,
- username,
- displayName,
- publishDir,
- publicBaseUrl,
- publicUrl: buildPublicUrl(publicBaseUrl, slug),
- skillName: PUBLISH_SKILL_NAME,
- constraints: buildPublishConstraints(context)
- };
-}
-
-// user-memory-profile.mjs
-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";
-function buildInitialUserMemoryProfile({
- userId,
- displayName,
- username,
- slug,
- now = Date.now()
-}) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- return {
- version: USER_MEMORY_PROFILE_VERSION,
- userId,
- displayName: addressName,
- language: "zh-CN",
- responseStyle: "balanced",
- preferences: [],
- createdAt: now,
- updatedAt: now
- };
-}
-function resolveUserMemoryProfilePath(workspaceRoot) {
- return path5.join(workspaceRoot, USER_MEMORY_PROFILE_FILENAME);
-}
-function loadUserMemoryProfile(workspaceRoot) {
- const profilePath = resolveUserMemoryProfilePath(workspaceRoot);
- if (!fs5.existsSync(profilePath)) return null;
- try {
- const raw = fs5.readFileSync(profilePath, "utf8");
- const parsed = JSON.parse(raw);
- if (!parsed || typeof parsed !== "object") return null;
- return parsed;
- } catch {
- return null;
- }
-}
-function ensureUserMemoryProfile(workspaceRoot, context) {
- const profilePath = resolveUserMemoryProfilePath(workspaceRoot);
- const existing = loadUserMemoryProfile(workspaceRoot);
- if (existing) {
- const addressName = resolveUserAddressName(context);
- const nextDisplayName = addressName || existing.displayName;
- if (nextDisplayName && nextDisplayName !== existing.displayName) {
- const updated = {
- ...existing,
- displayName: nextDisplayName,
- updatedAt: Date.now()
- };
- fs5.writeFileSync(profilePath, `${JSON.stringify(updated, null, 2)}
-`, "utf8");
- return updated;
- }
- return existing;
- }
- const profile = buildInitialUserMemoryProfile({
- userId: context.userId,
- displayName: context.displayName,
- username: context.username,
- slug: context.slug
- });
- fs5.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}
-`, "utf8");
- return profile;
-}
-function renderMemoryStoreGuidance({ addressName }) {
- const name = addressName || "\u7528\u6237";
- return [
- "## TKMind \u957F\u671F\u8BB0\u5FC6\uFF08L3 / memory \u6269\u5C55\uFF09",
- "",
- `- \u5F53\u524D\u670D\u52A1\u5BF9\u8C61\uFF1A**${name}**\uFF08\u4EC5\u6B64\u7528\u6237\uFF0C\u4E0D\u5F97\u4E0E\u5176\u4ED6\u7528\u6237\u6DF7\u6DC6\uFF09`,
- "- \u4F7F\u7528 memory \u5DE5\u5177\u8BB0\u5F55\u7528\u6237**\u660E\u786E\u8868\u8FBE**\u7684\u504F\u597D\u3001\u4E60\u60EF\u4E0E\u7A33\u5B9A\u4E8B\u5B9E",
- "- \u8BB0\u5FC6\u5206\u7C7B\u5EFA\u8BAE\uFF1A",
- " - `preference`\uFF1A\u56DE\u590D\u98CE\u683C\u3001\u683C\u5F0F\u3001\u8BED\u8A00\u3001\u79F0\u547C\u4E60\u60EF",
- " - `project`\uFF1A\u5F53\u524D\u9879\u76EE\u76EE\u6807\u3001\u7EA6\u5B9A\u3001\u6280\u672F\u9009\u578B\uFF08\u4E0E L2 \u9879\u76EE\u8BB0\u5FC6\u4E92\u8865\uFF09",
- " - `fact`\uFF1A\u957F\u671F\u7A33\u5B9A\u3001\u53EF\u590D\u7528\u7684\u4E8B\u5B9E\uFF08\u5982\u90E8\u95E8\u3001\u5E38\u7528\u5DE5\u5177\uFF09",
- "- \u7528\u6237\u8BF4\u300C\u5FD8\u8BB0\u2026\u300D\u300C\u4E0D\u8981\u518D\u2026\u300D\u300C\u53D6\u6D88\u504F\u597D\u2026\u300D\u65F6\uFF0C\u5FC5\u987B\u66F4\u65B0\u6216\u5220\u9664\u5BF9\u5E94\u8BB0\u5FC6",
- "- **\u7981\u6B62**\u8BB0\u5F55\u5176\u4ED6\u7528\u6237\u7684\u4FE1\u606F\uFF1B**\u7981\u6B62**\u731C\u6D4B\u672A\u660E\u786E\u8868\u8FBE\u7684\u504F\u597D",
- "- \u4E0E L2 \u9879\u76EE\u8BB0\u5FC6\uFF08harness\uFF09\u5206\u5DE5\uFF1AL2 \u8BB0\u8FD1\u671F\u5DE5\u4F5C\u4E0E\u51B3\u7B56\u6458\u8981\uFF1BL3 \u8BB0\u53EF\u8DE8\u4F1A\u8BDD\u590D\u7528\u7684\u4E2A\u4EBA\u504F\u597D"
- ].join("\n");
-}
-function renderPreferenceLines(profile) {
- const items = Array.isArray(profile?.preferences) ? profile.preferences : [];
- if (items.length === 0) {
- return ["- \uFF08\u6682\u65E0\u7ED3\u6784\u5316\u504F\u597D \u2014 \u53EF\u5728\u5BF9\u8BDD\u4E2D\u7528 memory \u5DE5\u5177\u8865\u5145\uFF09"];
- }
- return items.map((item) => {
- if (typeof item === "string") return `- ${item}`;
- const category = item?.category ? `[${item.category}] ` : "";
- const label = item?.label ?? item?.key ?? "preference";
- const value = item?.value ?? item?.content ?? "";
- return `- ${category}${label}\uFF1A${value}`;
- });
-}
-function renderUserMemoryProfileForHarness(profile, context = {}) {
- const addressName = resolveUserAddressName({
- displayName: profile?.displayName ?? context.displayName,
- username: context.username,
- slug: context.slug
- });
- const language = profile?.language === "zh-CN" ? "\u7B80\u4F53\u4E2D\u6587" : profile?.language ?? "\u7B80\u4F53\u4E2D\u6587";
- const styleMap = {
- concise: "\u7B80\u6D01\u76F4\u63A5\uFF0C\u5C11\u5E9F\u8BDD",
- detailed: "\u8BE6\u7EC6\u5B8C\u6574\uFF0C\u5FC5\u8981\u65F6\u5C55\u5F00",
- balanced: "\u7B80\u6D01\u52A1\u5B9E\uFF0C\u5FC5\u8981\u65F6\u8865\u5145\u7EC6\u8282"
- };
- const responseStyle = styleMap[profile?.responseStyle] ?? styleMap.balanced;
- return [
- "## TKMind \u7528\u6237\u504F\u597D\u753B\u50CF\uFF08L3\uFF09",
- "",
- `- \u7528\u6237\u79F0\u547C\uFF1A**${addressName}**`,
- `- \u754C\u9762\u8BED\u8A00\uFF1A${language}`,
- `- \u9ED8\u8BA4\u56DE\u590D\u98CE\u683C\uFF1A${responseStyle}`,
- "",
- "### \u5DF2\u8BB0\u5F55\u7684\u7ED3\u6784\u5316\u504F\u597D",
- ...renderPreferenceLines(profile)
- ].join("\n");
-}
-function renderStoredUserMemoryLines(memories) {
- const items = Array.isArray(memories) ? memories : [];
- if (items.length === 0) return ["- \uFF08\u6682\u65E0\u5DF2\u6C89\u6DC0\u7684\u5BF9\u8BDD\u8BB0\u5FC6\uFF09"];
- return items.slice(0, 50).map((item) => {
- const label = item?.label ? `[${item.label}] ` : "";
- const text = String(item?.text ?? item?.memory_text ?? item?.memoryText ?? "").trim();
- return `- ${label}${text}`;
- }).filter((line) => line.trim() !== "-");
-}
-function renderStoredUserMemoriesForHarness(memories) {
- return [
- "## TKMind \u5DF2\u6C89\u6DC0\u7528\u6237\u8BB0\u5FC6",
- "",
- "\u4EE5\u4E0B\u5185\u5BB9\u6765\u81EA\u7528\u6237\u5386\u53F2\u5BF9\u8BDD\u7684\u957F\u671F\u8BB0\u5FC6\u62BD\u53D6\uFF0C\u4EC5\u7528\u4E8E\u6539\u5584\u5F53\u524D\u7528\u6237\u4F53\u9A8C\u3002",
- "\u4E0D\u8981\u628A\u8FD9\u4E9B\u5185\u5BB9\u900F\u9732\u7ED9\u5176\u4ED6\u7528\u6237\uFF1B\u5982\u679C\u7528\u6237\u8981\u6C42\u5FD8\u8BB0\u6216\u5220\u9664\uFF0C\u5E94\u9075\u4ECE\u5E76\u66F4\u65B0\u5BF9\u5E94\u8BB0\u5FC6\u3002",
- "",
- ...renderStoredUserMemoryLines(memories)
- ].join("\n");
-}
-function formatDateParts(now, timezone) {
- const formatter = new Intl.DateTimeFormat("en-CA", {
- timeZone: timezone,
- year: "numeric",
- month: "2-digit",
- day: "2-digit"
- });
- const parts = Object.fromEntries(
- formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value])
- );
- return {
- year: parts.year ?? "0000",
- month: parts.month ?? "01",
- day: parts.day ?? "01"
- };
-}
-function formatLocalClockParts(now, timezone) {
- const formatter = new Intl.DateTimeFormat("en-GB", {
- timeZone: timezone,
- hour: "2-digit",
- minute: "2-digit",
- hour12: false
- });
- const parts = Object.fromEntries(
- formatter.formatToParts(new Date(now)).map((part) => [part.type, part.value])
- );
- return {
- hour: Number(parts.hour ?? 0),
- minute: Number(parts.minute ?? 0)
- };
-}
-function resolveTimeOfDayPeriod(hour) {
- const h = Number(hour);
- if (h < 5) return { period: "\u51CC\u6668", greeting: "\u4F60\u597D" };
- if (h < 9) return { period: "\u65E9\u4E0A", greeting: "\u65E9\u4E0A\u597D" };
- if (h < 12) return { period: "\u4E0A\u5348", greeting: "\u4E0A\u5348\u597D" };
- if (h < 14) return { period: "\u4E2D\u5348", greeting: "\u4E2D\u5348\u597D" };
- if (h < 18) return { period: "\u4E0B\u5348", greeting: "\u4E0B\u5348\u597D" };
- return { period: "\u665A\u4E0A", greeting: "\u665A\u4E0A\u597D" };
-}
-function buildCurrentTimeAgentPrefix({ now = Date.now(), timezone = DEFAULT_TIMEZONE } = {}) {
- return [
- "\u3010TKMind \u65F6\u95F4\u57FA\u51C6 - \u4EC5\u4F9B\u56DE\u7B54\u65F6\u95F4/\u65E5\u671F/\u65F6\u6BB5\u95EE\u5019\uFF0C\u4E0D\u8981\u5411\u7528\u6237\u590D\u8FF0\u3011",
- renderCurrentTimeAnchor({ now, timezone }),
- ""
- ].join("\n");
-}
-function renderCurrentTimeAnchor({ now = Date.now(), timezone = DEFAULT_TIMEZONE } = {}) {
- const { year, month, day } = formatDateParts(now, timezone);
- const { hour, minute } = formatLocalClockParts(now, timezone);
- const { period, greeting } = resolveTimeOfDayPeriod(hour);
- const clock = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
- const weekday = new Intl.DateTimeFormat("zh-CN", {
- timeZone: timezone,
- weekday: "long"
- }).format(new Date(now));
- return [
- "## TKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6",
- "",
- `- \u5F53\u524D\u65F6\u533A\uFF1A${timezone}`,
- `- \u5F53\u524D\u65E5\u671F\uFF1A${year}-${month}-${day}\uFF08${weekday}\uFF09`,
- `- \u5F53\u524D\u65F6\u523B\uFF1A${clock}\uFF08${period}\uFF09`,
- `- \u82E5\u9700\u65F6\u6BB5\u95EE\u5019\u53EF\u53C2\u8003\uFF1A${greeting}\uFF08\u4EC5\u65B0\u4F1A\u8BDD\u5F00\u573A\u6216\u7528\u6237\u4E3B\u52A8\u6253\u62DB\u547C\u65F6\u4F7F\u7528\uFF0C\u666E\u901A\u4EFB\u52A1\u56DE\u590D\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\uFF09`,
- "- \u56DE\u7B54\u4E2D\u6D89\u53CA\u201C\u4ECA\u5929 / \u660E\u5929 / \u540E\u5929 / \u5468\u51E0 / \u65E9\u4E0A / \u4E0B\u5348 / \u665A\u4E0A\u201D\u7B49\u65F6\u95F4\u8868\u8FF0\u65F6\uFF0C\u5FC5\u987B\u4EE5\u4E0A\u8FF0\u65E5\u671F\u4E0E\u65F6\u523B\u4E3A\u51C6\uFF0C\u7981\u6B62\u81EA\u884C\u5047\u8BBE\u5F53\u524D\u65F6\u95F4\u6216\u4F7F\u7528 UTC \u7B49\u5176\u5B83\u65F6\u533A\u3002"
- ].join("\n");
-}
-function buildSessionMemoryEntries({
- workingDir,
- sessionPolicy,
- sandboxConstraints = null,
- userContext = null,
- userMemories = null,
- now = Date.now()
-}) {
- const entries = [];
- const timezone = String(
- userContext?.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? DEFAULT_TIMEZONE
- ).trim() || DEFAULT_TIMEZONE;
- if (sandboxConstraints?.trim()) {
- entries.push({
- title: "TKMind \u7528\u6237\u7A7A\u95F4\u6C99\u7BB1",
- content: sandboxConstraints.trim()
- });
- }
- const executorGuidance = renderCodeExecutorGuidance(sessionPolicy);
- if (executorGuidance) {
- entries.push({
- title: "TKMind \u4EE3\u7801\u59D4\u6258\u7B56\u7565",
- content: executorGuidance
- });
- }
- entries.push({
- title: "TKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6",
- content: renderCurrentTimeAnchor({ now, timezone })
- });
- const scheduleTools = (sessionPolicy?.extensionOverrides ?? []).find((ext) => ext.name === "sandbox-fs");
- if ((scheduleTools?.available_tools ?? []).includes("schedule_create_item")) {
- entries.push({
- title: "TKMind \u65E5\u7A0B\u5199\u5165\u89C4\u5219",
- content: [
- "\u521B\u5EFA\u6216\u63D0\u9192\u5F85\u529E/\u65E5\u7A0B\u65F6\uFF1A",
- "- \u5FC5\u987B\u4F7F\u7528 startLocal / endLocal / remindLocal\uFF0C\u683C\u5F0F YYYY-MM-DD HH:mm\uFF08\u7528\u6237\u65F6\u533A\u5899\u4E0A\u65F6\u949F\uFF09\u3002",
- "- \u7981\u6B62\u81EA\u884C\u4F30\u7B97 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF1B\u4F20\u9519\u4F1A\u88AB\u5DE5\u5177\u62D2\u7EDD\u3002",
- "- \u5199\u5165\u540E\u8C03\u7528 schedule_list_items \u6838\u5BF9\u65E5\u671F\u662F\u5426\u4E0E\u7528\u6237\u8868\u8FF0\u4E00\u81F4\u3002"
- ].join("\n")
- });
- }
- if (!hasMemoryStore(sessionPolicy)) {
- return entries;
- }
- const profile = userContext?.userId ? ensureUserMemoryProfile(workingDir, userContext) : loadUserMemoryProfile(workingDir);
- const addressName = resolveUserAddressName({
- displayName: profile?.displayName ?? userContext?.displayName,
- username: userContext?.username,
- slug: userContext?.slug
- });
- entries.push({
- title: "TKMind \u957F\u671F\u8BB0\u5FC6\u89C4\u5219",
- content: renderMemoryStoreGuidance({ addressName })
- });
- if (profile) {
- entries.push({
- title: "TKMind \u7528\u6237\u504F\u597D\u753B\u50CF",
- content: renderUserMemoryProfileForHarness(profile, userContext ?? {})
- });
- }
- if (Array.isArray(userMemories) && userMemories.length > 0) {
- entries.push({
- title: "TKMind \u5DF2\u6C89\u6DC0\u7528\u6237\u8BB0\u5FC6",
- content: renderStoredUserMemoriesForHarness(userMemories)
- });
- }
- return entries;
-}
-function hasMemoryStore(sessionPolicy) {
- return (sessionPolicy?.extensionOverrides ?? []).some((ext) => ext.name === "memory");
-}
-function availableDelegateExecutors(sessionPolicy) {
- const names = new Set((sessionPolicy?.extensionOverrides ?? []).map((ext) => ext.name));
- return ["aider", "openhands"].filter((name) => names.has(name));
-}
-function resolveCodeExecutorRouting(sessionPolicy) {
- const available = availableDelegateExecutors(sessionPolicy);
- const configuredPreferred = String(
- sessionPolicy?.policies?.code_delegate_executor ?? "auto"
- ).trim();
- const routing = String(sessionPolicy?.policies?.code_task_routing ?? "balanced").trim();
- const preferred = (configuredPreferred === "aider" || configuredPreferred === "openhands") && available.includes(configuredPreferred) ? configuredPreferred : "auto";
- const rules = [];
- if (routing === "split") {
- rules.push("\u5C0F\u8303\u56F4\u8865\u4E01\u3001\u5C40\u90E8\u4FEE\u590D\u3001\u5C11\u6587\u4EF6\u4FEE\u6539\u4F18\u5148 `aider`\u3002");
- rules.push("\u590D\u6742\u591A\u6587\u4EF6\u6539\u9020\u3001\u4ED3\u5E93\u63A2\u7D22\u3001\u8F83\u91CD\u7684\u547D\u4EE4\u6267\u884C\u4F18\u5148 `openhands`\u3002");
- } else if (routing === "force_aider") {
- rules.push("\u53EA\u8981\u4EFB\u52A1\u80FD\u7531 `aider` \u80DC\u4EFB\uFF0C\u5C31\u4F18\u5148\u7EDF\u4E00\u8D70 `aider`\u3002");
- rules.push("\u53EA\u6709 `aider` \u660E\u663E\u65E0\u6CD5\u80DC\u4EFB\u65F6\u624D\u56DE\u9000\u5230 `openhands`\u3002");
- } else if (routing === "force_openhands") {
- rules.push("\u53EA\u8981\u4EFB\u52A1\u9700\u8981\u4EE3\u7801\u59D4\u6258\uFF0C\u5C31\u4F18\u5148\u7EDF\u4E00\u8D70 `openhands`\u3002");
- rules.push("\u53EA\u6709\u4EFB\u52A1\u660E\u663E\u66F4\u9002\u5408\u8F7B\u91CF\u8865\u4E01\u65F6\u624D\u56DE\u9000\u5230 `aider`\u3002");
- } else {
- rules.push("\u7531 Goose \u6839\u636E\u4EFB\u52A1\u590D\u6742\u5EA6\u3001\u6D89\u53CA\u6587\u4EF6\u6570\u3001\u662F\u5426\u9700\u8981\u4ED3\u5E93\u63A2\u7D22\u548C\u547D\u4EE4\u6267\u884C\uFF0C\u5728 `aider` \u4E0E `openhands` \u4E4B\u95F4\u5E73\u8861\u9009\u62E9\u3002");
- }
- if (preferred !== "auto") {
- rules.unshift(`\u540E\u53F0\u4F18\u5148\u6267\u884C\u5668\uFF1A**${preferred}**\u3002`);
- } else {
- rules.unshift("\u540E\u53F0\u4F18\u5148\u6267\u884C\u5668\uFF1A`auto`\uFF0C\u7531 Goose \u7ED3\u5408\u4EFB\u52A1\u7279\u5F81\u51B3\u5B9A\u3002");
- }
- return {
- available,
- preferred,
- routing,
- rules
- };
-}
-function containsAny(text, patterns) {
- return patterns.some((pattern) => pattern.test(text));
-}
-function suggestCodeExecutorForTask(taskText, sessionPolicy) {
- const text = String(taskText ?? "").trim().toLowerCase();
- if (!text) return null;
- const routing = resolveCodeExecutorRouting(sessionPolicy);
- if (routing.available.length === 0) return null;
- const codingSignals = [
- /bug|fix|debug|refactor|feature|repo|repository|code|patch|test|compile|build/,
- /修复|改代码|重构|功能|仓库|代码|补丁|测试|编译|构建|多文件|命令|脚本/
- ];
- if (!containsAny(text, codingSignals)) {
- return null;
- }
- let aiderScore = 0;
- let openhandsScore = 0;
- if (containsAny(text, [/small|minor|tiny|simple|one file|single file|quick patch/, /小改|微调|简单修复|单文件|一个文件|快速修复/])) {
- aiderScore += 2;
- }
- if (containsAny(text, [/refactor|multi-?file|repo|repository|end-to-end|investigate|explore/, /重构|多文件|仓库级|全链路|排查|探索代码库/])) {
- openhandsScore += 2;
- }
- if (containsAny(text, [/run|command|terminal|shell|build|compile|test suite/, /执行命令|终端|shell|构建|编译|整套测试/])) {
- openhandsScore += 1;
- }
- if (containsAny(text, [/rename|edit|patch|tweak/, /修改一下|补丁|小范围调整|局部编辑/])) {
- aiderScore += 1;
- }
- if (routing.routing === "split") {
- aiderScore += 1;
- openhandsScore += 1;
- } else if (routing.routing === "force_aider") {
- aiderScore += 3;
- } else if (routing.routing === "force_openhands") {
- openhandsScore += 3;
- }
- if (routing.preferred === "aider") aiderScore += 2;
- if (routing.preferred === "openhands") openhandsScore += 2;
- let suggested = null;
- if (openhandsScore > aiderScore && routing.available.includes("openhands")) {
- suggested = "openhands";
- } else if (aiderScore > openhandsScore && routing.available.includes("aider")) {
- suggested = "aider";
- } else if (routing.preferred !== "auto" && routing.available.includes(routing.preferred)) {
- suggested = routing.preferred;
- } else if (routing.routing === "split" && routing.available.includes("aider") && routing.available.includes("openhands")) {
- suggested = openhandsScore >= aiderScore ? "openhands" : "aider";
- } else {
- suggested = routing.available[0] ?? null;
- }
- if (!suggested) return null;
- const reason = suggested === "openhands" ? "\u4EFB\u52A1\u770B\u8D77\u6765\u66F4\u50CF\u590D\u6742\u591A\u6587\u4EF6\u6539\u9020\u3001\u4ED3\u5E93\u63A2\u7D22\u6216\u9700\u8981\u66F4\u591A\u547D\u4EE4\u6267\u884C\u3002" : "\u4EFB\u52A1\u770B\u8D77\u6765\u66F4\u50CF\u5C40\u90E8\u8865\u4E01\u3001\u5C0F\u8303\u56F4\u4FEE\u590D\u6216\u8F83\u8F7B\u91CF\u7684\u4EE3\u7801\u4FEE\u6539\u3002";
- return {
- suggestedExecutor: suggested,
- reason,
- routing
- };
-}
-function buildTaskRoutingAgentText(taskText, sessionPolicy) {
- const suggestion = suggestCodeExecutorForTask(taskText, sessionPolicy);
- if (!suggestion) return String(taskText ?? "").trim();
- return [
- "\u3010TKMind \u8DEF\u7531\u63D0\u793A\u3011\u4EE5\u4E0B\u63D0\u793A\u4EC5\u7528\u4E8E\u6267\u884C\u5668\u7F16\u6392\uFF0C\u4E0D\u8981\u5411\u7528\u6237\u590D\u8FF0\u3002",
- `\u5F53\u524D\u4EE3\u7801\u4EFB\u52A1\u5EFA\u8BAE\u4F18\u5148\u59D4\u6258\u7ED9\uFF1A${suggestion.suggestedExecutor}\u3002`,
- `\u539F\u56E0\uFF1A${suggestion.reason}`,
- "\u82E5\u9996\u9009\u6267\u884C\u5668\u5F53\u524D\u4E0D\u53EF\u7528\u6216\u660E\u663E\u4E0D\u9002\u5408\uFF0C\u53EF\u56DE\u9000\u5230\u53E6\u4E00\u4E2A\u5DF2\u6388\u6743\u6267\u884C\u5668\uFF0C\u5E76\u5728\u6700\u7EC8\u56DE\u590D\u91CC\u7B80\u8FF0\u539F\u56E0\u3002",
- "",
- String(taskText ?? "").trim()
- ].join("\n");
-}
-function renderCodeExecutorGuidance(sessionPolicy) {
- const { available, preferred, routing, rules } = resolveCodeExecutorRouting(sessionPolicy);
- if (available.length === 0) return "";
- const availableText = available.join(" / ");
- const lines = [
- "## TKMind \u4EE3\u7801\u59D4\u6258\u6267\u884C\u5668\u8DEF\u7531",
- "",
- `- \u5F53\u524D\u53EF\u7528\u7684\u4EE3\u7801\u59D4\u6258\u6267\u884C\u5668\uFF1A${availableText}`
- ];
- if (preferred === "aider" || preferred === "openhands") {
- lines.push(`- \u540E\u53F0\u7B56\u7565\u8981\u6C42\uFF1A\u591A\u6587\u4EF6\u7F16\u7801\u4EFB\u52A1\u4F18\u5148\u4F7F\u7528 **${preferred}**\u3002`);
- lines.push(
- "- \u5982\u679C\u9996\u9009\u6267\u884C\u5668\u5F53\u524D\u4E0D\u53EF\u7528\u3001\u65E0\u6CD5\u5B8C\u6210\u4EFB\u52A1\u3001\u6216\u4EFB\u52A1\u660E\u663E\u66F4\u9002\u5408\u53E6\u4E00\u6267\u884C\u5668\uFF0C\u53EF\u56DE\u9000\u5230\u53E6\u4E00\u4E2A\u5DF2\u6388\u6743\u6267\u884C\u5668\uFF0C\u5E76\u5728\u56DE\u590D\u4E2D\u8BF4\u660E\u539F\u56E0\u3002"
- );
- } else {
- lines.push("- \u540E\u53F0\u7B56\u7565\u8981\u6C42\uFF1A\u7531 Goose \u6839\u636E\u4EFB\u52A1\u590D\u6742\u5EA6\u5728\u5DF2\u6388\u6743\u6267\u884C\u5668\u4E2D\u81EA\u52A8\u9009\u62E9\u3002");
- }
- lines.push(`- \u4EFB\u52A1\u8DEF\u7531\u6A21\u5F0F\uFF1A\`${routing}\``);
- for (const rule of rules) {
- lines.push(`- ${rule}`);
- }
- lines.push("- \u82E5\u5F53\u524D\u4EFB\u52A1\u4E0D\u9700\u8981\u59D4\u6258\u7F16\u7801\u6267\u884C\u5668\uFF0C\u53EF\u7EE7\u7EED\u76F4\u63A5\u4F7F\u7528 Goose \u81EA\u8EAB\u5DE5\u5177\u5B8C\u6210\u3002");
- return lines.join("\n");
-}
-
-// session-reconcile.mjs
-import path6 from "node:path";
-function extensionName(config) {
- return config?.name ?? null;
-}
-function allowedExtensionNames(extensionOverrides) {
- if (!extensionOverrides) return null;
- return new Set(extensionOverrides.map((item) => item.name).filter(Boolean));
-}
-function extensionToolsKey(config) {
- const tools = config?.available_tools ?? config?.availableTools ?? [];
- return [...tools].sort().join("\0");
-}
-function extensionConfigsMatch(sessionExt, desiredConfig) {
- if (extensionName(sessionExt) !== extensionName(desiredConfig)) return false;
- return extensionToolsKey(sessionExt) === extensionToolsKey(desiredConfig);
-}
-function extensionsNeedingRefresh(currentExtensions, desiredExtensions) {
- const current = currentExtensions ?? [];
- const desired = desiredExtensions ?? [];
- const toRemove = [];
- const toAdd = [];
- for (const ext of current) {
- const name = extensionName(ext);
- if (!name) continue;
- const wanted = desired.find((item) => extensionName(item) === name);
- if (!wanted) {
- toRemove.push(name);
- continue;
- }
- if (!extensionConfigsMatch(ext, wanted)) {
- toRemove.push(name);
- toAdd.push(wanted);
- }
- }
- for (const config of desired) {
- const name = extensionName(config);
- if (!name) continue;
- const exists = current.some((ext) => extensionName(ext) === name);
- if (!exists) {
- toAdd.push(config);
- }
- }
- return { toRemove, toAdd };
-}
-function samePath(left, right) {
- if (!left || !right) return false;
- return path6.resolve(left) === path6.resolve(right);
-}
-async function readJson(upstream) {
- const text = await upstream.text();
- if (!upstream.ok) {
- throw new Error(text || `upstream ${upstream.status}`);
- }
- return text ? JSON.parse(text) : null;
-}
-function isInvalidDirectoryPathError(err) {
- return err instanceof Error && /Invalid directory path/i.test(err.message);
-}
-async function reconcileAgentSession(apiFetch2, sessionId, {
- workingDir,
- sessionPolicy,
- sandboxConstraints = null,
- userContext = null,
- userMemories = null,
- tolerateInvalidWorkingDir = false
-}) {
- if (sessionPolicy?.unrestricted) return;
- const session = await readJson(await apiFetch2(`/sessions/${sessionId}`));
- let needsRestart = false;
- if (workingDir && !samePath(session?.working_dir, workingDir)) {
- try {
- await readJson(
- await apiFetch2("/agent/update_working_dir", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId, working_dir: workingDir })
- })
- );
- } catch (err) {
- if (!(tolerateInvalidWorkingDir && isInvalidDirectoryPathError(err))) {
- throw err;
- }
- console.warn(
- `[session-reconcile] skip invalid working_dir during resume for session ${sessionId}: ${err.message}`
- );
- }
- }
- if (sessionPolicy?.gooseMode && session?.goose_mode !== sessionPolicy.gooseMode) {
- await readJson(
- await apiFetch2("/agent/update_session", {
- method: "POST",
- body: JSON.stringify({
- session_id: sessionId,
- goose_mode: sessionPolicy.gooseMode
- })
- })
- );
- needsRestart = true;
- }
- const desired = sessionPolicy?.extensionOverrides ?? [];
- const allowed = allowedExtensionNames(desired);
- const currentPayload = await readJson(await apiFetch2(`/sessions/${sessionId}/extensions`));
- const current = currentPayload?.extensions ?? [];
- let removedAny = false;
- for (const ext of current) {
- const name = extensionName(ext);
- if (!name || allowed.has(name)) continue;
- await readJson(
- await apiFetch2("/agent/remove_extension", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId, name })
- })
- );
- removedAny = true;
- }
- const refreshed = removedAny ? (await readJson(await apiFetch2(`/sessions/${sessionId}/extensions`)))?.extensions ?? [] : current;
- const { toRemove, toAdd } = extensionsNeedingRefresh(refreshed, desired);
- for (const name of toRemove) {
- await readJson(
- await apiFetch2("/agent/remove_extension", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId, name })
- })
- );
- needsRestart = true;
- }
- for (const config of toAdd) {
- await readJson(
- await apiFetch2("/agent/add_extension", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId, config })
- })
- );
- needsRestart = true;
- }
- if (needsRestart) {
- await readJson(
- await apiFetch2("/agent/restart", {
- method: "POST",
- body: JSON.stringify({ session_id: sessionId })
- })
- );
- }
- const sandboxText = sandboxConstraints?.trim() ? buildSandboxSessionConstraints({
- baseConstraints: sandboxConstraints,
- developerTools: developerToolsFromPolicy(sessionPolicy)
- }) : null;
- const memoryEntries = buildSessionMemoryEntries({
- workingDir,
- sessionPolicy,
- sandboxConstraints: sandboxText,
- userContext,
- userMemories
- });
- if (memoryEntries.length > 0) {
- for (const entry of memoryEntries) {
- if (!entry.content?.trim()) continue;
- await apiFetch2("/agent/harness_remember", {
- method: "POST",
- body: JSON.stringify({
- sessionId,
- content: entry.content,
- title: entry.title
- })
- });
- }
- await apiFetch2("/agent/harness_bootstrap", {
- method: "POST",
- body: JSON.stringify({
- sessionId,
- force: true
- })
- });
- }
-}
-
-// imgproxy-signer.mjs
-import crypto4 from "node:crypto";
-function createImgproxySigner(key, salt) {
- const keyBuf = Buffer.from(key, "hex");
- const saltBuf = Buffer.from(salt, "hex");
- 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}${path35}`;
- }
- function buildUrl(baseUrl, storagePath, preset = "display") {
- if (!baseUrl || !storagePath) {
- throw new Error("baseUrl and storagePath are required");
- }
- let width, height, quality, format;
- switch (preset) {
- case "vision":
- width = 768;
- height = 768;
- quality = 60;
- format = "jpg";
- break;
- case "thumb":
- width = 256;
- height = 256;
- quality = 70;
- format = "jpg";
- break;
- case "display":
- default:
- width = 1280;
- height = 1280;
- quality = 85;
- format = "jpg";
- break;
- }
- const unsignedPath = `/rs:fit:${width}:${height}:0/q:${quality}/plain/local:///${storagePath}@${format}`;
- const signedPath = signPath(unsignedPath);
- const baseUrlNormalized = String(baseUrl).replace(/\/$/, "");
- return `${baseUrlNormalized}${signedPath}`;
- }
- return { buildUrl, signPath };
-}
-
-// tkmind-proxy.mjs
-var insecureDispatcher = new Agent({
- connect: { rejectUnauthorized: false }
-});
-function isHttpsTarget(target) {
- return target.startsWith("https://");
-}
-function sanitizeUserFacingProxyMessage(message, fallback = "\u540E\u7AEF\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5") {
- const normalized = String(message ?? "").trim();
- if (!normalized) return fallback;
- if (!/goose|goosed/i.test(normalized)) return normalized;
- if (/超时|timeout/i.test(normalized)) {
- return "\u540E\u7AEF\u8FDE\u63A5\u8D85\u65F6\uFF0C\u8BF7\u786E\u8BA4\u540E\u7AEF\u670D\u52A1\u6B63\u5E38\u540E\u91CD\u8BD5";
- }
- if (/不可用|连接失败|failed to fetch|networkerror|fetch failed|upstream|econn|enotfound/i.test(normalized)) {
- return fallback;
- }
- return normalized.replace(/\bgoosed\b/gi, "\u540E\u7AEF\u670D\u52A1").replace(/\bgoose\b/gi, "\u540E\u7AEF");
-}
-var PUBLIC_IMAGE_STORAGE_KEY_PATTERN = /^users\/([^/]+)\/images\/(\d{4}-\d{2}-\d{2}\/[^?#"'<>@\s]+)$/i;
-function extractPublicImageStorageKey(rawUrl) {
- const value = String(rawUrl ?? "").trim();
- if (!value) return null;
- const directMatch = value.match(/(^|\/)(users\/[^?#"'<>@\s]+\/images\/\d{4}-\d{2}-\d{2}\/[^?#"'<>@\s]+)/i);
- if (directMatch?.[2]) return directMatch[2];
- const plainIndex = value.indexOf("plain/local:///");
- if (plainIndex >= 0) {
- const tail = value.slice(plainIndex + "plain/local:///".length);
- const storageKey = tail.replace(/@[a-z0-9]+(?:[?#].*)?$/i, "").replace(/[?#].*$/, "");
- return storageKey || null;
- }
- return null;
-}
-function buildPublicStandardImageUrl(rawUrl, userId) {
- const storageKey = extractPublicImageStorageKey(rawUrl);
- if (!storageKey) return null;
- const match = storageKey.match(PUBLIC_IMAGE_STORAGE_KEY_PATTERN);
- if (!match || match[1] !== String(userId ?? "")) return null;
- return buildPublicUrl(resolvePublicBaseUrl(), userId, `public/images/${match[2]}`);
-}
-async function apiFetch(target, apiSecret, pathname, init = {}) {
- const url = new URL(pathname, target);
- const headers = {
- ...init.headers ?? {},
- "X-Secret-Key": apiSecret
- };
- if (init.body && !headers["Content-Type"]) {
- headers["Content-Type"] = "application/json";
- }
- return undiciFetch(url, {
- ...init,
- headers,
- dispatcher: isHttpsTarget(target) ? insecureDispatcher : void 0
- });
-}
-function createRuntimeRouter({
- targets,
- targetHealthy,
- redisUrl = process.env.MEMIND_RUNTIME_REDIS_URL,
- namespace = process.env.MEMIND_RUNTIME_REDIS_NAMESPACE ?? "memind:runtime"
-}) {
- if (!redisUrl || targets.length <= 1) return null;
- let clientPromise = null;
- let warned = false;
- const workerIdForTarget = (target) => {
- const idx = targets.indexOf(target);
- return idx >= 0 ? `goosed-${idx + 1}` : `goosed-${Buffer.from(String(target)).toString("base64url")}`;
- };
- const key = (...parts) => [namespace, ...parts].join(":");
- const getClient = async () => {
- if (!clientPromise) {
- clientPromise = import("redis").then(async ({ createClient }) => {
- const client = createClient({ url: redisUrl });
- client.on("error", (err) => {
- if (!warned) {
- warned = true;
- console.warn("[RuntimeRouter] Redis error:", err instanceof Error ? err.message : err);
- }
- });
- await client.connect();
- console.log("[RuntimeRouter] Redis scheduler enabled");
- return client;
- }).catch((err) => {
- clientPromise = null;
- if (!warned) {
- warned = true;
- console.warn("[RuntimeRouter] Redis disabled:", err instanceof Error ? err.message : err);
- }
- return null;
- });
- }
- return clientPromise;
- };
- const readNumber = (value) => {
- const n = Number(value ?? 0);
- return Number.isFinite(n) ? n : 0;
- };
- const parseFirstTokenSample = (value) => {
- const parts = String(value ?? "").split(":");
- return readNumber(parts[1]);
- };
- const percentile = (values, pct) => {
- const sorted = values.map((value) => readNumber(value)).filter((value) => value > 0).sort((a, b) => a - b);
- if (sorted.length === 0) return 0;
- const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(pct / 100 * sorted.length) - 1));
- return sorted[idx];
- };
- const firstTokenWindowStats = async (client, workerId, windowMs) => {
- if (!client) return { count: 0, p50Ms: 0, p95Ms: 0 };
- const now = Date.now();
- const samples = await client.sendCommand([
- "ZRANGEBYSCORE",
- key("worker", workerId, "first_token_samples"),
- String(now - windowMs),
- String(now)
- ]).catch(() => []);
- const values = samples.map(parseFirstTokenSample).filter((value) => value > 0);
- return {
- count: values.length,
- p50Ms: percentile(values, 50),
- p95Ms: percentile(values, 95)
- };
- };
- const scoreWorker = async (client, target) => {
- const workerId = workerIdForTarget(target);
- const values = await client.mGet([
- key("worker", workerId, "active_streams"),
- key("worker", workerId, "active_sessions"),
- key("worker", workerId, "ewma_first_token_ms"),
- key("worker", workerId, "error_rate"),
- key("worker", workerId, "memory_pressure"),
- key("worker", workerId, "cpu_load"),
- key("worker", workerId, "fd_pressure"),
- key("worker", workerId, "drain")
- ]);
- if (/^(1|true|yes)$/i.test(String(values[7] ?? ""))) return Number.POSITIVE_INFINITY;
- return readNumber(values[0]) * 3 + readNumber(values[1]) + readNumber(values[2]) * 0.01 + readNumber(values[3]) * 5 + readNumber(values[4]) * 2 + readNumber(values[5]) * 2 + readNumber(values[6]) * 2;
- };
- const workerScoreFromValues = (values = []) => readNumber(values[0]) * 3 + readNumber(values[1]) + readNumber(values[2]) * 0.01 + readNumber(values[3]) * 5 + readNumber(values[4]) * 2 + readNumber(values[12]) * 2 + readNumber(values[13]) * 2;
- return {
- async pickTarget(fallbackPick, orderedTargets = targets) {
- const client = await getClient();
- if (!client) return fallbackPick();
- let winner = null;
- for (const target of orderedTargets) {
- if (!await targetHealthy(target)) continue;
- const score = await scoreWorker(client, target).catch(() => Number.POSITIVE_INFINITY);
- if (!Number.isFinite(score)) continue;
- if (!winner || score < winner.score) winner = { target, score };
- }
- return winner?.target ?? fallbackPick();
- },
- async registerSession(sessionId, target) {
- const client = await getClient();
- if (!client || !sessionId || !target) return;
- const workerId = workerIdForTarget(target);
- await client.multi().set(key("session", sessionId, "worker"), workerId, { EX: 60 * 60 * 24 * 30 }).set(key("session", sessionId, "target"), target, { EX: 60 * 60 * 24 * 30 }).exec().catch(() => null);
- },
- async resolveSessionTarget(sessionId) {
- const client = await getClient();
- if (!client || !sessionId) return null;
- const target = await client.get(key("session", sessionId, "target")).catch(() => null);
- return target && targets.includes(target) ? target : null;
- },
- async streamStarted(sessionId, target) {
- const client = await getClient();
- if (!client || !target) return;
- const workerId = workerIdForTarget(target);
- const streamKey = sessionId ? key("stream", sessionId, "status") : null;
- const now = String(Date.now());
- const multi = client.multi().incr(key("worker", workerId, "active_streams")).incr(key("worker", workerId, "stream_open_count")).set(key("worker", workerId, "last_stream_started_at"), now).set(key("worker", workerId, "heartbeat"), now, { EX: 30 });
- if (streamKey) multi.set(streamKey, "active", { EX: 60 * 60 });
- await multi.exec().catch(() => null);
- },
- async streamEnded(sessionId, target, { status = "closed" } = {}) {
- const client = await getClient();
- if (!client || !target) return;
- const workerId = workerIdForTarget(target);
- const streamKey = sessionId ? key("stream", sessionId, "status") : null;
- const activeKey = key("worker", workerId, "active_streams");
- const now = String(Date.now());
- const nextValue = await client.decr(activeKey).catch(() => null);
- const multi = client.multi().set(key("worker", workerId, "last_stream_ended_at"), now).set(key("worker", workerId, "heartbeat"), now, { EX: 30 });
- if (Number(nextValue ?? 0) < 0) multi.set(activeKey, "0");
- if (status === "aborted") multi.incr(key("worker", workerId, "stream_abort_count"));
- if (status === "error") multi.incr(key("worker", workerId, "stream_error_count"));
- if (streamKey) multi.set(streamKey, status, { EX: 600 });
- await multi.exec().catch(() => null);
- },
- async firstTokenObserved(target, latencyMs) {
- const client = await getClient();
- if (!client || !target) return;
- const workerId = workerIdForTarget(target);
- const sample = Math.max(0, Math.round(Number(latencyMs) || 0));
- const ewmaKey = key("worker", workerId, "ewma_first_token_ms");
- const samplesKey = key("worker", workerId, "first_token_samples");
- const prev = readNumber(await client.get(ewmaKey).catch(() => null));
- const next = prev > 0 ? Math.round(prev * 0.8 + sample * 0.2) : sample;
- const nowMs3 = Date.now();
- const now = String(nowMs3);
- await client.multi().set(ewmaKey, String(next)).set(key("worker", workerId, "last_first_token_ms"), String(sample)).set(key("worker", workerId, "last_first_token_at"), now).incr(key("worker", workerId, "first_token_count")).set(key("worker", workerId, "heartbeat"), now, { EX: 30 }).exec().catch(() => null);
- await client.sendCommand([
- "ZADD",
- samplesKey,
- String(nowMs3),
- `${now}:${sample}:${Math.random().toString(36).slice(2)}`
- ]).catch(() => null);
- await client.sendCommand(["ZREMRANGEBYSCORE", samplesKey, "0", String(nowMs3 - 60 * 60 * 1e3)]).catch(() => null);
- await client.sendCommand(["EXPIRE", samplesKey, String(2 * 60 * 60)]).catch(() => null);
- },
- async getStatus() {
- const client = await getClient();
- const workers = [];
- for (const target of targets) {
- const workerId = workerIdForTarget(target);
- const values = client ? await client.mGet([
- key("worker", workerId, "active_streams"),
- key("worker", workerId, "active_sessions"),
- key("worker", workerId, "ewma_first_token_ms"),
- key("worker", workerId, "error_rate"),
- key("worker", workerId, "memory_pressure"),
- key("worker", workerId, "heartbeat"),
- key("worker", workerId, "drain"),
- key("worker", workerId, "stream_open_count"),
- key("worker", workerId, "stream_abort_count"),
- key("worker", workerId, "stream_error_count"),
- key("worker", workerId, "last_stream_started_at"),
- key("worker", workerId, "last_stream_ended_at"),
- key("worker", workerId, "cpu_load"),
- key("worker", workerId, "fd_pressure"),
- key("worker", workerId, "fd_count"),
- key("worker", workerId, "container_pids"),
- key("worker", workerId, "container_health"),
- 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, "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);
- workers.push({
- id: workerId,
- target,
- activeStreams: readNumber(values?.[0]),
- activeSessions: readNumber(values?.[1]),
- ewmaFirstTokenMs: readNumber(values?.[2]),
- errorRate: readNumber(values?.[3]),
- memoryPressure: readNumber(values?.[4]),
- heartbeat: values?.[5] ? Number(values[5]) : null,
- drain: /^(1|true|yes)$/i.test(String(values?.[6] ?? "")),
- streamOpenCount: readNumber(values?.[7]),
- streamAbortCount: readNumber(values?.[8]),
- streamErrorCount: readNumber(values?.[9]),
- lastStreamStartedAt: values?.[10] ? Number(values[10]) : null,
- lastStreamEndedAt: values?.[11] ? Number(values[11]) : null,
- cpuLoad: readNumber(values?.[12]),
- fdPressure: readNumber(values?.[13]),
- fdCount: readNumber(values?.[14]),
- containerPids: readNumber(values?.[15]),
- containerHealth: values?.[16] ?? null,
- metricsSampledAt: values?.[17] ? Number(values[17]) : null,
- 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)
- });
- }
- return {
- enabled: Boolean(client),
- namespace,
- workers
- };
- }
- };
-}
-async function readJsonBody(req) {
- if (req.method === "GET" || req.method === "HEAD") return null;
- const chunks = [];
- for await (const chunk of req) {
- chunks.push(chunk);
- }
- if (chunks.length === 0) return null;
- const raw = Buffer.concat(chunks).toString("utf8");
- if (!raw.trim()) return null;
- return JSON.parse(raw);
-}
-function sendProxyResponse(res, upstream) {
- res.status(upstream.status);
- upstream.headers.forEach((value, key) => {
- if (key === "transfer-encoding") return;
- res.setHeader(key, value);
- });
- if (!upstream.body) {
- res.end();
- return;
- }
- Readable.fromWeb(upstream.body).pipe(res);
-}
-function extractSessionId(req, body) {
- const fromParams = req.params?.sessionId ?? req.params?.id;
- if (fromParams) return fromParams;
- if (body?.session_id) return body.session_id;
- if (body?.sessionId) return body.sessionId;
- return null;
-}
-function firstUserText(message) {
- return message?.content?.find?.((item) => item?.type === "text" && typeof item.text === "string")?.text ?? "";
-}
-var IMAGE_URL_LINE_RE = /^\[图片\d+]:\s*(\S.+)$/;
-var PUBLIC_HTML_LINK_PATTERN = /https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi;
-var PUBLIC_HTML_MARKDOWN_LINK_PATTERN = /\[([^\]\n]*)\]\((https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html))\)/gi;
-var USER_IDENTITY_BLOCK_PATTERN = /^\[用户身份\][\s\S]*?(?:\n{2,}|$)/;
-var IMAGE_URL_LINES_PATTERN = /\n*\[图片\d+]: [^\n]+/g;
-var TKMIND_VISION_NOTE_PATTERN = /\n*【TKMind 图片分析结果[\s\S]*$/;
-function decodePathSegment(segment) {
- try {
- return decodeURIComponent(segment);
- } catch {
- return segment;
- }
-}
-function encodeUrlPath(relativePath) {
- return String(relativePath ?? "").split("/").filter(Boolean).map((part) => encodeURIComponent(part)).join("/");
-}
-function normalizeStaticHtmlRelativePath(relativePath) {
- const parts = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== "..");
- if (parts.length === 0) return "";
- if (parts[0].toLowerCase() === "public") return ["public", ...parts.slice(1)].join("/");
- if (parts.length === 1 && parts[0].toLowerCase().endsWith(".html")) return `public/${parts[0]}`;
- return parts.join("/");
-}
-function canonicalizeStaticPageUrl(publicUrl2, originalRelativePath, canonicalRelativePath) {
- const originalClean = String(originalRelativePath ?? "").replace(/^\/+/, "");
- if (!canonicalRelativePath || canonicalRelativePath === originalClean) return publicUrl2;
- const suffix = encodeUrlPath(originalClean).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- return String(publicUrl2).replace(new RegExp(`${suffix}$`), encodeUrlPath(canonicalRelativePath));
-}
-function buildMissingPublicHtmlNotice(filename) {
- return `\uFF08\u9875\u9762\u751F\u6210\u672A\u5B8C\u6210\uFF0C\u5DF2\u963B\u6B62\u663E\u793A\u5931\u6548\u94FE\u63A5\uFF1A${filename || "\u9875\u9762"}\u3002\uFF09`;
-}
-function publicHtmlExistsForUser(owner, relativePath, currentUser) {
- const normalizedOwner = String(owner ?? "").trim().toLowerCase();
- const normalizedUserId = String(currentUser?.id ?? "").trim().toLowerCase();
- const normalizedUsername = String(currentUser?.username ?? "").trim().toLowerCase();
- if (!normalizedOwner) return false;
- if (normalizedOwner !== normalizedUserId && normalizedOwner !== normalizedUsername) return true;
- const normalizedRelativePath = normalizeStaticHtmlRelativePath(relativePath);
- if (!normalizedRelativePath || !normalizedRelativePath.toLowerCase().endsWith(".html")) return false;
- 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);
- const canonicalRelativePath = normalizeStaticHtmlRelativePath(relativePath);
- if (!publicHtmlExistsForUser(owner, canonicalRelativePath, currentUser)) {
- return {
- ok: false,
- notice: buildMissingPublicHtmlNotice(path7.posix.basename(canonicalRelativePath || relativePath))
- };
- }
- return {
- ok: true,
- url: canonicalizeStaticPageUrl(publicUrl2, relativePath, canonicalRelativePath)
- };
-}
-function sanitizePublicHtmlLinksInText(text, currentUser) {
- let next = String(text ?? "").replace(
- PUBLIC_HTML_MARKDOWN_LINK_PATTERN,
- (match, label, url, owner, rawRelativePath) => {
- const result = sanitizeOwnPublicHtmlUrl(url, owner, rawRelativePath, currentUser);
- if (!result.ok) return result.notice;
- return label ? `[${label}](${result.url})` : result.url;
- }
- );
- return next.replace(PUBLIC_HTML_LINK_PATTERN, (match, owner, rawRelativePath) => {
- const result = sanitizeOwnPublicHtmlUrl(match, owner, rawRelativePath, currentUser);
- return result.ok ? result.url : result.notice;
- });
-}
-function sanitizeUserVisibleMessageText(text, currentUser) {
- return sanitizePublicHtmlLinksInText(
- String(text ?? "").replace(USER_IDENTITY_BLOCK_PATTERN, "").replace(TKMIND_VISION_NOTE_PATTERN, "").replace(IMAGE_URL_LINES_PATTERN, "").trim(),
- currentUser
- );
-}
-function sanitizeSessionMessagePublicHtmlLinks(message, currentUser) {
- if (!message || !Array.isArray(message.content)) return message;
- let changed = false;
- const imageUrls = extractImageUrlsFromMessage(message);
- const content = message.content.map((item) => {
- if (item?.type !== "text" || typeof item.text !== "string") return item;
- const nextText = sanitizeUserVisibleMessageText(item.text, currentUser);
- if (nextText === item.text) return item;
- changed = true;
- return { ...item, text: nextText };
- });
- const currentDisplayText = typeof message.metadata?.displayText === "string" ? message.metadata.displayText : null;
- const nextDisplayText = currentDisplayText == null ? null : sanitizeUserVisibleMessageText(currentDisplayText, currentUser);
- const metadataChanged = currentDisplayText != null && nextDisplayText !== currentDisplayText || !Array.isArray(message.metadata?.imageUrls) && imageUrls.length > 0;
- if (!changed && !metadataChanged) return message;
- return {
- ...message,
- content,
- metadata: {
- ...message.metadata ?? {},
- ...nextDisplayText != null ? { displayText: nextDisplayText } : {},
- ...!Array.isArray(message.metadata?.imageUrls) && imageUrls.length > 0 ? { imageUrls } : {}
- }
- };
-}
-function sanitizeSessionConversationPublicHtmlLinks(conversation, currentUser) {
- if (!Array.isArray(conversation)) return conversation;
- return conversation.map((message) => sanitizeSessionMessagePublicHtmlLinks(message, currentUser));
-}
-function createSessionEventSanitizer(currentUser, { onEvent } = {}) {
- let buffer = "";
- const flushChunk = (controller, chunk) => {
- if (!chunk) return;
- const block = String(chunk);
- if (!block.includes("data: ")) {
- controller.push(block);
- return;
- }
- const lines = block.split("\n");
- const sanitizedLines = lines.map((line) => {
- if (!line.startsWith("data: ")) return line;
- const raw = line.slice(6);
- let event;
- try {
- event = JSON.parse(raw);
- } catch {
- return line;
- }
- if (typeof onEvent === "function") {
- try {
- onEvent(event);
- } catch {
- }
- }
- if (event?.type === "Message" && event.message) {
- event.message = sanitizeSessionMessagePublicHtmlLinks(event.message, currentUser);
- } else if (event?.type === "UpdateConversation" && Array.isArray(event.conversation)) {
- event.conversation = sanitizeSessionConversationPublicHtmlLinks(event.conversation, currentUser);
- }
- return `data: ${JSON.stringify(event)}`;
- });
- controller.push(sanitizedLines.join("\n"));
- };
- return new Transform2({
- transform(chunk, _encoding, callback) {
- buffer += chunk.toString("utf8");
- let boundary = buffer.indexOf("\n\n");
- while (boundary >= 0) {
- const block = buffer.slice(0, boundary + 2);
- buffer = buffer.slice(boundary + 2);
- flushChunk(this, block);
- boundary = buffer.indexOf("\n\n");
- }
- callback();
- },
- flush(callback) {
- flushChunk(this, buffer);
- buffer = "";
- callback();
- }
- });
-}
-function extractImageUrlsFromMessage(userMessage) {
- const urls = [];
- const imageUrls = userMessage?.metadata?.imageUrls;
- if (Array.isArray(imageUrls)) {
- urls.push(...imageUrls);
- }
- const content = userMessage?.content;
- if (Array.isArray(content)) {
- for (const item of content) {
- if (item?.type === "image_url" && item.image_url?.url) {
- urls.push(item.image_url.url);
- continue;
- }
- if (item?.type !== "text" || typeof item.text !== "string") continue;
- for (const line of item.text.split("\n")) {
- const match = line.trim().match(IMAGE_URL_LINE_RE);
- if (match?.[1]) urls.push(match[1].trim());
- }
- }
- }
- return [...new Set(urls.filter((url) => typeof url === "string" && url.trim()))];
-}
-function messageHasImages(userMessage) {
- return extractImageUrlsFromMessage(userMessage).length > 0;
-}
-function prependAgentTextToUserMessage(body, transformText) {
- const originalText = firstUserText(body?.user_message);
- if (!originalText?.trim()) return body;
- const agentText = transformText(originalText);
- if (!agentText || agentText === originalText) return body;
- const userMessage = body.user_message ?? {};
- const content = Array.isArray(userMessage.content) ? [...userMessage.content] : [];
- const firstTextIndex = content.findIndex(
- (item) => item?.type === "text" && typeof item.text === "string"
- );
- if (firstTextIndex >= 0) {
- content[firstTextIndex] = { ...content[firstTextIndex], text: agentText };
- } else {
- content.unshift({ type: "text", text: agentText });
- }
- return {
- ...body,
- user_message: {
- ...userMessage,
- content,
- metadata: {
- ...userMessage.metadata ?? {},
- displayText: userMessage.metadata?.displayText && String(userMessage.metadata.displayText).trim() ? userMessage.metadata.displayText : originalText
- }
- }
- };
-}
-function injectCurrentTimeAnchor(body, timezone) {
- const tz = String(timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? "Asia/Shanghai").trim() || "Asia/Shanghai";
- return prependAgentTextToUserMessage(body, (originalText) => {
- if (originalText.includes("TKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6")) return originalText;
- return `${buildCurrentTimeAgentPrefix({ timezone: tz })}${originalText}`;
- });
-}
-function injectTaskRoutingHint(body, sessionPolicy) {
- return prependAgentTextToUserMessage(
- body,
- (originalText) => buildTaskRoutingAgentText(originalText, sessionPolicy)
- );
-}
-async function buildVisionPayload({
- userMessage,
- userId,
- publishLayout,
- localFetchAsset,
- llmProviderService: llmProviderService2,
- imgproxySigner = null
-}) {
- void imgproxySigner;
- 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() : "";
- const imageItems = [];
- for (const rawUrl of rawImageUrls) {
- try {
- const match = rawUrl.match(/\/mindspace\/v1\/assets\/([^/?#]+)(?:\/download)?/);
- let buffer;
- let mimeType = "image/jpeg";
- const fetchableUrl = (() => {
- if (/^https?:\/\//i.test(rawUrl)) return rawUrl;
- if (rawUrl.startsWith("/")) {
- const baseUrl = process.env.H5_PUBLIC_BASE_URL || "http://127.0.0.1:8081";
- return new URL(rawUrl, baseUrl).toString();
- }
- return rawUrl;
- })();
- try {
- const response = await undiciFetch(fetchableUrl);
- if (!response.ok) {
- throw new Error(`Vision fetch failed: ${response.status}`);
- }
- buffer = Buffer.from(await response.arrayBuffer());
- mimeType = response.headers.get("content-type") || "image/jpeg";
- } catch (fetchErr) {
- if (!match || !localFetchAsset) {
- throw fetchErr;
- }
- const assetId = decodeURIComponent(match[1]);
- console.warn(`Vision fetch fallback for asset ${assetId}:`, fetchErr instanceof Error ? fetchErr.message : fetchErr);
- const asset = await localFetchAsset(userId, assetId);
- buffer = asset.buffer;
- mimeType = asset.mimeType;
- }
- let relativePath = rawUrl;
- try {
- const parsed = new URL(rawUrl);
- relativePath = parsed.pathname + parsed.search;
- } catch {
- }
- const publicStandardUrl = buildPublicStandardImageUrl(rawUrl, userId);
- imageItems.push({
- mimeType,
- data: buffer.toString("base64"),
- relativePath,
- rawUrl,
- embedUrl: publicStandardUrl ?? relativePath
- });
- } catch (err) {
- console.warn("Vision image fetch skipped:", err instanceof Error ? err.message : err);
- }
- }
- if (imageItems.length === 0) return 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
-${visionDescription}
-
-` : "") + `\u5199\u4F5C\u7EA6\u675F\uFF1A\u4E0D\u5F97\u6539\u5199\u56FE\u7247\u91CC\u4EBA\u7269\u7684\u5E74\u9F84\u3001\u6027\u522B\u3001\u4EBA\u6570\u6216\u4E3B\u4F53\u5173\u7CFB\uFF1B\u5982\u679C\u7528\u6237\u660E\u786E\u8981\u6C42\u513F\u7AE5\u8BED\u6C14\u6216\u7AE5\u8DA3\u98CE\u683C\uFF0C\u4E5F\u53EA\u80FD\u8C03\u6574\u8868\u8FBE\u65B9\u5F0F\uFF0C\u4E0D\u80FD\u628A\u56FE\u7247\u4E3B\u4F53\u6539\u5199\u6210\u513F\u7AE5\u573A\u666F\u3002
-\u56FE\u7247 HTML \u5D4C\u5165\u8DEF\u5F84\uFF08\u76F4\u63A5\u5199\u5165
\u6807\u7B7E\uFF1B\u4EE5\u4E0B\u90FD\u662F\u65E0\u9700 cookie \u7684\u516C\u5F00\u538B\u7F29\u6807\u51C6\u56FE\u7247\uFF0C\u7981\u6B62\u4F7F\u7528 local://\u3001/users/ \u79C1\u6709\u8DEF\u5F84\u3001\u539F\u56FE\u5730\u5740\u6216\u9700\u8981\u767B\u5F55\u6001\u7684\u4E0B\u8F7D\u94FE\u63A5\uFF09\uFF1A
-${pathList}
-\u6267\u884C\u8981\u6C42\uFF1A\u5FC5\u987B\u5148\u8C03\u7528 load_skill \u2192 static-page-publish\uFF08\u6BCF\u6B21\u751F\u6210\u9875\u9762\u90FD\u8981\u8C03\u7528\uFF0C\u4E0D\u53EF\u7701\u7565\uFF09\uFF0C\u518D\u7528 write_file \u5199\u5165 public/\u9875\u9762.html\uFF08\u7981\u6B62\u7528 shell/cat/heredoc \u5199 HTML\uFF0C\u5BB9\u5668\u5185\u6587\u4EF6\u4E0D\u4F1A\u51FA\u73B0\u5728\u516C\u7F51\u94FE\u63A5\uFF09\uFF1B` + (publicUrlPrefix ? `\u5B8C\u6210\u540E\u5411\u7528\u6237\u7ED9\u51FA Markdown \u53EF\u70B9\u51FB\u94FE\u63A5\uFF0C\u683C\u5F0F\uFF1A[\u9875\u9762\u6807\u9898](${publicUrlPrefix}<\u6587\u4EF6\u540D>.html)\uFF1B` : "\u5B8C\u6210\u540E\u6309\u6280\u80FD\u8BF4\u660E\u91CC\u7684\u94FE\u63A5\u683C\u5F0F\u7ED9\u7528\u6237\u4E00\u4E2A Markdown \u53EF\u70B9\u51FB\u94FE\u63A5\uFF1B") + "\u4E0D\u8981\u5411\u7528\u6237\u5C55\u793A HTML \u4EE3\u7801\u5757\u3002";
- let updatedContent = Array.isArray(userMessage?.content) ? [...userMessage.content] : [];
- for (const item of imageItems) {
- updatedContent = updatedContent.map((c) => {
- if (c?.type !== "text" || typeof c.text !== "string") return c;
- const updated = c.text.replaceAll(item.rawUrl, item.embedUrl);
- return updated === c.text ? c : { ...c, text: updated };
- });
- }
- const lastTextIdx = updatedContent.reduceRight(
- (found, item, i) => found === -1 && item?.type === "text" ? i : found,
- -1
- );
- if (lastTextIdx >= 0) {
- updatedContent[lastTextIdx] = {
- ...updatedContent[lastTextIdx],
- text: updatedContent[lastTextIdx].text + injectedNote
- };
- } else {
- updatedContent.push({ type: "text", text: injectedNote });
- }
- return {
- userMessage: {
- ...userMessage,
- content: updatedContent,
- metadata: {
- ...userMessage.metadata ?? {},
- ...originalDisplayText ? { displayText: originalDisplayText } : {}
- }
- },
- billableImageCount: visionDescription ? 1 : 0
- };
-}
-function createTkmindProxy({
- apiTarget,
- apiTargets,
- apiSecret,
- userAuth: userAuth2,
- llmProviderService: llmProviderService2,
- localFetchAsset,
- subscriptionService: subscriptionService2,
- sessionSnapshotService: sessionSnapshotService2,
- conversationMemoryService: conversationMemoryService2
-}) {
- const targets = apiTargets?.length ? apiTargets : apiTarget ? [apiTarget] : [];
- const primaryTarget = targets[0] ?? apiTarget ?? "";
- let rrIdx = 0;
- 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
- );
- console.log("[TKMindProxy] imgproxy signer initialized");
- } catch (err) {
- console.warn("[TKMindProxy] imgproxy signer init failed:", err instanceof Error ? err.message : err);
- }
- }
- async function targetHealthy(target) {
- try {
- const upstream = await apiFetch(target, apiSecret, "/status", {
- method: "GET",
- signal: AbortSignal.timeout(1500)
- });
- return upstream.ok;
- } catch {
- return false;
- }
- }
- const runtimeRouter = createRuntimeRouter({ targets, targetHealthy });
- function isGeneratedSessionName2(name) {
- const normalized = typeof name === "string" ? name.trim() : "";
- return /^20\d{6}(?:[_-]\d+)?$/.test(normalized);
- }
- function hasDefaultSessionName(session) {
- if (session?.user_set_name) return false;
- const name = typeof session?.name === "string" ? session.name.trim() : "";
- return name === "" || name === "New Chat" || name === "\u65B0\u5BF9\u8BDD" || isGeneratedSessionName2(name);
- }
- async function enrichSessionHistory(sessions) {
- if (!sessionSnapshotService2?.isEnabled?.()) return;
- const needsSummary = sessions.filter(
- (session) => hasDefaultSessionName(session) || Number(session?.message_count ?? 0) === 0
- );
- if (needsSummary.length === 0) return;
- try {
- const summaries = await sessionSnapshotService2.getHistorySummaries(needsSummary.map((s) => s.id));
- for (const session of needsSummary) {
- const summary = summaries.get(session.id);
- if (!summary) continue;
- if (summary.title && hasDefaultSessionName(session)) {
- session.name = summary.title;
- }
- if (Number(session?.message_count ?? 0) === 0 && Number(summary.messageCount ?? 0) > 0) {
- session.message_count = Number(summary.messageCount);
- }
- }
- } catch {
- }
- }
- function projectSessionSummary(session) {
- return {
- id: session?.id,
- name: typeof session?.name === "string" ? session.name : "",
- message_count: Number(session?.message_count ?? 0),
- created_at: session?.created_at ?? void 0,
- updated_at: session?.updated_at ?? void 0,
- user_set_name: Boolean(session?.user_set_name),
- recipe: session?.recipe ?? null
- };
- }
- function sortSessionsByRecent(left, right) {
- const leftTime = Date.parse(left?.updated_at ?? left?.created_at ?? "") || 0;
- const rightTime = Date.parse(right?.updated_at ?? right?.created_at ?? "") || 0;
- return rightTime - leftTime;
- }
- function matchesSessionQuery(summary, rawQuery) {
- const query = String(rawQuery ?? "").trim().toLowerCase();
- if (!query) return true;
- const haystacks = [
- summary?.name,
- summary?.recipe?.title,
- summary?.id
- ].filter((value) => typeof value === "string" && value.trim()).map((value) => value.toLowerCase());
- return haystacks.some((value) => value.includes(query));
- }
- function paginateSessionConversation(conversation, beforeValue, limitValue) {
- const visibleConversation = Array.isArray(conversation) ? conversation.filter((message) => message?.metadata?.userVisible) : [];
- const total = visibleConversation.length;
- const before = Math.max(Number(beforeValue ?? 0) || 0, 0);
- const requestedLimit = Number(limitValue ?? 0) || 0;
- if (requestedLimit <= 0) {
- return {
- conversation: visibleConversation,
- page: {
- total,
- before: 0,
- limit: total,
- returned: total,
- has_more_before: false
- }
- };
- }
- const limit = Math.min(Math.max(requestedLimit, 1), 200);
- const end = Math.max(total - before, 0);
- const start = Math.max(end - limit, 0);
- const paged = visibleConversation.slice(start, end);
- return {
- conversation: paged,
- page: {
- total,
- before,
- limit,
- returned: paged.length,
- has_more_before: start > 0
- }
- };
- }
- async function pickTarget() {
- if (targets.length <= 1) return primaryTarget;
- const fallbackPick = async () => {
- for (let i = 0; i < targets.length; i += 1) {
- const target = targets[rrIdx];
- rrIdx = (rrIdx + 1) % targets.length;
- if (await targetHealthy(target)) return target;
- }
- return primaryTarget;
- };
- if (runtimeRouter) {
- const orderedTargets = targets.map((_, i) => targets[(rrIdx + i) % targets.length]);
- rrIdx = (rrIdx + 1) % targets.length;
- return runtimeRouter.pickTarget(fallbackPick, orderedTargets);
- }
- return fallbackPick();
- }
- async function rememberSessionTarget(sessionId, target) {
- if (!runtimeRouter || !sessionId || !target) return;
- await runtimeRouter.registerSession(sessionId, target).catch(() => null);
- }
- async function markStreamStarted(sessionId, target) {
- if (!runtimeRouter || !sessionId || !target) return;
- await runtimeRouter.streamStarted(sessionId, target).catch(() => null);
- }
- async function markStreamEnded(sessionId, target, options = {}) {
- if (!runtimeRouter || !sessionId || !target) return;
- await runtimeRouter.streamEnded(sessionId, target, options).catch(() => null);
- }
- function markFirstTokenObserved(target, latencyMs) {
- if (!runtimeRouter || !target) return;
- void runtimeRouter.firstTokenObserved(target, latencyMs).catch(() => null);
- }
- async function getRuntimeStatus() {
- const targetStatuses = [];
- for (const target of targets) {
- targetStatuses.push({
- target,
- healthy: await targetHealthy(target)
- });
- }
- const routerStatus = runtimeRouter ? await runtimeRouter.getStatus().catch((err) => ({
- enabled: false,
- error: err instanceof Error ? err.message : String(err)
- })) : { enabled: false, reason: "MEMIND_RUNTIME_REDIS_URL not configured" };
- return {
- publicBaseUrl: process.env.H5_PUBLIC_BASE_URL ?? null,
- router: routerStatus,
- toolRuntime: {
- defaultMode: "chat",
- codeToolMode: "code",
- chatInjectsCodeTools: false,
- codeRunsEnabled: ["1", "true", "yes", "on"].includes(
- String(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED ?? "").trim().toLowerCase()
- ),
- aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5),
- openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5)
- },
- targets: targetStatuses
- };
- }
- async function startSessionForUser(userId, {
- workingDir,
- sessionPolicy = null,
- recipe = null
- } = {}) {
- const resolvedWorkingDir = workingDir ?? await userAuth2.resolveWorkingDir(userId);
- const resolvedSessionPolicy = sessionPolicy ?? await userAuth2.getAgentSessionPolicy(userId);
- const startTarget = await pickTarget();
- const upstream = await apiFetch(startTarget, apiSecret, "/agent/start", {
- method: "POST",
- body: JSON.stringify({
- ...resolvedWorkingDir ? { working_dir: resolvedWorkingDir } : {},
- enable_context_memory: resolvedSessionPolicy?.enableContextMemory,
- ...resolvedSessionPolicy?.extensionOverrides ? { extension_overrides: resolvedSessionPolicy.extensionOverrides } : {},
- ...recipe ? { recipe } : {}
- })
- });
- const text = await upstream.text();
- if (!upstream.ok) {
- throw new Error(text || `\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25 (${upstream.status})`);
- }
- const session = JSON.parse(text);
- if (!session?.id) {
- throw new Error("\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25\uFF1A\u7F3A\u5C11 session id");
- }
- await rememberSessionTarget(session.id, startTarget);
- await userAuth2.registerAgentSession(userId, session.id, startTarget);
- if (resolvedSessionPolicy?.gooseMode) {
- const modeRes = await apiFetch(startTarget, apiSecret, "/agent/update_session", {
- method: "POST",
- body: JSON.stringify({
- session_id: session.id,
- goose_mode: resolvedSessionPolicy.gooseMode
- })
- });
- if (!modeRes.ok) {
- const modeText = await modeRes.text().catch(() => "");
- throw new Error(modeText || `\u8BBE\u7F6E\u4F1A\u8BDD\u6A21\u5F0F\u5931\u8D25 (${modeRes.status})`);
- }
- }
- const publishLayout = await userAuth2.getUserPublishLayout(userId);
- const userMemories = conversationMemoryService2?.listMemories ? await conversationMemoryService2.listMemories(userId, { limit: 40 }).catch(() => []) : [];
- await reconcileAgentSession(
- (pathname, init) => apiFetch(startTarget, apiSecret, pathname, init),
- session.id,
- {
- workingDir: resolvedWorkingDir,
- sessionPolicy: resolvedSessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- userMemories,
- userContext: publishLayout ? {
- userId,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- }
- );
- await applySessionLlmProvider(session.id);
- return session;
- }
- async function resolveTarget(sessionId) {
- if (targets.length <= 1 || !sessionId) return primaryTarget;
- try {
- const routedTarget = await runtimeRouter?.resolveSessionTarget(sessionId);
- if (routedTarget) return routedTarget;
- const { target, node } = await userAuth2.getSessionTarget(sessionId);
- if (target && targets.includes(target)) return target;
- return targets[node] ?? primaryTarget;
- } catch {
- return primaryTarget;
- }
- }
- async function applySessionLlmProvider(sessionId) {
- if (!llmProviderService2 || !sessionId) return null;
- try {
- const target = await resolveTarget(sessionId);
- return await llmProviderService2.applyBestProviderForSession(
- sessionId,
- (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init)
- );
- } catch (err) {
- console.warn(
- "LLM provider apply skipped:",
- err instanceof Error ? err.message : err
- );
- return null;
- }
- }
- async function applyLocalFallbackForSession(sessionId) {
- if (!llmProviderService2 || !sessionId) return null;
- const target = await resolveTarget(sessionId);
- return llmProviderService2.applyLocalFallbackForSession(
- sessionId,
- (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init)
- );
- }
- const visionActiveSessions = /* @__PURE__ */ new Set();
- async function applyVisionProviderForSession(sessionId) {
- if (!llmProviderService2 || !sessionId) return null;
- const target = await resolveTarget(sessionId);
- return llmProviderService2.applyVisionProviderForSession(
- sessionId,
- (url, init) => apiFetch(target, apiSecret, `${url.pathname}${url.search}`, init)
- );
- }
- async function buildVisionBody(userMessage, userId, publishLayout) {
- return buildVisionPayload({
- userMessage,
- userId,
- publishLayout,
- localFetchAsset,
- llmProviderService: llmProviderService2,
- imgproxySigner
- });
- }
- async function getSessionPolicyForToolMode(userId, toolMode = "chat") {
- if (toolMode === "code" && userAuth2.getCodeAgentSessionPolicy) {
- return userAuth2.getCodeAgentSessionPolicy(userId);
- }
- return userAuth2.getAgentSessionPolicy(userId);
- }
- async function reconcileSessionPolicyForUser(userId, sessionId, { toolMode = "chat" } = {}) {
- if (!userId || !sessionId) return;
- const target = await resolveTarget(sessionId);
- const workingDir = await userAuth2.resolveWorkingDir(userId);
- const sessionPolicy = await getSessionPolicyForToolMode(userId, toolMode);
- const publishLayout = await userAuth2.getUserPublishLayout(userId);
- const userMemories = conversationMemoryService2?.listMemories ? await conversationMemoryService2.listMemories(userId, { limit: 40 }).catch(() => []) : [];
- await reconcileAgentSession(
- (pathname, init) => apiFetch(target, apiSecret, pathname, init),
- sessionId,
- {
- workingDir,
- sessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- tolerateInvalidWorkingDir: true,
- userMemories,
- userContext: publishLayout ? {
- userId,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- }
- );
- }
- async function submitSessionReplyForUser(userId, sessionId, requestId, userMessage, { toolMode = "chat" } = {}) {
- if (!userId || !sessionId) throw new Error("\u7F3A\u5C11\u4F1A\u8BDD\u4FE1\u606F");
- const owns = await userAuth2.ownsSession(userId, sessionId);
- if (!owns) throw new Error("\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD");
- const gate = await userAuth2.canUseChat(userId);
- if (!gate.ok) {
- const err = new Error(gate.message ?? "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u7EE7\u7EED\u4F7F\u7528");
- err.code = gate.code;
- err.status = 402;
- throw err;
- }
- await reconcileSessionPolicyForUser(userId, sessionId, { toolMode });
- await applySessionLlmProvider(sessionId);
- const user = await userAuth2.getUserById(userId);
- if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728");
- let finalUserMessage = userMessage;
- 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) {
- finalUserMessage = visionResult.userMessage;
- }
- if (visionResult?.billableImageCount > 0 && subscriptionService2) {
- await subscriptionService2.consumeImageQuota(userId, visionResult.billableImageCount).catch((err) => {
- console.warn(
- "Subscription image quota consume skipped:",
- err instanceof Error ? err.message : err
- );
- });
- }
- }
- const policyState = await userAuth2.resolveUserPolicies(user);
- let body = {
- request_id: requestId,
- user_message: finalUserMessage
- };
- if (!policyState.unrestricted) {
- body = injectTaskRoutingHint(
- body,
- await userAuth2.getAgentSessionPolicy(userId)
- );
- }
- const target = await resolveTarget(sessionId);
- const upstream = await apiFetch(
- target,
- apiSecret,
- `/sessions/${encodeURIComponent(sessionId)}/reply`,
- {
- method: "POST",
- body: JSON.stringify(body)
- }
- );
- if (!upstream.ok) {
- const text = await upstream.text().catch(() => "");
- throw new Error(text || `\u53D1\u9001\u5931\u8D25 (${upstream.status})`);
- }
- return { ok: true };
- }
- const requireUser = async (req, res, next) => {
- try {
- const session = req.userSession;
- if (!session) {
- res.status(401).json({ message: "\u672A\u767B\u5F55" });
- return;
- }
- const me = await userAuth2.getMe(req.userToken);
- if (!me) {
- res.status(401).json({ message: "\u767B\u5F55\u5DF2\u8FC7\u671F" });
- return;
- }
- req.currentUser = me;
- next();
- } catch (err) {
- res.status(500).json({ message: err instanceof Error ? err.message : "\u8BA4\u8BC1\u5931\u8D25" });
- }
- };
- const ensureChatAllowed = async (req, res, next) => {
- const gate = await userAuth2.canUseChat(req.currentUser.id);
- if (!gate.ok) {
- res.status(402).json({
- message: gate.message,
- code: gate.code,
- balanceCents: gate.balanceCents,
- minRechargeCents: gate.minRechargeCents,
- suggestedTiers: gate.suggestedTiers
- });
- return;
- }
- next();
- };
- const handlers = {
- "POST /agent/start": [
- requireUser,
- ensureChatAllowed,
- async (req, res) => {
- try {
- const workingDir = await userAuth2.resolveWorkingDir(req.currentUser.id);
- const sessionPolicy = await userAuth2.getAgentSessionPolicy(req.currentUser.id);
- const startTarget = await pickTarget();
- const upstream = await apiFetch(startTarget, apiSecret, "/agent/start", {
- method: "POST",
- body: JSON.stringify({
- working_dir: workingDir,
- enable_context_memory: sessionPolicy.enableContextMemory,
- ...sessionPolicy.extensionOverrides ? { extension_overrides: sessionPolicy.extensionOverrides } : {},
- ...req.body?.recipe ? { recipe: req.body.recipe } : {}
- })
- });
- const text = await upstream.text();
- if (!upstream.ok) {
- res.status(upstream.status).send(text);
- return;
- }
- const session = JSON.parse(text);
- if (session?.id) {
- await rememberSessionTarget(session.id, startTarget);
- await userAuth2.registerAgentSession(
- req.currentUser.id,
- session.id,
- startTarget
- );
- if (sessionPolicy.gooseMode) {
- const modeRes = await apiFetch(startTarget, apiSecret, "/agent/update_session", {
- method: "POST",
- body: JSON.stringify({
- session_id: session.id,
- goose_mode: sessionPolicy.gooseMode
- })
- });
- if (!modeRes.ok) {
- const modeText = await modeRes.text().catch(() => "");
- res.status(modeRes.status).send(modeText || "\u8BBE\u7F6E\u4F1A\u8BDD\u6A21\u5F0F\u5931\u8D25");
- return;
- }
- }
- const publishLayout = await userAuth2.getUserPublishLayout(req.currentUser.id);
- const userMemories = conversationMemoryService2?.listMemories ? await conversationMemoryService2.listMemories(req.currentUser.id, { limit: 40 }).catch(() => []) : [];
- const api2 = (pathname, init) => apiFetch(startTarget, apiSecret, pathname, init);
- try {
- await reconcileAgentSession(api2, session.id, {
- workingDir,
- sessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- userMemories,
- userContext: publishLayout ? {
- userId: req.currentUser.id,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- });
- } catch (reconcileErr) {
- res.status(500).json({
- message: reconcileErr instanceof Error ? `\u4F1A\u8BDD\u7B56\u7565\u540C\u6B65\u5931\u8D25\uFF1A${reconcileErr.message}` : "\u4F1A\u8BDD\u7B56\u7565\u540C\u6B65\u5931\u8D25"
- });
- return;
- }
- await applySessionLlmProvider(session.id);
- }
- res.status(upstream.status).json(session);
- } catch (err) {
- res.status(500).json({ message: err instanceof Error ? err.message : "\u542F\u52A8\u4F1A\u8BDD\u5931\u8D25" });
- }
- }
- ],
- "POST /agent/resume": [
- requireUser,
- ensureChatAllowed,
- async (req, res) => {
- try {
- const sessionId = req.body?.session_id;
- if (!sessionId) {
- res.status(400).json({ message: "\u7F3A\u5C11 session_id" });
- return;
- }
- const owns = await userAuth2.ownsSession(req.currentUser.id, sessionId);
- if (!owns) {
- res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" });
- return;
- }
- const resumeTarget = await resolveTarget(sessionId);
- const upstream = await apiFetch(resumeTarget, apiSecret, "/agent/resume", {
- method: "POST",
- body: JSON.stringify(req.body ?? {})
- });
- const text = await upstream.text();
- if (!upstream.ok) {
- res.status(upstream.status).send(text);
- return;
- }
- const payload = JSON.parse(text);
- const skipReconcile = req.body?.skip_reconcile === true;
- if (!skipReconcile) {
- const workingDir = await userAuth2.resolveWorkingDir(req.currentUser.id);
- const sessionPolicy = await userAuth2.getAgentSessionPolicy(req.currentUser.id);
- const publishLayout = await userAuth2.getUserPublishLayout(req.currentUser.id);
- const userMemories = conversationMemoryService2?.listMemories ? await conversationMemoryService2.listMemories(req.currentUser.id, { limit: 40 }).catch(() => []) : [];
- try {
- await reconcileAgentSession(
- (pathname, init) => apiFetch(resumeTarget, apiSecret, pathname, init),
- sessionId,
- {
- workingDir,
- sessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- tolerateInvalidWorkingDir: true,
- userMemories,
- userContext: publishLayout ? {
- userId: req.currentUser.id,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- }
- );
- } catch (reconcileErr) {
- res.status(500).json({
- message: reconcileErr instanceof Error ? `\u4F1A\u8BDD\u6062\u590D\u540E\u7B56\u7565\u540C\u6B65\u5931\u8D25\uFF1A${reconcileErr.message}` : "\u4F1A\u8BDD\u6062\u590D\u540E\u7B56\u7565\u540C\u6B65\u5931\u8D25"
- });
- return;
- }
- }
- await applySessionLlmProvider(sessionId);
- res.status(upstream.status).json(payload);
- } catch (err) {
- res.status(500).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u6062\u590D\u4F1A\u8BDD\u5931\u8D25",
- "\u6062\u590D\u4F1A\u8BDD\u5931\u8D25"
- )
- });
- }
- }
- ],
- "GET /sessions": [
- requireUser,
- async (req, res) => {
- try {
- const offset = Math.max(Number(req.query?.offset ?? 0) || 0, 0);
- const requestedLimit = Number(req.query?.limit ?? 20) || 20;
- const limit = Math.min(Math.max(requestedLimit, 1), 100);
- const query = String(req.query?.query ?? "").trim();
- const owned = await userAuth2.listOwnedSessionIds(req.currentUser.id);
- const sessionsById = /* @__PURE__ */ new Map();
- let healthyTargets = 0;
- let lastFailure = null;
- for (const target of targets) {
- try {
- const upstream = await apiFetch(target, apiSecret, "/sessions", {
- method: "GET",
- signal: AbortSignal.timeout(12e3)
- });
- const text = await upstream.text();
- if (!upstream.ok) {
- lastFailure = text || `upstream ${upstream.status}`;
- continue;
- }
- healthyTargets += 1;
- const payload = JSON.parse(text);
- for (const item of payload.sessions ?? []) {
- if (owned.has(item.id)) sessionsById.set(item.id, item);
- }
- } catch (err) {
- lastFailure = sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25",
- "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25"
- );
- }
- }
- if (healthyTargets === 0) {
- res.status(502).json({ message: lastFailure ?? "\u540E\u7AEF\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" });
- return;
- }
- if (healthyTargets < targets.length) {
- res.setHeader("X-TKMind-Degraded", "1");
- }
- const sessions = [...sessionsById.values()].sort(sortSessionsByRecent);
- await enrichSessionHistory(sessions);
- if (typeof userAuth2.getSessionOrigins === "function" && sessions.length > 0) {
- try {
- const origins = await userAuth2.getSessionOrigins(sessions.map((s) => s.id));
- for (const session of sessions) {
- session.origin = origins.get(session.id) ?? "h5";
- }
- } catch {
- }
- }
- const summaries = sessions.map(projectSessionSummary).filter((item) => matchesSessionQuery(item, query));
- const paged = summaries.slice(offset, offset + limit);
- res.json({
- sessions: paged,
- page: {
- total: summaries.length,
- offset,
- limit,
- has_more: offset + paged.length < summaries.length,
- query
- }
- });
- } catch (err) {
- res.status(500).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25",
- "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25"
- )
- });
- }
- }
- ]
- };
- const sessionScoped = (build) => [
- requireUser,
- async (req, res, next) => {
- try {
- const body = req.body ?? await readJsonBody(req);
- req.body = body;
- const sessionId = extractSessionId(req, body);
- if (!sessionId) {
- res.status(400).json({ message: "\u7F3A\u5C11 session_id" });
- return;
- }
- const owns = await userAuth2.ownsSession(req.currentUser.id, sessionId);
- if (!owns) {
- res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" });
- return;
- }
- req.agentSessionId = sessionId;
- await build(req, res, next);
- } catch (err) {
- res.status(500).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u8BF7\u6C42\u5931\u8D25",
- "\u8BF7\u6C42\u5931\u8D25"
- )
- });
- }
- }
- ];
- const proxySessionEvents = async (req, res, sessionId, { onAfterFinish, onEvent } = {}) => {
- const upstreamAbort = new AbortController();
- const streamRequestedAt = Date.now();
- let clientClosed = false;
- const abortUpstream = () => {
- clientClosed = true;
- upstreamAbort.abort();
- };
- req.once("close", abortUpstream);
- try {
- const pathname = `/sessions/${sessionId}/events`;
- const sessionTarget = await resolveTarget(sessionId);
- const upstream = await apiFetch(sessionTarget, apiSecret, pathname, {
- method: "GET",
- signal: upstreamAbort.signal,
- headers: {
- Accept: "text/event-stream",
- "Last-Event-ID": req.get("last-event-id") ?? ""
- }
- });
- if (clientClosed) return;
- if (!upstream.ok || !upstream.body) {
- const text = await upstream.text().catch(() => "");
- res.status(upstream.status).send(text);
- return;
- }
- res.status(upstream.status);
- res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
- res.setHeader("Cache-Control", "no-cache, no-transform");
- res.setHeader("Connection", "keep-alive");
- res.setHeader("X-Accel-Buffering", "no");
- res.flushHeaders?.();
- let pendingBalance = null;
- const billingTransform = createSseBillingTransform({
- onFinish: async (event) => {
- const billingRequestId = event.request_id ?? event.chat_request_id ?? null;
- const result = await userAuth2.billSessionUsage(
- req.currentUser.id,
- sessionId,
- event.token_state,
- billingRequestId
- );
- if (result.ok && result.costCents > 0 && result.balanceCents != null) {
- pendingBalance = {
- balanceCents: result.balanceCents,
- tokensUsed: result.tokensUsed ?? void 0,
- lastUsage: {
- inputTokens: result.deltaInputTokens ?? 0,
- outputTokens: result.deltaOutputTokens ?? 0,
- costCents: result.costCents
- }
- };
- }
- if (typeof onAfterFinish === "function") {
- void Promise.resolve().then(() => onAfterFinish(sessionId, req.currentUser.id)).catch(() => {
- });
- }
- }
- });
- const source = Readable.fromWeb(upstream.body);
- let firstChunkSeen = false;
- const firstTokenProbe = new Transform2({
- transform(chunk, _encoding, callback) {
- if (!firstChunkSeen && chunk?.length > 0) {
- firstChunkSeen = true;
- markFirstTokenObserved(sessionTarget, Date.now() - streamRequestedAt);
- }
- callback(null, chunk);
- }
- });
- const linkSanitizer = createSessionEventSanitizer(req.currentUser, { onEvent });
- const waitForDrain = () => new Promise((resolve) => res.once("drain", resolve));
- const writeClientChunk = async (chunk) => {
- if (res.writableEnded || clientClosed) return;
- let needsDrain = !res.write(chunk);
- if (pendingBalance != null && !res.writableEnded && !clientClosed) {
- needsDrain = !res.write(appendBalanceEvent(pendingBalance)) || needsDrain;
- pendingBalance = null;
- }
- if (needsDrain && !res.writableEnded && !clientClosed) {
- await waitForDrain();
- }
- };
- const clientSink = new Writable({
- write(chunk, _encoding, callback) {
- writeClientChunk(chunk).then(() => callback(), callback);
- }
- });
- const keepalive = setInterval(() => {
- if (!res.writableEnded && !clientClosed) {
- const ok = res.write(": keepalive\n\n");
- if (!ok) source.pause();
- }
- }, 2e4);
- res.on("drain", () => source.resume());
- await markStreamStarted(sessionId, sessionTarget);
- let streamCloseStatus = "closed";
- try {
- try {
- await pipeline(source, firstTokenProbe, linkSanitizer, billingTransform, clientSink);
- } catch (err) {
- streamCloseStatus = clientClosed || upstreamAbort.signal.aborted ? "aborted" : "error";
- throw err;
- }
- } finally {
- clearInterval(keepalive);
- req.off("close", abortUpstream);
- await markStreamEnded(sessionId, sessionTarget, { status: streamCloseStatus });
- if (!res.writableEnded) res.end();
- }
- } catch (err) {
- req.off("close", abortUpstream);
- if (clientClosed || upstreamAbort.signal.aborted) {
- if (!res.writableEnded) res.end();
- return;
- }
- res.status(502).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "SSE \u4EE3\u7406\u5931\u8D25",
- "SSE \u4EE3\u7406\u5931\u8D25"
- )
- });
- }
- };
- const proxyFallback = async (req, res) => {
- try {
- const pathname = req.originalUrl.replace(/^\/api/, "") || "/";
- if (isNativeH5ApiPath(pathname)) {
- res.status(404).json({
- message: "H5 \u672C\u5730\u63A5\u53E3\u672A\u627E\u5230\uFF0C\u8BF7\u786E\u8BA4\u670D\u52A1\u7AEF\u5DF2\u66F4\u65B0\u5E76\u91CD\u542F",
- code: "not_found"
- });
- return;
- }
- const isAgentRunPath = req.method === "POST" && pathname === "/agent/runs" || req.method === "GET" && /^\/agent\/runs\/[^/]+$/.test(pathname) || req.method === "GET" && /^\/agent\/runs\/[^/]+\/events$/.test(pathname);
- if (isAgentRunPath) {
- res.status(503).json({
- message: "Agent Run \u63A5\u53E3\u9700\u5728 Portal \u672C\u5730\u5904\u7406\uFF0C\u8BF7\u786E\u8BA4 server.mjs \u5DF2\u66F4\u65B0\u5E76\u91CD\u542F\u540E\u7AEF",
- code: "AGENT_RUNS_NATIVE_REQUIRED"
- });
- return;
- }
- const policyState = await userAuth2.resolveUserPolicies(req.currentUser);
- const capabilityState = await userAuth2.resolveUserCapabilities(req.currentUser);
- const gate = evaluateProxyRequest(req.method, pathname, policyState.policies, {
- unrestricted: policyState.unrestricted
- });
- if (!gate.allowed) {
- res.status(403).json({ message: gate.reason ?? "\u8BE5 API \u5DF2\u88AB\u7B56\u7565\u7981\u6B62" });
- return;
- }
- if (/^\/agent\/harness_(bootstrap|remember)$/.test(pathname) && !capabilityState.unrestricted && !capabilityState.capabilities.context_memory) {
- res.status(403).json({ message: "\u5F53\u524D\u8D26\u6237\u672A\u5F00\u901A\u9879\u76EE\u8BB0\u5FC6\uFF0C\u65E0\u6CD5\u8BBF\u95EE\u8BE5 API" });
- return;
- }
- const isReplyPath = pathname.match(/^\/sessions\/[^/]+\/reply$/) && req.method === "POST";
- const sessionMatch = pathname.match(/^\/sessions\/([^/]+)/);
- if (isReplyPath) {
- res.status(410).json({
- message: "\u804A\u5929\u63D0\u4EA4\u5165\u53E3\u5DF2\u7EDF\u4E00\u4E3A POST /agent/runs",
- code: "AGENT_RUNS_REQUIRED"
- });
- return;
- }
- let baseBody = req.body;
- if (isReplyPath && !policyState.unrestricted) {
- const publishLayout = await userAuth2.getUserPublishLayout(req.currentUser.id).catch(() => null);
- baseBody = injectCurrentTimeAnchor(req.body, publishLayout?.timezone);
- baseBody = injectTaskRoutingHint(
- baseBody,
- await userAuth2.getAgentSessionPolicy(req.currentUser.id)
- );
- }
- if (isReplyPath && llmProviderService2) {
- const sessionId = sessionMatch?.[1];
- if (sessionId) {
- const hasImages = messageHasImages(req.body?.user_message);
- 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,
- req.currentUser?.id,
- publishLayout
- ).catch(() => null);
- if (visionResult?.userMessage) {
- baseBody = { ...baseBody ?? req.body, user_message: visionResult.userMessage };
- }
- if (visionResult?.billableImageCount > 0 && subscriptionService2) {
- await subscriptionService2.consumeImageQuota(
- req.currentUser.id,
- visionResult.billableImageCount
- ).catch((err) => {
- console.warn(
- "Subscription image quota consume skipped:",
- err instanceof Error ? err.message : err
- );
- });
- }
- }
- }
- }
- const body = req.method === "GET" || req.method === "HEAD" ? void 0 : baseBody ? JSON.stringify(baseBody) : void 0;
- const fallbackTarget = req.goosedTarget ?? (sessionMatch ? await resolveTarget(sessionMatch[1]) : primaryTarget);
- const upstream = await apiFetch(fallbackTarget, apiSecret, pathname, {
- method: req.method,
- body,
- headers: {
- Accept: req.get("accept") ?? "*/*",
- "Last-Event-ID": req.get("last-event-id") ?? ""
- }
- });
- if (req.method === "GET" && /^\/sessions\/[^/]+$/.test(pathname) && upstream.ok) {
- const payload = await upstream.json().catch(() => null);
- if (payload && Array.isArray(payload.conversation)) {
- const sanitizedConversation = sanitizeSessionConversationPublicHtmlLinks(
- payload.conversation,
- req.currentUser
- );
- const { conversation, page } = paginateSessionConversation(
- sanitizedConversation,
- req.query?.history_before,
- req.query?.history_limit
- );
- payload.conversation = conversation;
- payload.conversation_page = page;
- }
- res.status(upstream.status).json(payload ?? {});
- return;
- }
- sendProxyResponse(res, upstream);
- } catch (err) {
- res.status(502).json({
- message: sanitizeUserFacingProxyMessage(
- err instanceof Error ? err.message : "\u4EE3\u7406\u5931\u8D25",
- "\u4EE3\u7406\u5931\u8D25"
- )
- });
- }
- };
- return {
- requireUser,
- ensureChatAllowed,
- applySessionLlmProvider,
- applyLocalFallbackForSession,
- applyVisionProviderForSession,
- reconcileSessionPolicyForUser,
- handlers,
- sessionScoped,
- proxyFallback,
- proxySessionEvents,
- resolveTarget,
- startSessionForUser,
- getRuntimeStatus,
- submitSessionReplyForUser,
- apiFetch: async (pathname, init) => apiFetch(await pickTarget(), apiSecret, pathname, init),
- apiFetchTo: (target, pathname, init) => apiFetch(target, apiSecret, pathname, init)
- };
-}
-
-// user-auth.mjs
-import crypto6 from "node:crypto";
-import fs9 from "node:fs";
-import net from "node:net";
-import path10 from "node:path";
-import { Algorithm as Argon2Algorithm, hashRawSync as argon2HashRawSync } from "@node-rs/argon2";
-
-// billing.mjs
-function loadBillingConfig() {
- const useBackendCost = process.env.H5_USE_BACKEND_COST === "1";
- const marginMultiplier = Number(process.env.H5_MARGIN_MULTIPLIER ?? 1);
- return {
- useBackendCost,
- usdCnyRate: Number(process.env.H5_USD_CNY_RATE ?? 7.2),
- marginMultiplier: Number.isFinite(marginMultiplier) && marginMultiplier > 0 ? marginMultiplier : 1,
- inputCentsPer1k: Number(process.env.H5_BILL_INPUT_CENTS_PER_1K ?? 2),
- outputCentsPer1k: Number(process.env.H5_BILL_OUTPUT_CENTS_PER_1K ?? 6),
- minBillCents: Number(process.env.H5_MIN_BILL_CENTS ?? 1)
- };
-}
-function normalizeTokenState(raw) {
- if (!raw || typeof raw !== "object") {
- return {
- inputTokens: 0,
- outputTokens: 0,
- totalTokens: 0,
- accumulatedInputTokens: 0,
- accumulatedOutputTokens: 0,
- accumulatedTotalTokens: 0,
- accumulatedCost: null
- };
- }
- return {
- inputTokens: Number(raw.inputTokens ?? raw.input_tokens ?? 0),
- outputTokens: Number(raw.outputTokens ?? raw.output_tokens ?? 0),
- totalTokens: Number(raw.totalTokens ?? raw.total_tokens ?? 0),
- accumulatedInputTokens: Number(
- raw.accumulatedInputTokens ?? raw.accumulated_input_tokens ?? raw.inputTokens ?? 0
- ),
- accumulatedOutputTokens: Number(
- raw.accumulatedOutputTokens ?? raw.accumulated_output_tokens ?? raw.outputTokens ?? 0
- ),
- accumulatedTotalTokens: Number(
- raw.accumulatedTotalTokens ?? raw.accumulated_total_tokens ?? raw.totalTokens ?? 0
- ),
- accumulatedCost: raw.accumulatedCost ?? raw.accumulated_cost ?? null
- };
-}
-function computeDeltaCostCents(previous, current, config = loadBillingConfig()) {
- const prevCost = previous?.lastAccumulatedCost == null ? null : Number(previous.lastAccumulatedCost);
- const currCost = current.accumulatedCost == null ? null : Number(current.accumulatedCost);
- const margin = config.marginMultiplier ?? 1;
- if (config.useBackendCost && prevCost != null && currCost != null && currCost >= prevCost) {
- const deltaUsd = currCost - prevCost;
- if (deltaUsd <= 0) return 0;
- return Math.max(config.minBillCents, Math.ceil(deltaUsd * config.usdCnyRate * 100 * margin));
- }
- if (config.useBackendCost && prevCost == null && currCost != null && currCost > 0) {
- return Math.max(config.minBillCents, Math.ceil(currCost * config.usdCnyRate * 100 * margin));
- }
- const prevIn = Number(previous?.lastInputTokens ?? 0);
- const prevOut = Number(previous?.lastOutputTokens ?? 0);
- const deltaIn = Math.max(0, current.accumulatedInputTokens - prevIn);
- const deltaOut = Math.max(0, current.accumulatedOutputTokens - prevOut);
- if (deltaIn === 0 && deltaOut === 0) return 0;
- const raw = deltaIn / 1e3 * config.inputCentsPer1k + deltaOut / 1e3 * config.outputCentsPer1k;
- if (raw <= 0) return 0;
- return Math.max(config.minBillCents, Math.ceil(raw));
-}
-
-// billing-recharge.mjs
-import crypto5 from "node:crypto";
-function loadRechargeConfig() {
- const tiers = (process.env.H5_RECHARGE_TIERS_CENTS ?? "500,1000,3000,5000,10000,20000").split(",").map((value) => Number(value.trim())).filter((value) => Number.isFinite(value) && value > 0);
- return {
- tiersCents: tiers.length ? tiers : [500, 1e3, 3e3, 5e3, 1e4, 2e4],
- minRechargeCents: Number(process.env.H5_MIN_RECHARGE_CENTS ?? 500),
- orderTtlMs: Number(process.env.H5_RECHARGE_ORDER_TTL_MS ?? 15 * 60 * 1e3),
- maxPendingOrders: Number(process.env.H5_RECHARGE_MAX_PENDING ?? 3),
- dailyLimitCents: Number(process.env.H5_RECHARGE_DAILY_LIMIT_CENTS ?? 2e5)
- };
-}
-function isAllowedRechargeAmount(amountCents, config = loadRechargeConfig()) {
- return config.tiersCents.includes(Number(amountCents));
-}
-function buildInsufficientBalancePayload(balanceCents, config = loadRechargeConfig()) {
- return {
- code: "INSUFFICIENT_BALANCE",
- message: "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u7EE7\u7EED\u4F7F\u7528",
- balanceCents: Number(balanceCents ?? 0),
- minRechargeCents: config.minRechargeCents,
- suggestedTiers: config.tiersCents
- };
-}
-function createOutTradeNo() {
- const stamp = Date.now().toString(36).toUpperCase();
- const rand = crypto5.randomBytes(4).toString("hex").toUpperCase();
- return `TK${stamp}${rand}`.slice(0, 32);
-}
-function mapOrderRow(row) {
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- amountCents: Number(row.amount_cents),
- channel: row.channel,
- status: row.status,
- payMode: row.pay_mode,
- outTradeNo: row.out_trade_no,
- providerTxn: row.provider_txn,
- paidAt: row.paid_at == null ? null : Number(row.paid_at),
- expireAt: Number(row.expire_at),
- createdAt: Number(row.created_at),
- codeUrl: row.code_url ?? null,
- h5Url: row.h5_url ?? null
- };
-}
-function createRechargeService(pool2, { userAuth: userAuth2, wechatPay, config = loadRechargeConfig() } = {}) {
- const expireStaleOrders = async (userId) => {
- const now = Date.now();
- await pool2.query(
- `UPDATE h5_payment_orders
- SET status = 'expired', updated_at = ?
- WHERE user_id = ? AND status = 'pending' AND expire_at <= ?`,
- [now, userId, now]
- );
- };
- const countPendingOrders = async (userId) => {
- const now = Date.now();
- const [rows] = await pool2.query(
- `SELECT COUNT(*) AS total
- FROM h5_payment_orders
- WHERE user_id = ? AND status = 'pending' AND expire_at > ?`,
- [userId, now]
- );
- return Number(rows[0]?.total ?? 0);
- };
- const sumPaidToday = async (userId) => {
- const start = /* @__PURE__ */ new Date();
- start.setHours(0, 0, 0, 0);
- const [rows] = await pool2.query(
- `SELECT COALESCE(SUM(amount_cents), 0) AS total
- FROM h5_payment_orders
- WHERE user_id = ? AND status = 'paid' AND paid_at >= ?`,
- [userId, start.getTime()]
- );
- return Number(rows[0]?.total ?? 0);
- };
- const getOrderById = async (orderId) => {
- const [rows] = await pool2.query(`SELECT * FROM h5_payment_orders WHERE id = ? LIMIT 1`, [
- orderId
- ]);
- return mapOrderRow(rows[0]);
- };
- const getOrderByOutTradeNo = async (outTradeNo) => {
- const [rows] = await pool2.query(
- `SELECT * FROM h5_payment_orders WHERE out_trade_no = ? LIMIT 1`,
- [outTradeNo]
- );
- return mapOrderRow(rows[0]);
- };
- const getBillingConfig = async (userId) => {
- const user = await userAuth2.getUserById(userId);
- return {
- wechatEnabled: Boolean(wechatPay?.enabled),
- tiersCents: config.tiersCents,
- minRechargeCents: config.minRechargeCents,
- balanceCents: user ? Number(user.balance_cents ?? 0) : 0
- };
- };
- const createOrder = async ({ userId, amountCents, payScene, clientIp }) => {
- const amount = Number(amountCents);
- if (!isAllowedRechargeAmount(amount, config)) {
- return { ok: false, message: "\u5145\u503C\u91D1\u989D\u4E0D\u5728\u5141\u8BB8\u6863\u4F4D\u5185" };
- }
- if (!wechatPay?.enabled) {
- return { ok: false, message: "\u5FAE\u4FE1\u652F\u4ED8\u5C1A\u672A\u914D\u7F6E\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- const user = await userAuth2.getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.status === "disabled") return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528" };
- await expireStaleOrders(userId);
- const pending = await countPendingOrders(userId);
- if (pending >= config.maxPendingOrders) {
- return { ok: false, message: "\u5F85\u652F\u4ED8\u8BA2\u5355\u8FC7\u591A\uFF0C\u8BF7\u5148\u5B8C\u6210\u6216\u7B49\u5F85\u8FC7\u671F\u540E\u518D\u8BD5" };
- }
- const paidToday = await sumPaidToday(userId);
- if (paidToday + amount > config.dailyLimitCents) {
- return { ok: false, message: "\u5DF2\u8D85\u8FC7\u4ECA\u65E5\u5145\u503C\u4E0A\u9650\uFF0C\u8BF7\u660E\u65E5\u518D\u8BD5" };
- }
- const now = Date.now();
- const orderId = crypto5.randomUUID();
- const outTradeNo = createOutTradeNo();
- const expireAt = now + config.orderTtlMs;
- const description = `TKMind\u8D26\u6237\u5145\u503C\xA5${(amount / 100).toFixed(2)}`;
- const mode = payScene === "jsapi" ? "jsapi" : payScene === "h5" ? "h5" : "native";
- let codeUrl = null;
- let h5Url = null;
- let jsapiParams = null;
- try {
- if (mode === "jsapi") {
- const appId = wechatPay.appId;
- if (!appId) {
- return { ok: false, message: "\u5FAE\u4FE1\u652F\u4ED8 AppID \u672A\u914D\u7F6E" };
- }
- const openid = await userAuth2.getWechatOpenidForUser(userId, appId);
- if (!openid) {
- return {
- ok: false,
- message: "\u8BF7\u5148\u7528\u5FAE\u4FE1\u767B\u5F55\u5E76\u7ED1\u5B9A\u8D26\u53F7\u540E\u518D\u5145\u503C"
- };
- }
- const result = await wechatPay.createJsapiOrder({
- outTradeNo,
- description,
- amountCents: amount,
- clientIp,
- openid
- });
- jsapiParams = result.jsapiParams;
- } else if (mode === "h5") {
- const result = await wechatPay.createH5Order({
- outTradeNo,
- description,
- amountCents: amount,
- clientIp
- });
- h5Url = result.h5Url;
- } else {
- const result = await wechatPay.createNativeOrder({
- outTradeNo,
- description,
- amountCents: amount,
- clientIp
- });
- codeUrl = result.codeUrl;
- }
- } catch (err) {
- return {
- ok: false,
- message: err instanceof Error ? err.message : "\u521B\u5EFA\u652F\u4ED8\u8BA2\u5355\u5931\u8D25"
- };
- }
- await pool2.query(
- `INSERT INTO h5_payment_orders
- (id, user_id, amount_cents, channel, status, pay_mode, out_trade_no,
- code_url, h5_url, expire_at, client_ip, created_at, updated_at)
- VALUES (?, ?, ?, 'wechat', 'pending', ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- orderId,
- userId,
- amount,
- mode,
- outTradeNo,
- codeUrl,
- h5Url,
- expireAt,
- clientIp ?? null,
- now,
- now
- ]
- );
- return {
- ok: true,
- order: {
- id: orderId,
- amountCents: amount,
- status: "pending",
- payMode: mode,
- expireAt,
- codeUrl,
- h5Url,
- jsapiParams
- }
- };
- };
- const fulfillOrder = async (order, transaction) => {
- if (!order || order.status === "paid") {
- return { ok: true, alreadyPaid: true };
- }
- if (order.status !== "pending") {
- return { ok: false, message: "\u8BA2\u5355\u72B6\u6001\u4E0D\u53EF\u652F\u4ED8" };
- }
- const tradeState = transaction?.trade_state ?? (transaction?.result_code === "SUCCESS" ? "SUCCESS" : transaction?.result_code) ?? transaction?.trade_state_desc;
- if (tradeState && tradeState !== "SUCCESS") {
- return { ok: false, message: `\u652F\u4ED8\u672A\u6210\u529F: ${tradeState}` };
- }
- const paidAmount = Number(
- transaction?.amount?.total ?? transaction?.amount?.payer_total ?? transaction?.total_fee ?? 0
- );
- if (paidAmount !== order.amountCents) {
- return { ok: false, message: "\u652F\u4ED8\u91D1\u989D\u4E0E\u8BA2\u5355\u4E0D\u4E00\u81F4" };
- }
- const now = Date.now();
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT * FROM h5_payment_orders WHERE id = ? FOR UPDATE`,
- [order.id]
- );
- const locked = mapOrderRow(rows[0]);
- if (!locked) {
- await conn.rollback();
- return { ok: false, message: "\u8BA2\u5355\u4E0D\u5B58\u5728" };
- }
- if (locked.status === "paid") {
- await conn.commit();
- return { ok: true, alreadyPaid: true };
- }
- if (locked.status !== "pending") {
- await conn.rollback();
- return { ok: false, message: "\u8BA2\u5355\u72B6\u6001\u4E0D\u53EF\u652F\u4ED8" };
- }
- const recharge = await userAuth2.recharge(locked.userId, locked.amountCents, null, "\u7528\u6237\u81EA\u52A9\u5145\u503C", {
- paymentOrderId: locked.id,
- conn
- });
- if (!recharge.ok) {
- await conn.rollback();
- return recharge;
- }
- await conn.query(
- `UPDATE h5_payment_orders
- SET status = 'paid', provider_txn = ?, paid_at = ?, updated_at = ?
- WHERE id = ?`,
- [transaction.transaction_id ?? transaction.out_trade_no ?? null, now, now, locked.id]
- );
- await conn.commit();
- return { ok: true, user: recharge.user, balanceCents: recharge.user.balanceCents };
- } catch (err) {
- await conn.rollback();
- throw err;
- } finally {
- conn.release();
- }
- };
- const handleWechatNotify = async ({ headers, body }) => {
- const { transaction } = wechatPay.verifyNotify({ headers, body });
- const outTradeNo = transaction?.out_trade_no;
- if (!outTradeNo) {
- throw new Error("\u56DE\u8C03\u7F3A\u5C11\u5546\u6237\u8BA2\u5355\u53F7");
- }
- const order = await getOrderByOutTradeNo(outTradeNo);
- if (!order) {
- throw new Error("\u8BA2\u5355\u4E0D\u5B58\u5728");
- }
- return fulfillOrder(order, transaction);
- };
- const getOrderForUser = async (userId, orderId) => {
- const order = await getOrderById(orderId);
- if (!order || order.userId !== userId) return null;
- if (order.status === "pending" && order.expireAt <= Date.now()) {
- await pool2.query(
- `UPDATE h5_payment_orders SET status = 'expired', updated_at = ? WHERE id = ? AND status = 'pending'`,
- [Date.now(), orderId]
- );
- return { ...order, status: "expired" };
- }
- return order;
- };
- return {
- config,
- getBillingConfig,
- createOrder,
- getOrderForUser,
- handleWechatNotify,
- fulfillOrder,
- getOrderByOutTradeNo
- };
-}
-
-// user-space.mjs
-import fs7 from "node:fs";
-import path8 from "node:path";
-var UPLOAD_ZONE_CODES = ["oa", "public"];
-function resolveMindspaceStorageRoot(h5Root, env = process.env) {
- return path8.resolve(env.MINDSPACE_STORAGE_ROOT ?? path8.join(h5Root, "data", "mindspace"));
-}
-function resolveUserWorkspaceRoot(h5Root, user) {
- return resolvePublishDir(h5Root, user);
-}
-var WORKSPACE_HINTS_FILENAME2 = ".tkmindhints";
-var LEGACY_WORKSPACE_HINTS_FILENAME2 = ".goosehints";
-function renderUserSpaceBrandingBlock(userAddressName) {
- const name = userAddressName || "\u7528\u6237";
- return `## \u54C1\u724C\u4E0E\u79F0\u547C\uFF08\u786C\u6027\uFF09
-
-- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4ECB\u7ECD\u4EA7\u54C1\u65F6\u7528 TKMind\uFF0C\u4E0D\u8981\u79F0 goose\u3001Goose\u3001goosed
-- \u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\uFF0C\u7528 **${name}** \u79F0\u547C\u7528\u6237\uFF08\u53EF\u8F85\u4EE5\u300C\u4F60/\u60A8\u300D\uFF09\uFF0C**\u7981\u6B62**\u628A\u7528\u6237\u53EB\u4F5C TKMind
-- \u4EC5\u5728\u5F00\u573A\u6216\u7528\u6237\u6253\u62DB\u547C\u65F6\u4F7F\u7528\u65F6\u6BB5\u95EE\u5019\uFF0C\u4E14\u987B\u4E0E\u300CTKMind \u5F53\u524D\u65F6\u95F4\u57FA\u51C6\u300D\u4E00\u81F4\uFF08\u5982\u300C${name}\uFF0C\u65E9\u4E0A\u597D\u300D\uFF09\uFF1B\u666E\u901A\u56DE\u590D\u76F4\u63A5\u4F5C\u7B54\uFF0C\u4E0D\u8981\u6BCF\u6761\u90FD\u52A0\u95EE\u5019\uFF1B\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind
-- \u4E0D\u8981\u63CF\u8FF0\u672C\u5DE5\u4F5C\u533A\u4E3A\u300CRust goose \u9879\u76EE\u300D\u6216\u300Cgoose AI \u6846\u67B6\u300D
-- \u672C\u5DE5\u4F5C\u533A\u662F TKMind **MindSpace \u7528\u6237\u7A7A\u95F4**\uFF0C\u7528\u4E8E OA/\u516C\u5F00\u6587\u4EF6\u7BA1\u7406\u4E0E\u9759\u6001\u9875\u9762\u751F\u6210
-`;
-}
-function resolveZoneDir(workspaceRoot, categoryCode) {
- return path8.join(workspaceRoot, categoryCode);
-}
-function resolveZoneFilePath(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) {
- fs7.mkdirSync(resolveZoneDir(workspaceRoot, code), { recursive: true });
- }
-}
-function mirrorAssetToZone2({ workspaceRoot, categoryCode, filename, sourcePath }) {
- if (!UPLOAD_ZONE_CODES.includes(categoryCode)) return null;
- if (!sourcePath || !fs7.existsSync(sourcePath)) return null;
- ensureUserZoneDirs(workspaceRoot);
- const dest = resolveZoneFilePath(workspaceRoot, categoryCode, filename);
- 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 (fs7.existsSync(target)) fs7.unlinkSync(target);
-}
-function isPathInsideUserWorkspace(workspaceRoot, requestedPath) {
- 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 });
- const zoneLines = UPLOAD_ZONE_CODES.map(
- (code) => `- \`${code}/\` \u2014 ${zoneLabel(code)}\uFF08\u7528\u6237\u4E0A\u4F20\u843D\u5728\u6B64\u5206\u533A\uFF09`
- );
- return `# TKMind \u7528\u6237\u7A7A\u95F4\u5206\u533A
-
-\u4F60\u662F\u7528\u6237 **${addressName}** \u7684\u4E13\u5C5E TKMind \u52A9\u624B\u3002\u5F53\u524D\u4F1A\u8BDD\u5DE5\u4F5C\u533A\uFF08\u552F\u4E00\u6587\u4EF6\u6839\u76EE\u5F55\uFF09\uFF1A
-\`${workspaceRoot}\`
-
-${renderUserSpaceBrandingBlock(addressName)}
-## \u5206\u533A\u89C4\u5219\uFF08\u786C\u6027\uFF09
-
-\u7528\u6237\u4E0A\u4F20\u7684\u6587\u4EF6**\u53EA\u4F1A**\u51FA\u73B0\u5728\u4E0B\u5217\u5B50\u76EE\u5F55\u4E4B\u4E00\uFF1A
-
-${zoneLines.join("\n")}
-
-## \u67E5\u627E / \u8BFB\u53D6\u6587\u4EF6
-
-1. \u5728\u5DE5\u4F5C\u533A\u5185\u641C\u7D22\uFF0C\u4F8B\u5982 \`tree oa/\`\u3001\`find oa -name '*.csv'\`\u3001\`cat oa/\u6587\u4EF6\u540D.csv\`
-2. \u7528\u6237\u8BF4\u300COA \u533A\u300D\u2192 \`oa/\`\uFF1B\u300C\u79C1\u4EBA\u533A\u300D\u2192 \`private/\`\uFF1B\u300C\u516C\u5F00\u533A\u300D\u2192 \`public/\`
-3. **\u7981\u6B62**\u8BBF\u95EE\u5176\u5B83\u7528\u6237\u76EE\u5F55\u3001\`${PUBLISH_ROOT_DIR}/\` \u6839\u76EE\u5F55\uFF08\u975E\u672C\u7528\u6237\uFF09\u3001\`data/mindspace/\`\u3001\u4E3B\u673A\u7EDD\u5BF9\u8DEF\u5F84
-4. **\u7981\u6B62**\u7528\u516C\u7F51 URL \u5217\u76EE\u5F55\u6216\u8BFB CSV\uFF1B\u516C\u7F51\u94FE\u63A5\u4EC5\u7528\u4E8E\u5206\u4EAB\u5DF2\u53D1\u5E03 HTML
-5. \u627E\u4E0D\u5230\u65F6\u8BF4\u660E\u8BE5\u5206\u533A\u5185\u6CA1\u6709\u8BE5\u6587\u4EF6\uFF0C\u8BF7\u7528\u6237\u786E\u8BA4\u4E0A\u4F20\u5230\u4E86\u54EA\u4E2A\u5206\u533A
-
-## \u751F\u6210\u9875\u9762
-
-- \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
-- \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\`
-- \u7ED9\u94FE\u63A5\u65F6\u6309 \`static-page-publish\` skill \u91CC\u7684\u516C\u7F51\u524D\u7F00\u6A21\u677F\u62FC\u63A5\u771F\u5B9E\u5730\u5740\uFF0C\u4E0D\u786E\u5B9A\u57DF\u540D\u65F6\u7528\u76F8\u5BF9\u8DEF\u5F84 \`public/xxx.html\` \u8BF4\u660E\uFF0C\u4E0D\u8981\u731C\u4E00\u4E2A\u57DF\u540D\u5145\u6570
-- **\u7981\u6B62**\u56DE\u590D\u300C\u8BF7\u624B\u52A8\u4FDD\u5B58\u5230 public \u76EE\u5F55\u300D\u300C\u6211\u65E0\u6CD5\u751F\u6210\u9875\u9762\u300D\u2014\u2014\u9664\u975E \`write_file\` / \`edit_file\` \u8C03\u7528\u5DF2\u5931\u8D25\u5E76\u8BF4\u660E\u5177\u4F53\u9519\u8BEF
-
-## \u5DE5\u4F5C\u533A\u6587\u4EF6\u4E0E OA \u754C\u9762
-
-- \u5199\u5165 \`oa/\`\u3001\`private/\`\u3001\`public/\` \u4E0B\u4EFB\u610F\u5C42\u7EA7\u7684\u652F\u6301\u7C7B\u578B\u6587\u4EF6\uFF08docx\u3001md\u3001txt\u3001csv\u3001pdf\u3001\u56FE\u7247\u7B49\uFF09\u4F1A**\u81EA\u52A8\u540C\u6B65**\u5230 MindSpace \u8D44\u4EA7\u5E93\uFF08\u542B\u5B50\u76EE\u5F55\uFF0C\u5982 \`oa/\u8BD7\u6B4C\u6563\u6587/\u590F\u65E5\u7684\u8BD7\u7BC7.md\`\uFF09
-- \u6253\u5F00\u5BF9\u5E94\u5206\u533A\u6216\u4FDD\u5B58\u6587\u4EF6\u540E\u4F1A\u51FA\u73B0\u5728\u754C\u9762\u4E2D\uFF0C\u53EF\u76F4\u63A5\u9884\u89C8\u6216\u4E0B\u8F7D
-- \u751F\u6210 docx/pdf \u7B49 OA \u8D44\u6599\u65F6\uFF0C\u53EF\u76F4\u63A5 \`write_file\` \u5230 \`oa/<\u5B50\u76EE\u5F55>/<\u6587\u4EF6\u540D>\`\uFF0C\u65E0\u9700\u624B\u52A8\u590D\u5236\u5230\u6839\u76EE\u5F55
-- \u7528\u6237\u4E0A\u4F20\u7684\u6587\u4EF6\u4ECD\u4EE5\u754C\u9762\u5165\u5E93\u4E3A\u51C6\uFF0C\u5E76\u955C\u50CF\u5230\u4E0A\u8FF0\u5206\u533A
-`;
-}
-function buildUserSpaceConstraints({ username, workspaceRoot, publicBaseUrl, slug, displayName }) {
- const addressName = resolveUserAddressName({ displayName, username, slug });
- const zoneList = UPLOAD_ZONE_CODES.map((code) => `\`${code}/\`\uFF08${zoneLabel(code)}\uFF09`).join("\u3001");
- return [
- "## TKMind \u7528\u6237\u7A7A\u95F4\u5206\u533A\uFF08\u786C\u6027\u7EA6\u675F\uFF09",
- "",
- `- \u4F60\u662F **TKMind** \u52A9\u624B\uFF1B\u4E0E\u7528\u6237\u5BF9\u8BDD\u65F6\u7528 **${addressName}** \u79F0\u547C\u7528\u6237\uFF0C\u7981\u6B62\u628A\u7528\u6237\u53EB\u4F5C TKMind`,
- "- \u7981\u6B62\u79F0 goose / Goose / goosed \u6216\u300CRust goose \u9879\u76EE\u300D",
- `- \u7528\u6237 **${addressName}** \u7684 Agent \u5DE5\u4F5C\u533A\uFF1A\`${workspaceRoot}\``,
- `- \u7528\u6237\u4E0A\u4F20\u843D\u5728\u5206\u533A\u5B50\u76EE\u5F55\uFF1A${zoneList}\uFF08\u4F8B\u5982 OA \u6587\u4EF6\u5728 \`oa/\`\uFF09`,
- "- **\u67E5\u627E\u6587\u4EF6**\uFF1A\u53EA\u5728\u4E0A\u8FF0\u5DE5\u4F5C\u533A\u5185\u641C\u7D22\uFF08\u5982 `oa/2025-12-06T13-34_export.csv`\uFF09",
- "- **\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",
- "- **\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\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 = path8.join(workspaceRoot, WORKSPACE_HINTS_FILENAME2);
- const legacyPath = path8.join(workspaceRoot, LEGACY_WORKSPACE_HINTS_FILENAME2);
- const content = renderUserSpaceHints({ ...context, workspaceRoot });
- fs7.writeFileSync(hintsPath, content, "utf8");
- if (fs7.existsSync(legacyPath)) {
- fs7.unlinkSync(legacyPath);
- }
- return hintsPath;
-}
-async function syncUserZonesFromAssets(pool2, storageRoot, userId, workspaceRoot) {
- ensureUserZoneDirs(workspaceRoot);
- const placeholders = UPLOAD_ZONE_CODES.map(() => "?").join(", ");
- const [rows] = await pool2.query(
- `SELECT a.original_filename, c.category_code, v.storage_key
- FROM h5_assets a
- JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id
- JOIN h5_asset_versions v ON v.id = a.current_version_id
- WHERE a.user_id = ? AND a.status <> 'deleted' AND c.category_code IN (${placeholders})`,
- [userId, ...UPLOAD_ZONE_CODES]
- );
- for (const row of rows) {
- const sourcePath = path8.join(storageRoot, row.storage_key);
- mirrorAssetToZone2({
- workspaceRoot,
- categoryCode: row.category_code,
- filename: row.original_filename,
- sourcePath
- });
- }
- return { workspaceRoot, mirrored: rows.length };
-}
-async function ensureUserSpaceLayout({
- pool: pool2,
- storageRoot,
- userId,
- username,
- displayName,
- publicBaseUrl,
- slug,
- workspaceRoot
-}) {
- ensureUserZoneDirs(workspaceRoot);
- if (pool2 && userId) {
- await syncUserZonesFromAssets(pool2, storageRoot, userId, workspaceRoot);
- }
- const context = {
- username: username ?? slug,
- displayName,
- workspaceRoot,
- slug,
- publicBaseUrl
- };
- ensureUserSpaceHints(workspaceRoot, context);
- return {
- workspaceRoot,
- zonesRoot: workspaceRoot,
- zonesPublicDir: resolveZoneDir(workspaceRoot, "public"),
- constraints: buildUserSpaceConstraints(context),
- ...context
- };
-}
-
-// skills-registry.mjs
-import fs8 from "node:fs";
-import path9 from "node:path";
-import { fileURLToPath as fileURLToPath4 } from "node:url";
-var __dirname3 = path9.dirname(fileURLToPath4(import.meta.url));
-var DEFAULT_USER_SKILLS = {
- web: true,
- search: true,
- "schedule-assistant": true,
- "form-builder": true,
- "table-viewer": true,
- "product-campaign-page": true,
- "docx-generate": true,
- "long-image-download": true,
- [PUBLISH_SKILL_NAME]: false
-};
-var USER_ROLE_SKILL_PRESETS = {
- user: DEFAULT_USER_SKILLS,
- creator: {
- ...DEFAULT_USER_SKILLS,
- [PUBLISH_SKILL_NAME]: true,
- kanban: true,
- timeline: true
- },
- developer: {
- ...DEFAULT_USER_SKILLS,
- git: true,
- "diff-viewer": true,
- "code-playground": true,
- "test-runner": true
- }
-};
-function parseSkillFrontmatter(content) {
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
- if (!match) return { name: null, description: "" };
- const block = match[1];
- const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? null;
- const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
- return { name, description };
-}
-function listPlatformSkillCatalog(h5Root = __dirname3) {
- const skillsRoot = path9.join(h5Root, "skills");
- if (!fs8.existsSync(skillsRoot)) return [];
- const catalog = [];
- for (const entry of fs8.readdirSync(skillsRoot, { withFileTypes: true })) {
- if (!entry.isDirectory()) continue;
- 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({
- name,
- dirName: entry.name,
- label: name,
- description: meta.description || "\u5E73\u53F0\u901A\u7528\u6280\u80FD",
- category: "platform",
- requiresPublish: name === PUBLISH_SKILL_NAME
- });
- }
- return catalog.sort((a, b) => a.name.localeCompare(b.name));
-}
-function isValidSkillName(catalog, name) {
- return catalog.some((item) => item.name === name);
-}
-function normalizeSkillPatch(catalog, patch) {
- const normalized = {};
- for (const [key, value] of Object.entries(patch ?? {})) {
- if (!isValidSkillName(catalog, key)) continue;
- normalized[key] = Boolean(value);
- }
- return normalized;
-}
-function resolveSkillMap(roleGrants, userOverrides, catalog) {
- const resolved = {};
- for (const item of catalog) {
- const key = item.name;
- if (key in userOverrides) {
- resolved[key] = userOverrides[key];
- } else if (key in roleGrants) {
- resolved[key] = roleGrants[key];
- } else {
- resolved[key] = DEFAULT_USER_SKILLS[key] ?? false;
- }
- }
- return resolved;
-}
-function grantedSkillNames(skillMap) {
- return Object.entries(skillMap).filter(([, enabled]) => enabled).map(([name]) => name);
-}
-function applySkillGrantsToCapabilities(capabilities, skillMap) {
- const effective = { ...capabilities };
- const enabled = grantedSkillNames(skillMap);
- if (enabled.length > 0) {
- effective.skills = true;
- }
- if (enabled.includes(PUBLISH_SKILL_NAME)) {
- effective.static_publish = true;
- }
- return effective;
-}
-function copySkillTree(srcDir, destDir) {
- 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 {
- fs8.copyFileSync(from, to);
- }
- }
-}
-function syncSkillsToWorkspace({
- h5Root,
- publishDir,
- skillMap,
- catalog,
- user,
- publicBaseUrl
-}) {
- const enabled = new Set(grantedSkillNames(skillMap));
- const platformNames = new Set(catalog.map((item) => item.name));
- 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)) {
- fs8.rmSync(path9.join(agentsSkills, entry.name), { recursive: true, force: true });
- }
- }
- }
- for (const item of catalog) {
- if (!enabled.has(item.name)) continue;
- 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(),
- username: user.username ? String(user.username).trim().toLowerCase() : void 0,
- publicBaseUrl,
- publishDir
- });
- continue;
- }
- if (fs8.existsSync(srcDir)) {
- if (fs8.existsSync(destDir)) fs8.rmSync(destDir, { recursive: true, force: true });
- copySkillTree(srcDir, destDir);
- }
- }
-}
-
-// user-auth.mjs
-var USER_COOKIE = "tkmind_user_session";
-function safeEqual2(left, right) {
- const a = Buffer.from(left);
- const b = Buffer.from(right);
- return a.length === b.length && crypto6.timingSafeEqual(a, b);
-}
-var PASSWORD_ALGORITHM_PBKDF2 = "pbkdf2-sha512";
-var PASSWORD_ALGORITHM_ARGON2ID = "argon2id";
-var ARGON2_MEMORY = 64 * 1024;
-var ARGON2_PASSES = 3;
-var ARGON2_PARALLELISM = 1;
-var ARGON2_TAG_LENGTH = 32;
-function hashPasswordPbkdf2(password, salt) {
- return crypto6.pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
-}
-function hashPasswordArgon2id(password, salt) {
- return argon2HashRawSync(password, {
- salt: Buffer.from(salt, "hex"),
- parallelism: ARGON2_PARALLELISM,
- outputLen: ARGON2_TAG_LENGTH,
- memoryCost: ARGON2_MEMORY,
- timeCost: ARGON2_PASSES,
- algorithm: Argon2Algorithm.Argon2id
- }).toString("hex");
-}
-function createPasswordRecord(password, algorithm = PASSWORD_ALGORITHM_ARGON2ID) {
- const salt = crypto6.randomBytes(16).toString("hex");
- if (algorithm === PASSWORD_ALGORITHM_ARGON2ID) {
- return {
- salt,
- passwordHash: hashPasswordArgon2id(password, salt),
- passwordAlgorithm: PASSWORD_ALGORITHM_ARGON2ID
- };
- }
- return {
- salt,
- passwordHash: hashPasswordPbkdf2(password, salt),
- passwordAlgorithm: PASSWORD_ALGORITHM_PBKDF2
- };
-}
-function verifyPassword(password, row) {
- const algorithm = row.password_algorithm || PASSWORD_ALGORITHM_PBKDF2;
- if (algorithm === PASSWORD_ALGORITHM_ARGON2ID) {
- return safeEqual2(hashPasswordArgon2id(password, row.salt), row.password_hash);
- }
- return safeEqual2(hashPasswordPbkdf2(password, row.salt), row.password_hash);
-}
-function normalizeUsername(username) {
- return username.trim().toLowerCase();
-}
-function isValidUsername(username) {
- return /^[a-z0-9_]{2,32}$/.test(username);
-}
-function isValidEmail(email) {
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
-}
-function hashSessionToken(token) {
- return crypto6.createHash("sha256").update(token).digest("hex");
-}
-function createUserAuth(pool2, options = {}) {
- 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);
- const skillCatalog = listPlatformSkillCatalog(h5Root);
- const defaultSignupBalanceCents = Number(options.defaultSignupBalanceCents ?? 500);
- const lowBalanceGiftThresholdCents = Number(options.lowBalanceGiftThresholdCents ?? 100);
- const lowBalanceGiftAmountCents = Number(options.lowBalanceGiftAmountCents ?? 1e3);
- const sessionTtlMs = Number(options.sessionTtlMs ?? 7 * 24 * 60 * 60 * 1e3);
- const loginMaxFailures = Number(options.loginMaxFailures ?? 5);
- const loginFailureWindowMs = Number(options.loginFailureWindowMs ?? 5 * 60 * 1e3);
- const persistSessions = options.persistSessions !== false && Boolean(pool2);
- let rechargeNotifier = typeof options.onRechargeNotification === "function" ? options.onRechargeNotification : null;
- const subscriptionService2 = options.subscriptionService ?? null;
- const sessions = /* @__PURE__ */ new Map();
- const loginFailures = /* @__PURE__ */ new Map();
- const pruneLoginFailures = (now = Date.now()) => {
- for (const [key, state] of loginFailures) {
- if (state.resetAt <= now) loginFailures.delete(key);
- }
- };
- const pruneSessions = (now = Date.now()) => {
- for (const [token, session] of sessions) {
- if (session.expiresAt <= now) sessions.delete(token);
- }
- };
- const storeSession = async (userId, role, token, now = Date.now()) => {
- const expiresAt = now + sessionTtlMs;
- sessions.set(token, { userId, role, expiresAt });
- if (!persistSessions) return expiresAt;
- await pool2.query(
- `INSERT INTO h5_login_sessions (id, user_id, token_hash, expires_at, created_at)
- VALUES (?, ?, ?, ?, ?)`,
- [crypto6.randomUUID(), userId, hashSessionToken(token), expiresAt, now]
- );
- return expiresAt;
- };
- const revokeAllSessionsForUser = async (userId, now = Date.now()) => {
- for (const [token, session] of sessions) {
- if (session.userId === userId) sessions.delete(token);
- }
- if (!persistSessions) return;
- await pool2.query(
- `UPDATE h5_login_sessions SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL`,
- [now, userId]
- );
- };
- const ensureWorkspace = (workspaceRoot) => {
- fs9.mkdirSync(workspaceRoot, { recursive: true });
- };
- const isAdminRole = (user) => user?.role === "admin";
- const getUserById = async (userId) => {
- const [rows] = await pool2.query(
- `SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
- u.plan_type, u.workspace_root,
- s.quota_bytes, s.used_bytes, s.reserved_bytes,
- w.balance_cents, w.tokens_used,
- (SELECT COALESCE(SUM(ABS(amount_cents)), 0)
- FROM h5_billing_ledger l
- WHERE l.user_id = u.id AND l.type = 'deduct') AS spent_cents
- FROM h5_users u
- LEFT JOIN h5_user_spaces s ON s.user_id = u.id
- LEFT JOIN h5_user_wallets w ON w.user_id = u.id
- WHERE u.id = ?`,
- [userId]
- );
- return rows[0] ?? null;
- };
- const publicUser = (row) => {
- const balanceCents = Number(row.balance_cents ?? 0);
- const spentCents = Number(row.spent_cents ?? 0);
- const base = {
- id: row.id,
- username: row.username,
- slug: row.slug ?? row.username,
- email: row.email ?? null,
- displayName: row.display_name,
- role: row.role,
- status: row.status,
- planType: row.plan_type ?? "free",
- workspaceRoot: row.workspace_root,
- balanceCents,
- totalCreditCents: balanceCents + spentCents,
- tokensUsed: Number(row.tokens_used ?? 0),
- spaceQuotaBytes: Number(row.quota_bytes ?? 0),
- spaceUsedBytes: Number(row.used_bytes ?? 0),
- spaceReservedBytes: Number(row.reserved_bytes ?? 0),
- spaceAvailableBytes: Math.max(
- 0,
- Number(row.quota_bytes ?? 0) - Number(row.used_bytes ?? 0) - Number(row.reserved_bytes ?? 0)
- )
- };
- const publishKey = row.id;
- return {
- ...base,
- publishSlug: publishKey,
- publishUrl: buildPublicUrl(publicBaseUrl, publishKey),
- publishSkillName: PUBLISH_SKILL_NAME
- };
- };
- const publishLayoutFor = async (user, { migrateLegacy = true } = {}) => {
- const web = ensureUserPublishLayout({
- h5Root,
- publicBaseUrl,
- user,
- legacyUsersRoot: migrateLegacy ? usersRoot : null
- });
- const space = await ensureUserSpaceLayout({
- pool: pool2,
- storageRoot,
- userId: user.id,
- username: user.username ?? web.slug,
- displayName: user.displayName,
- publicBaseUrl,
- slug: web.slug,
- workspaceRoot: web.publishDir
- });
- const hintsContext = {
- slug: web.slug,
- username: user.username ?? web.username,
- displayName: user.displayName,
- publicBaseUrl,
- publishDir: web.publishDir
- };
- ensurePublishSkillInstalled(web.publishDir, hintsContext);
- ensureWorkspaceHintsInstalled(web.publishDir, hintsContext);
- const legacyPublishDir = resolveLegacyPublishDir(h5Root, user);
- if (legacyPublishDir && legacyPublishDir !== web.publishDir && fs9.existsSync(legacyPublishDir)) {
- ensureWorkspaceHintsInstalled(legacyPublishDir, { ...hintsContext, publishDir: legacyPublishDir });
- }
- ensureUserMemoryProfile(web.publishDir, {
- userId: user.id,
- displayName: user.displayName ?? user.display_name,
- username: user.username ?? web.username,
- slug: web.slug
- });
- return {
- ...web,
- ...space,
- publishDir: web.publishDir,
- constraints: web.constraints
- };
- };
- const listSkillGrants = async (subjectType, subjectId) => {
- const [rows] = await pool2.query(
- `SELECT skill_name, enabled
- FROM h5_user_skill_grants
- WHERE subject_type = ? AND subject_id = ?`,
- [subjectType, subjectId]
- );
- return Object.fromEntries(rows.map((row) => [row.skill_name, Boolean(row.enabled)]));
- };
- const resolveUserSkillMap = async (user) => {
- if (!user || user.role === "admin") {
- return Object.fromEntries(skillCatalog.map((item) => [item.name, true]));
- }
- const roleDefaults = await listSkillGrants("role", "user");
- const userOverrides = await listSkillGrants("user", user.id);
- return resolveSkillMap(roleDefaults, userOverrides, skillCatalog);
- };
- const syncUserSkillsForUser = async (user) => {
- if (!user || user.role === "admin") return;
- const layout = await syncUserPublishWorkspace(user);
- const skillMap = await resolveUserSkillMap(user);
- syncSkillsToWorkspace({
- h5Root,
- publishDir: layout?.publishDir,
- skillMap,
- catalog: skillCatalog,
- user,
- publicBaseUrl
- });
- };
- const syncUserPublishWorkspace = async (user) => {
- if (!user) return null;
- const layout = await publishLayoutFor(user);
- 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 = ?`, [
- layout.publishDir,
- now,
- user.id
- ]);
- await pool2.query(`DELETE FROM h5_user_path_grants WHERE user_id = ?`, [user.id]);
- await pool2.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [user.id, layout.publishDir]
- );
- }
- return layout;
- };
- const recordSignupBonus = async (conn, userId, amountCents, now) => {
- const amount = Number(amountCents);
- if (!Number.isFinite(amount) || amount <= 0) return;
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, note, operator_id, created_at)
- VALUES (?, 'adjust', ?, 0, '\u65B0\u7528\u6237\u8D60\u9001', NULL, ?)`,
- [userId, amount, now]
- );
- };
- const shouldEnableLowBalanceGift = ({ isAdmin = false, initialBalanceCents }) => {
- return !isAdmin && Number(initialBalanceCents) === defaultSignupBalanceCents && defaultSignupBalanceCents > 0;
- };
- const grantLowBalanceGiftIfNeeded = async (conn, { userId, currentBalance, nextBalance, now }) => {
- if (lowBalanceGiftAmountCents <= 0 || lowBalanceGiftThresholdCents < 0 || nextBalance > lowBalanceGiftThresholdCents) {
- return { gifted: false, balanceAfter: nextBalance };
- }
- const [rows] = await conn.query(
- `SELECT low_balance_gift_eligible, low_balance_gift_granted_at
- FROM h5_users
- WHERE id = ?
- FOR UPDATE`,
- [userId]
- );
- const user = rows[0];
- if (!user || !Boolean(user.low_balance_gift_eligible) || user.low_balance_gift_granted_at != null) {
- return { gifted: false, balanceAfter: nextBalance };
- }
- const giftedBalance = nextBalance + lowBalanceGiftAmountCents;
- await conn.query(
- `UPDATE h5_user_wallets
- SET balance_cents = ?, updated_at = ?
- WHERE user_id = ?`,
- [giftedBalance, now, userId]
- );
- await conn.query(
- `UPDATE h5_users
- SET low_balance_gift_eligible = 0,
- low_balance_gift_granted_at = ?,
- status = CASE WHEN status = 'suspended' THEN 'active' ELSE status END,
- updated_at = ?
- WHERE id = ?`,
- [now, now, userId]
- );
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, note, operator_id, created_at)
- VALUES (?, 'adjust', ?, 0, ?, NULL, ?)`,
- [userId, lowBalanceGiftAmountCents, "\u65B0\u7528\u6237\u4F4E\u4F59\u989D\u81EA\u52A8\u8D60\u9001", now]
- );
- await conn.query(
- `INSERT INTO h5_user_notifications
- (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
- VALUES (?, ?, 'web', 'low_balance_gift', ?, ?, ?, 'unread', NULL, ?, ?)`,
- [
- crypto6.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`,
- JSON.stringify({
- triggerBalanceCents: nextBalance,
- previousBalanceCents: currentBalance,
- giftAmountCents: lowBalanceGiftAmountCents,
- thresholdCents: lowBalanceGiftThresholdCents
- }),
- now,
- now
- ]
- );
- return { gifted: true, balanceAfter: giftedBalance };
- };
- const register = async ({ username, password, displayName, email }) => {
- const normalized = normalizeUsername(username);
- if (!isValidUsername(normalized)) {
- return { ok: false, message: "\u7528\u6237\u540D\u4EC5\u652F\u6301 2-32 \u4F4D\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF" };
- }
- if (!password || password.length < 6) {
- return { ok: false, message: "\u5BC6\u7801\u81F3\u5C11 6 \u4F4D" };
- }
- if (!email || !isValidEmail(email.trim())) {
- return { ok: false, message: "\u8BF7\u8F93\u5165\u6709\u6548\u90AE\u7BB1" };
- }
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
- const userId = crypto6.randomUUID();
- const layout = await publishLayoutFor({ id: userId, username: normalized });
- const workspaceRoot = layout.publishDir;
- const now = Date.now();
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- await conn.query(
- `INSERT INTO h5_users
- (id, username, slug, email, display_name, salt, password_hash, password_algorithm,
- role, status, plan_type, workspace_root, low_balance_gift_eligible, low_balance_gift_granted_at,
- created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'user', 'active', 'free', ?, 1, NULL, ?, ?)`,
- [
- userId,
- normalized,
- normalized,
- email?.trim().toLowerCase() || null,
- displayName?.trim() || normalized,
- salt,
- passwordHash,
- passwordAlgorithm,
- workspaceRoot,
- now,
- now
- ]
- );
- await conn.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)`,
- [userId, defaultSignupBalanceCents, now]
- );
- await recordSignupBonus(conn, userId, defaultSignupBalanceCents, now);
- await conn.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [userId, workspaceRoot]
- );
- await initializeDefaultSpace(conn, userId, {
- quotaBytes: Number(process.env.MINDSPACE_FREE_QUOTA_BYTES ?? 5 * 1024 * 1024),
- now
- });
- await conn.commit();
- ensureWorkspace(workspaceRoot);
- ensureUserMemoryProfile(workspaceRoot, {
- userId,
- displayName: displayName?.trim() || normalized,
- username: normalized,
- slug: normalized
- });
- if (subscriptionService2) {
- subscriptionService2.grantSubscription(userId, "free", null, null, "\u6CE8\u518C\u8D60\u9001\u514D\u8D39\u5957\u9910").catch(() => {
- });
- }
- const user = await getUserById(userId);
- return { ok: true, user: publicUser(user) };
- } catch (err) {
- await conn.rollback();
- if (err?.code === "ER_DUP_ENTRY") {
- return { ok: false, message: "\u7528\u6237\u540D\u3001\u4E3B\u9875\u5730\u5740\u6216\u90AE\u7BB1\u5DF2\u5B58\u5728" };
- }
- throw err;
- } finally {
- conn.release();
- }
- };
- const login = async ({ username, password, ip = "unknown", now = Date.now() }) => {
- pruneLoginFailures(now);
- const normalized = normalizeUsername(username);
- const failureKey = `${ip}:${normalized}`;
- const failure = loginFailures.get(failureKey);
- if (failure && failure.count >= loginMaxFailures && failure.resetAt > now) {
- return {
- ok: false,
- message: "\u5C1D\u8BD5\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
- retryAfterMs: failure.resetAt - now
- };
- }
- const [rows] = await pool2.query(
- `SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
- u.plan_type, u.workspace_root,
- u.salt, u.password_hash, u.password_algorithm, w.balance_cents, w.tokens_used
- FROM h5_users u
- LEFT JOIN h5_user_wallets w ON w.user_id = u.id
- WHERE u.username = ?`,
- [normalized]
- );
- const row = rows[0];
- if (!row) {
- const current = failure && failure.resetAt > now ? failure : { count: 0, resetAt: now + loginFailureWindowMs };
- current.count += 1;
- loginFailures.set(failureKey, current);
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF" };
- }
- if (!verifyPassword(password, row)) {
- const current = failure && failure.resetAt > now ? failure : { count: 0, resetAt: now + loginFailureWindowMs };
- current.count += 1;
- loginFailures.set(failureKey, current);
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF" };
- }
- if (row.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- if ((row.password_algorithm || PASSWORD_ALGORITHM_PBKDF2) !== PASSWORD_ALGORITHM_ARGON2ID) {
- const nextPassword = createPasswordRecord(password);
- await pool2.query(
- `UPDATE h5_users
- SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ?
- WHERE id = ?`,
- [nextPassword.salt, nextPassword.passwordHash, nextPassword.passwordAlgorithm, now, row.id]
- );
- row.salt = nextPassword.salt;
- row.password_hash = nextPassword.passwordHash;
- row.password_algorithm = nextPassword.passwordAlgorithm;
- }
- loginFailures.delete(failureKey);
- const token = crypto6.randomBytes(32).toString("base64url");
- await storeSession(row.id, row.role, token, now);
- return { ok: true, token, user: publicUser(row) };
- };
- const resetPassword = async ({ username, email, password }) => {
- const normalized = normalizeUsername(username);
- if (!isValidUsername(normalized)) {
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u90AE\u7BB1\u4E0D\u6B63\u786E" };
- }
- if (!email || !isValidEmail(email.trim())) {
- return { ok: false, message: "\u8BF7\u8F93\u5165\u6709\u6548\u90AE\u7BB1" };
- }
- if (!password || password.length < 6) {
- return { ok: false, message: "\u65B0\u5BC6\u7801\u81F3\u5C11 6 \u4F4D" };
- }
- const [rows] = await pool2.query(
- `SELECT id, email, status FROM h5_users WHERE username = ? LIMIT 1`,
- [normalized]
- );
- const row = rows[0];
- const normalizedEmail = email.trim().toLowerCase();
- if (!row || (row.email ?? "").toLowerCase() !== normalizedEmail) {
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u90AE\u7BB1\u4E0D\u6B63\u786E" };
- }
- if (row.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
- const now = Date.now();
- await pool2.query(
- `UPDATE h5_users SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ? WHERE id = ?`,
- [salt, passwordHash, passwordAlgorithm, now, row.id]
- );
- await revokeAllSessionsForUser(row.id, now);
- return { ok: true };
- };
- const verify = async (token, now = Date.now()) => {
- if (!token) return null;
- pruneSessions(now);
- const cached = sessions.get(token);
- if (cached) {
- if (cached.expiresAt <= now) {
- sessions.delete(token);
- return null;
- }
- const user = await getUserById(cached.userId);
- if (!user || user.status === "disabled") {
- await revoke(token, now);
- return null;
- }
- cached.expiresAt = now + sessionTtlMs;
- if (persistSessions) {
- await pool2.query(
- `UPDATE h5_login_sessions
- SET expires_at = ?
- WHERE token_hash = ? AND revoked_at IS NULL`,
- [cached.expiresAt, hashSessionToken(token)]
- );
- }
- return cached;
- }
- if (!persistSessions) return null;
- const tokenHash = hashSessionToken(token);
- const [rows] = await pool2.query(
- `SELECT s.user_id, s.expires_at, u.role, u.status
- FROM h5_login_sessions s
- JOIN h5_users u ON u.id = s.user_id
- WHERE s.token_hash = ? AND s.revoked_at IS NULL
- LIMIT 1`,
- [tokenHash]
- );
- const row = rows[0];
- if (!row || Number(row.expires_at ?? 0) <= now || row.status === "disabled") {
- if (row) await revoke(token, now);
- return null;
- }
- const expiresAt = now + sessionTtlMs;
- await pool2.query(
- `UPDATE h5_login_sessions SET expires_at = ? WHERE token_hash = ? AND revoked_at IS NULL`,
- [expiresAt, tokenHash]
- );
- const session = { userId: row.user_id, role: row.role, expiresAt };
- sessions.set(token, session);
- return session;
- };
- const revoke = async (token, now = Date.now()) => {
- if (!token) return;
- sessions.delete(token);
- if (!persistSessions) return;
- await pool2.query(
- `UPDATE h5_login_sessions SET revoked_at = ? WHERE token_hash = ? AND revoked_at IS NULL`,
- [now, hashSessionToken(token)]
- );
- };
- const getMe = async (token) => {
- const session = await verify(token);
- if (!session) return null;
- const user = await getUserById(session.userId);
- if (!user) return null;
- return publicUser(user);
- };
- const listPathGrants = async (userId) => {
- const [rows] = await pool2.query(
- `SELECT path, mode FROM h5_user_path_grants WHERE user_id = ? ORDER BY path`,
- [userId]
- );
- return rows.map((row) => ({ path: row.path, mode: row.mode }));
- };
- const resolveWorkingDir = async (userId) => {
- const user = await getUserById(userId);
- if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728");
- const layout = await syncUserPublishWorkspace(user);
- return layout.publishDir;
- };
- const getUserPublishLayout = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return null;
- return syncUserPublishWorkspace(user);
- };
- const isPathAllowed = async (userId, requestedPath) => {
- const user = await getUserById(userId);
- if (!user) return false;
- if (isAdminRole(user)) return true;
- const layout = await publishLayoutFor(user, { migrateLegacy: false });
- return isPathInsideUserWorkspace(layout.publishDir, requestedPath);
- };
- const repairAllUserPublishDirs = async () => {
- const [rows] = await pool2.query(
- `SELECT id, username, role, workspace_root FROM h5_users WHERE role = 'user'`
- );
- for (const row of rows) {
- await syncUserSkillsForUser(row);
- }
- };
- const seedRoleSkillDefaults = async () => {
- const now = Date.now();
- for (const [name, enabled] of Object.entries(DEFAULT_USER_SKILLS)) {
- await pool2.query(
- `INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
- VALUES ('role', 'user', ?, ?, ?)
- ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), updated_at = VALUES(updated_at)`,
- [name, enabled ? 1 : 0, now]
- );
- }
- };
- const registerAgentSession = async (userId, agentSessionId, goosedTarget = 0) => {
- const isLegacyIndex = typeof goosedTarget === "number" || /^\d+$/.test(String(goosedTarget));
- const goosedNode = isLegacyIndex ? Number(goosedTarget) : 0;
- const targetUrl = isLegacyIndex ? null : String(goosedTarget);
- await pool2.query(
- `INSERT INTO h5_user_sessions (agent_session_id, user_id, goosed_node, goosed_target, created_at)
- VALUES (?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE user_id = VALUES(user_id),
- goosed_node = VALUES(goosed_node), goosed_target = VALUES(goosed_target)`,
- [agentSessionId, userId, goosedNode, targetUrl, Date.now()]
- );
- };
- const getSessionNode = async (agentSessionId) => {
- const [rows] = await pool2.query(
- `SELECT goosed_node FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
- [agentSessionId]
- );
- return rows[0]?.goosed_node ?? 0;
- };
- const getSessionTarget = async (agentSessionId) => {
- const [rows] = await pool2.query(
- `SELECT goosed_node, goosed_target FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
- [agentSessionId]
- );
- return { target: rows[0]?.goosed_target ?? null, node: rows[0]?.goosed_node ?? 0 };
- };
- const ownsSession = async (userId, agentSessionId) => {
- const [rows] = await pool2.query(
- `SELECT 1 FROM h5_user_sessions WHERE agent_session_id = ? AND user_id = ? LIMIT 1`,
- [agentSessionId, userId]
- );
- return rows.length > 0;
- };
- const listOwnedSessionIds = async (userId) => {
- const [rows] = await pool2.query(
- `SELECT agent_session_id FROM h5_user_sessions WHERE user_id = ?`,
- [userId]
- );
- return new Set(rows.map((row) => row.agent_session_id));
- };
- const setSessionOrigin = async (agentSessionId, origin) => {
- if (!agentSessionId || origin !== "h5" && origin !== "wechat") return;
- await pool2.query(
- `UPDATE h5_user_sessions SET origin = ? WHERE agent_session_id = ?`,
- [origin, agentSessionId]
- );
- };
- const getSessionOrigins = async (agentSessionIds = []) => {
- const ids = [...new Set(agentSessionIds)].filter(Boolean);
- if (ids.length === 0) return /* @__PURE__ */ new Map();
- const [rows] = await pool2.query(
- `SELECT agent_session_id, origin FROM h5_user_sessions WHERE agent_session_id IN (?)`,
- [ids]
- );
- return new Map(rows.map((row) => [row.agent_session_id, row.origin]));
- };
- const unregisterAgentSession = async (userId, agentSessionId) => {
- await pool2.query(
- `DELETE FROM h5_user_sessions WHERE agent_session_id = ? AND user_id = ?`,
- [agentSessionId, userId]
- );
- };
- const canUseChat = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528" };
- }
- if (user.status === "suspended" && !isAdminRole(user)) {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u6682\u505C" };
- }
- if (isAdminRole(user)) {
- return { ok: true, balanceCents: Number(user.balance_cents ?? 0) };
- }
- if (subscriptionService2) {
- const sub = await subscriptionService2.getActiveSubscription(userId);
- if (sub) {
- const unlimited = sub.periodTokensLimit === 0;
- const hasQuota = unlimited || sub.periodTokensUsed < sub.periodTokensLimit;
- const balanceCents2 = Number(user.balance_cents ?? 0);
- if (hasQuota) {
- return { ok: true, balanceCents: balanceCents2, subscription: sub };
- }
- if (balanceCents2 > 0) {
- return { ok: true, balanceCents: balanceCents2, subscription: sub, overQuota: true };
- }
- return {
- ok: false,
- message: "\u672C\u6708\u989D\u5EA6\u5DF2\u7528\u5B8C\uFF0C\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u6216\u5347\u7EA7\u5957\u9910",
- ...buildInsufficientBalancePayload(balanceCents2, loadRechargeConfig())
- };
- }
- }
- const balanceCents = Number(user.balance_cents ?? 0);
- if (balanceCents <= 0) {
- return {
- ok: false,
- message: "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u7EE7\u7EED\u4F7F\u7528",
- ...buildInsufficientBalancePayload(balanceCents, loadRechargeConfig())
- };
- }
- return { ok: true, balanceCents };
- };
- const listUsers = async ({ page = 1, pageSize = 20, search = "", role = "", status = "" } = {}) => {
- const safePageSize = Math.min(Math.max(Number(pageSize) || 20, 1), 100);
- const safePage = Math.max(Number(page) || 1, 1);
- const offset = (safePage - 1) * safePageSize;
- const params = [];
- const clauses = [];
- if (search) {
- clauses.push("(u.username LIKE ? OR u.display_name LIKE ?)");
- params.push(`%${search}%`, `%${search}%`);
- }
- if (role) {
- clauses.push("u.role = ?");
- params.push(role);
- }
- if (status) {
- clauses.push("u.status = ?");
- params.push(status);
- }
- const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
- const [[{ total }]] = await pool2.query(
- `SELECT COUNT(*) AS total FROM h5_users u ${where}`,
- params
- );
- const [rows] = await pool2.query(
- `SELECT u.id, u.username, u.slug, u.email, u.display_name, u.role, u.status,
- u.plan_type, u.workspace_root,
- s.quota_bytes, s.used_bytes, s.reserved_bytes,
- u.created_at, u.updated_at, w.balance_cents, w.tokens_used
- FROM h5_users u
- LEFT JOIN h5_user_spaces s ON s.user_id = u.id
- LEFT JOIN h5_user_wallets w ON w.user_id = u.id
- ${where}
- ORDER BY u.created_at DESC
- LIMIT ${safePageSize} OFFSET ${offset}`,
- params
- );
- return {
- users: rows.map((row) => ({ ...publicUser(row), createdAt: Number(row.created_at), updatedAt: Number(row.updated_at) })),
- total: Number(total),
- page: safePage,
- pageSize: safePageSize
- };
- };
- const getUserPublic = async (userId) => {
- const user = await getUserById(userId);
- return user ? publicUser(user) : null;
- };
- const createUser = async ({
- username,
- password,
- displayName,
- workspaceRoot,
- balanceCents,
- role = "user",
- email
- }) => {
- const normalized = normalizeUsername(username);
- if (!isValidUsername(normalized)) {
- return { ok: false, message: "\u7528\u6237\u540D\u683C\u5F0F\u65E0\u6548" };
- }
- if (!password || password.length < 6) {
- return { ok: false, message: "\u5BC6\u7801\u81F3\u5C11 6 \u4F4D" };
- }
- const isAdmin = role === "admin";
- const userId = crypto6.randomUUID();
- const root = (await publishLayoutFor({ id: userId, username: normalized })).publishDir;
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(password);
- const now = Date.now();
- const conn = await pool2.getConnection();
- const initialBalanceCents = Number(balanceCents ?? defaultSignupBalanceCents);
- try {
- await conn.beginTransaction();
- await conn.query(
- `INSERT INTO h5_users
- (id, username, slug, email, display_name, salt, password_hash, password_algorithm,
- role, status, plan_type, workspace_root, low_balance_gift_eligible, low_balance_gift_granted_at,
- created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 'free', ?, ?, NULL, ?, ?)`,
- [
- userId,
- normalized,
- normalized,
- email?.trim().toLowerCase() || null,
- displayName?.trim() || normalized,
- salt,
- passwordHash,
- passwordAlgorithm,
- isAdmin ? "admin" : "user",
- root,
- shouldEnableLowBalanceGift({ isAdmin, initialBalanceCents }) ? 1 : 0,
- now,
- now
- ]
- );
- await conn.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)`,
- [userId, initialBalanceCents, now]
- );
- await conn.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [userId, root]
- );
- if (!isAdmin) {
- await initializeDefaultSpace(conn, userId, {
- quotaBytes: Number(process.env.MINDSPACE_FREE_QUOTA_BYTES ?? 5 * 1024 * 1024),
- now
- });
- }
- await conn.commit();
- ensureWorkspace(root);
- if (subscriptionService2 && !isAdmin) {
- subscriptionService2.grantSubscription(userId, "free", null, null, "\u6CE8\u518C\u8D60\u9001\u514D\u8D39\u5957\u9910").catch(() => {
- });
- }
- const user = await getUserById(userId);
- return { ok: true, user: publicUser(user) };
- } catch (err) {
- await conn.rollback();
- if (err?.code === "ER_DUP_ENTRY") {
- return { ok: false, message: "\u7528\u6237\u540D\u5DF2\u5B58\u5728" };
- }
- throw err;
- } finally {
- conn.release();
- }
- };
- const updateUser = async (userId, patch) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const now = Date.now();
- const fields = [];
- const values = [];
- if (patch.displayName !== void 0) {
- fields.push("display_name = ?");
- values.push(patch.displayName.trim() || user.username);
- }
- if (patch.status !== void 0) {
- fields.push("status = ?");
- values.push(patch.status);
- }
- if (patch.workspaceRoot !== void 0) {
- const root = path10.resolve(patch.workspaceRoot);
- fields.push("workspace_root = ?");
- values.push(root);
- ensureWorkspace(root);
- await pool2.query(`DELETE FROM h5_user_path_grants WHERE user_id = ?`, [userId]);
- await pool2.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [userId, root]
- );
- }
- if (patch.role !== void 0) {
- fields.push("role = ?");
- values.push(patch.role === "admin" ? "admin" : "user");
- }
- if (fields.length > 0) {
- fields.push("updated_at = ?");
- values.push(now, userId);
- await pool2.query(`UPDATE h5_users SET ${fields.join(", ")} WHERE id = ?`, values);
- }
- if (patch.status === "disabled" || patch.status === "suspended") {
- await revokeAllSessionsForUser(userId, now);
- }
- if (patch.balanceCents !== void 0) {
- await pool2.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)
- ON DUPLICATE KEY UPDATE balance_cents = VALUES(balance_cents), updated_at = VALUES(updated_at)`,
- [userId, Number(patch.balanceCents), now]
- );
- }
- if (patch.spaceQuotaBytes !== void 0) {
- const quotaBytes = Math.floor(Number(patch.spaceQuotaBytes));
- if (!Number.isFinite(quotaBytes) || quotaBytes <= 0) {
- return { ok: false, message: "\u7A7A\u95F4\u5927\u5C0F\u65E0\u6548" };
- }
- const [spaceRows] = await pool2.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes
- FROM h5_user_spaces
- WHERE user_id = ?
- LIMIT 1`,
- [userId]
- );
- const currentSpace = spaceRows[0];
- const occupiedBytes = Number(currentSpace?.used_bytes ?? 0) + Number(currentSpace?.reserved_bytes ?? 0);
- if (quotaBytes < occupiedBytes) {
- return {
- ok: false,
- message: `\u7A7A\u95F4\u4E0D\u80FD\u5C0F\u4E8E\u5DF2\u4F7F\u7528\u5BB9\u91CF ${Math.ceil(occupiedBytes / 1024 / 1024)} MB`
- };
- }
- if (currentSpace?.id) {
- await pool2.query(
- `UPDATE h5_user_spaces
- SET quota_bytes = ?, updated_at = ?
- WHERE user_id = ?`,
- [quotaBytes, now, userId]
- );
- } else {
- await initializeDefaultSpace(pool2, userId, {
- quotaBytes,
- now
- });
- }
- }
- const updated = await getUserById(userId);
- return { ok: true, user: publicUser(updated) };
- };
- const purchaseSpaceQuota = async (userId, sizeMb) => {
- const purchaseMb = Math.floor(Number(sizeMb));
- if (!Number.isFinite(purchaseMb) || purchaseMb <= 0) {
- return { ok: false, message: "\u6269\u5BB9\u5927\u5C0F\u65E0\u6548" };
- }
- const deltaBytes = purchaseMb * 1024 * 1024;
- const costCents = purchaseMb * 200;
- const now = Date.now();
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [spaceRows] = await conn.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes
- FROM h5_user_spaces
- WHERE user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [userId]
- );
- if (!spaceRows[0]) {
- await initializeDefaultSpace(conn, userId, { now });
- }
- const [walletRows] = await conn.query(
- `SELECT balance_cents
- FROM h5_user_wallets
- WHERE user_id = ?
- FOR UPDATE`,
- [userId]
- );
- const balanceCents = Number(walletRows[0]?.balance_cents ?? 0);
- if (balanceCents < costCents) {
- await conn.rollback();
- return {
- ok: false,
- code: "INSUFFICIENT_BALANCE",
- message: "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5148\u5145\u503C\u540E\u518D\u8D2D\u4E70\u7A7A\u95F4",
- balanceCents,
- minRechargeCents: Math.max(500, costCents - balanceCents),
- suggestedTiers: loadRechargeConfig().tiersCents
- };
- }
- await conn.query(
- `UPDATE h5_user_wallets
- SET balance_cents = balance_cents - ?, updated_at = ?
- WHERE user_id = ?`,
- [costCents, now, userId]
- );
- await conn.query(
- `UPDATE h5_user_spaces
- SET quota_bytes = quota_bytes + ?, updated_at = ?
- WHERE user_id = ?`,
- [deltaBytes, now, userId]
- );
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, note, operator_id, created_at)
- VALUES (?, 'deduct', ?, 0, ?, NULL, ?)`,
- [userId, costCents, `space_purchase:${purchaseMb}MB`, now]
- );
- await conn.query(
- `INSERT INTO h5_user_notifications
- (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
- VALUES (?, ?, 'web', 'space_purchase', ?, ?, ?, 'unread', NULL, ?, ?)`,
- [
- crypto6.randomUUID(),
- userId,
- "\u7A7A\u95F4\u6269\u5BB9\u6210\u529F",
- `\u5DF2\u8D2D\u4E70 ${purchaseMb} MB \u7A7A\u95F4\uFF0C\u652F\u4ED8 \xA5${(costCents / 100).toFixed(2)}\u3002`,
- JSON.stringify({ purchaseMb, deltaBytes, costCents }),
- now,
- now
- ]
- );
- await conn.commit();
- const [updatedSpaceRows] = await pool2.query(
- `SELECT quota_bytes, used_bytes, reserved_bytes
- FROM h5_user_spaces
- WHERE user_id = ?
- LIMIT 1`,
- [userId]
- );
- const updatedSpace = updatedSpaceRows[0] ?? {};
- const updatedUser = await getUserById(userId);
- return {
- ok: true,
- balanceCents: Number(updatedUser?.balance_cents ?? Math.max(0, balanceCents - costCents)),
- quota: {
- quotaBytes: Number(updatedSpace.quota_bytes ?? 0),
- usedBytes: Number(updatedSpace.used_bytes ?? 0),
- reservedBytes: Number(updatedSpace.reserved_bytes ?? 0),
- availableBytes: Math.max(
- 0,
- Number(updatedSpace.quota_bytes ?? 0) - Number(updatedSpace.used_bytes ?? 0) - Number(updatedSpace.reserved_bytes ?? 0)
- )
- }
- };
- } catch (err) {
- await conn.rollback();
- throw err;
- } finally {
- conn.release();
- }
- };
- const recharge = async (userId, amountCents, operatorId, note = "", options2 = {}) => {
- const amount = Number(amountCents);
- if (!Number.isFinite(amount) || amount <= 0) {
- return { ok: false, message: "\u5145\u503C\u91D1\u989D\u65E0\u6548" };
- }
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const paymentOrderId = options2.paymentOrderId ?? null;
- const ledgerNote = paymentOrderId ? `order:${paymentOrderId}` : note || (operatorId ? "\u7BA1\u7406\u5458\u5145\u503C" : "\u8D26\u6237\u5145\u503C");
- const rechargeType = paymentOrderId ? "self_recharge" : operatorId ? "admin_recharge" : "recharge";
- const now = Date.now();
- const ownsConnection = !options2.conn;
- const conn = options2.conn ?? await pool2.getConnection();
- try {
- if (ownsConnection) await conn.beginTransaction();
- await conn.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)
- ON DUPLICATE KEY UPDATE
- balance_cents = balance_cents + VALUES(balance_cents),
- updated_at = VALUES(updated_at)`,
- [userId, amount, now]
- );
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, note, operator_id, created_at)
- VALUES (?, 'recharge', ?, 0, ?, ?, ?)`,
- [userId, amount, ledgerNote, operatorId, now]
- );
- const title = paymentOrderId ? "\u5145\u503C\u6210\u529F" : operatorId ? "\u7BA1\u7406\u5458\u5DF2\u5145\u503C" : "\u8D26\u6237\u5145\u503C\u6210\u529F";
- const body = paymentOrderId ? `\u4F60\u5DF2\u6210\u529F\u5145\u503C \xA5${(amount / 100).toFixed(2)}\uFF0C\u4F59\u989D\u5DF2\u66F4\u65B0\u3002` : operatorId ? `\u7BA1\u7406\u5458\u5DF2\u4E3A\u4F60\u5145\u503C \xA5${(amount / 100).toFixed(2)}\uFF0C\u4F59\u989D\u5DF2\u66F4\u65B0\u3002` : `\u4F60\u7684\u8D26\u6237\u5DF2\u5145\u503C \xA5${(amount / 100).toFixed(2)}\uFF0C\u4F59\u989D\u5DF2\u66F4\u65B0\u3002`;
- await conn.query(
- `INSERT INTO h5_user_notifications
- (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at)
- VALUES (?, ?, 'web', ?, ?, ?, ?, 'unread', NULL, ?, ?)`,
- [
- crypto6.randomUUID(),
- userId,
- rechargeType,
- title,
- body,
- JSON.stringify({
- amountCents: amount,
- operatorId: operatorId ?? null,
- paymentOrderId
- }),
- now,
- now
- ]
- );
- if (user.status === "suspended") {
- await conn.query(`UPDATE h5_users SET status = 'active', updated_at = ? WHERE id = ?`, [
- now,
- userId
- ]);
- }
- if (ownsConnection) await conn.commit();
- const updated = await getUserById(userId);
- if (rechargeNotifier) {
- try {
- await rechargeNotifier({
- userId,
- amountCents: amount,
- operatorId: operatorId ?? null,
- paymentOrderId,
- dedupeKey: paymentOrderId ? `recharge:${paymentOrderId}` : null,
- notificationType: rechargeType,
- title,
- body,
- user: publicUser(updated)
- });
- } catch (err) {
- console.warn(
- "Recharge notifier failed:",
- err instanceof Error ? err.message : String(err)
- );
- }
- }
- return { ok: true, user: publicUser(updated) };
- } catch (err) {
- if (ownsConnection) await conn.rollback();
- throw err;
- } finally {
- if (ownsConnection) conn.release();
- }
- };
- const getBillingState = async (agentSessionId) => {
- const [rows] = await pool2.query(
- `SELECT agent_session_id, user_id, last_accumulated_cost, last_input_tokens,
- last_output_tokens, updated_at
- FROM h5_session_billing_state
- WHERE agent_session_id = ?`,
- [agentSessionId]
- );
- const row = rows[0];
- if (!row) return null;
- return {
- agentSessionId: row.agent_session_id,
- userId: row.user_id,
- lastAccumulatedCost: row.last_accumulated_cost,
- lastInputTokens: Number(row.last_input_tokens ?? 0),
- lastOutputTokens: Number(row.last_output_tokens ?? 0),
- updatedAt: Number(row.updated_at)
- };
- };
- const billSessionUsage = async (userId, agentSessionId, tokenStateRaw, requestId = null) => {
- const user = await getUserById(userId);
- if (isAdminRole(user)) {
- return {
- ok: true,
- costCents: 0,
- balanceCents: Number(user?.balance_cents ?? 0),
- tokensUsed: Number(user?.tokens_used ?? 0),
- deltaInputTokens: 0,
- deltaOutputTokens: 0
- };
- }
- const tokenState = normalizeTokenState(tokenStateRaw);
- const config = loadBillingConfig();
- const normalizedRequestId = requestId ? String(requestId).trim() || null : null;
- const now = Date.now();
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- if (normalizedRequestId) {
- const [existingUsage] = await conn.query(
- `SELECT cost_cents FROM h5_usage_records WHERE request_id = ? LIMIT 1`,
- [normalizedRequestId]
- );
- if (existingUsage[0]) {
- const [walletRows] = await conn.query(
- `SELECT balance_cents, tokens_used FROM h5_user_wallets WHERE user_id = ?`,
- [userId]
- );
- await conn.commit();
- return {
- ok: true,
- costCents: 0,
- balanceCents: walletRows[0] ? Number(walletRows[0].balance_cents) : null,
- tokensUsed: walletRows[0] ? Number(walletRows[0].tokens_used ?? 0) : null,
- deltaInputTokens: 0,
- deltaOutputTokens: 0
- };
- }
- }
- await conn.query(
- `INSERT INTO h5_session_billing_state
- (agent_session_id, user_id, last_accumulated_cost, last_input_tokens, last_output_tokens, updated_at)
- VALUES (?, ?, NULL, 0, 0, ?)
- ON DUPLICATE KEY UPDATE agent_session_id = agent_session_id`,
- [agentSessionId, userId, now]
- );
- const [stateRows] = await conn.query(
- `SELECT last_accumulated_cost, last_input_tokens, last_output_tokens
- FROM h5_session_billing_state
- WHERE agent_session_id = ?
- FOR UPDATE`,
- [agentSessionId]
- );
- const stateRow = stateRows[0];
- const previous = stateRow ? {
- lastAccumulatedCost: stateRow.last_accumulated_cost,
- lastInputTokens: Number(stateRow.last_input_tokens ?? 0),
- lastOutputTokens: Number(stateRow.last_output_tokens ?? 0)
- } : null;
- if (previous && tokenState.accumulatedInputTokens <= Number(previous.lastInputTokens ?? 0) && tokenState.accumulatedOutputTokens <= Number(previous.lastOutputTokens ?? 0)) {
- const [walletRows] = await conn.query(
- `SELECT balance_cents, tokens_used FROM h5_user_wallets WHERE user_id = ?`,
- [userId]
- );
- await conn.commit();
- return {
- ok: true,
- costCents: 0,
- balanceCents: walletRows[0] ? Number(walletRows[0].balance_cents) : null,
- tokensUsed: walletRows[0] ? Number(walletRows[0].tokens_used ?? 0) : null,
- deltaInputTokens: 0,
- deltaOutputTokens: 0
- };
- }
- let costCents = computeDeltaCostCents(previous, tokenState, config);
- const deltaIn = Math.max(
- 0,
- tokenState.accumulatedInputTokens - Number(previous?.lastInputTokens ?? 0)
- );
- const deltaOut = Math.max(
- 0,
- tokenState.accumulatedOutputTokens - Number(previous?.lastOutputTokens ?? 0)
- );
- const deltaTokens = deltaIn + deltaOut;
- await conn.query(
- `INSERT INTO h5_session_billing_state
- (agent_session_id, user_id, last_accumulated_cost, last_input_tokens, last_output_tokens, updated_at)
- VALUES (?, ?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- last_accumulated_cost = VALUES(last_accumulated_cost),
- last_input_tokens = VALUES(last_input_tokens),
- last_output_tokens = VALUES(last_output_tokens),
- updated_at = VALUES(updated_at)`,
- [
- agentSessionId,
- userId,
- tokenState.accumulatedCost,
- tokenState.accumulatedInputTokens,
- tokenState.accumulatedOutputTokens,
- now
- ]
- );
- if (costCents > 0 && subscriptionService2) {
- const coverage = await subscriptionService2.consumeQuota(userId, deltaTokens, conn);
- if (coverage.fullyCovers) {
- costCents = 0;
- } else if (coverage.overageRate < 1) {
- costCents = Math.max(1, Math.ceil(costCents * coverage.overageRate));
- }
- }
- let balanceAfter = null;
- let tokensUsedAfter = null;
- if (costCents > 0) {
- const [walletRows] = await conn.query(
- `SELECT w.balance_cents, w.tokens_used, u.status
- FROM h5_user_wallets w
- JOIN h5_users u ON u.id = w.user_id
- WHERE w.user_id = ?
- FOR UPDATE`,
- [userId]
- );
- const wallet = walletRows[0];
- if (!wallet) {
- await conn.rollback();
- return { ok: false, message: "\u94B1\u5305\u4E0D\u5B58\u5728", costCents: 0 };
- }
- const currentBalance = Number(wallet.balance_cents ?? 0);
- const nextBalance = Math.max(0, currentBalance - costCents);
- balanceAfter = nextBalance;
- tokensUsedAfter = Number(wallet.tokens_used ?? 0) + deltaTokens;
- await conn.query(
- `UPDATE h5_user_wallets
- SET balance_cents = ?, tokens_used = tokens_used + ?, updated_at = ?
- WHERE user_id = ?`,
- [nextBalance, deltaTokens, now, userId]
- );
- await conn.query(
- `INSERT INTO h5_usage_records
- (user_id, agent_session_id, request_id, input_tokens, output_tokens, cost_cents, balance_after_cents, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- userId,
- agentSessionId,
- normalizedRequestId,
- deltaIn,
- deltaOut,
- costCents,
- nextBalance,
- now
- ]
- );
- await conn.query(
- `INSERT INTO h5_billing_ledger
- (user_id, type, amount_cents, tokens, session_id, note, operator_id, created_at)
- VALUES (?, 'deduct', ?, ?, ?, ?, NULL, ?)`,
- [
- userId,
- -costCents,
- deltaTokens,
- agentSessionId,
- `\u5BF9\u8BDD\u6263\u8D39 input=${deltaIn} output=${deltaOut}`,
- now
- ]
- );
- const lowBalanceGift = await grantLowBalanceGiftIfNeeded(conn, {
- userId,
- currentBalance,
- nextBalance,
- now
- });
- balanceAfter = lowBalanceGift.balanceAfter;
- if (balanceAfter <= 0) {
- await conn.query(`UPDATE h5_users SET status = 'suspended', updated_at = ? WHERE id = ?`, [
- now,
- userId
- ]);
- }
- } else {
- const user2 = await getUserById(userId);
- balanceAfter = user2 ? Number(user2.balance_cents) : null;
- tokensUsedAfter = user2 ? Number(user2.tokens_used ?? 0) : null;
- }
- await conn.commit();
- return {
- ok: true,
- costCents,
- balanceCents: balanceAfter,
- tokensUsed: tokensUsedAfter,
- deltaInputTokens: deltaIn,
- deltaOutputTokens: deltaOut
- };
- } catch (err) {
- await conn.rollback();
- throw err;
- } finally {
- conn.release();
- }
- };
- const listUsageRecords = async ({ userId = null, page = 1, pageSize = 20, limit = null } = {}) => {
- if (limit !== null) {
- const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
- const params2 = [];
- const where2 = userId ? "WHERE r.user_id = ?" : "";
- if (userId) params2.push(userId);
- const [rows2] = await pool2.query(
- `SELECT r.id, r.user_id, u.username, r.agent_session_id, r.request_id,
- r.input_tokens, r.output_tokens, r.cost_cents, r.balance_after_cents, r.created_at
- FROM h5_usage_records r JOIN h5_users u ON u.id = r.user_id
- ${where2} ORDER BY r.created_at DESC LIMIT ${safeLimit}`,
- params2
- );
- return rows2.map((row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, agentSessionId: row.agent_session_id, requestId: row.request_id, inputTokens: Number(row.input_tokens), outputTokens: Number(row.output_tokens), costCents: Number(row.cost_cents), balanceAfterCents: Number(row.balance_after_cents), createdAt: Number(row.created_at) }));
- }
- const safePageSize = Math.min(Math.max(Number(pageSize) || 50, 1), 200);
- const safePage = Math.max(Number(page) || 1, 1);
- const offset = (safePage - 1) * safePageSize;
- const params = [];
- const where = userId ? "WHERE r.user_id = ?" : "";
- if (userId) params.push(userId);
- const [[{ total }]] = await pool2.query(
- `SELECT COUNT(*) AS total FROM h5_usage_records r ${where}`,
- params
- );
- const [rows] = await pool2.query(
- `SELECT r.id, r.user_id, u.username, r.agent_session_id, r.request_id,
- r.input_tokens, r.output_tokens, r.cost_cents, r.balance_after_cents, r.created_at
- FROM h5_usage_records r JOIN h5_users u ON u.id = r.user_id
- ${where}
- ORDER BY r.created_at DESC
- LIMIT ${safePageSize} OFFSET ${offset}`,
- params
- );
- return {
- records: rows.map((row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, agentSessionId: row.agent_session_id, requestId: row.request_id, inputTokens: Number(row.input_tokens), outputTokens: Number(row.output_tokens), costCents: Number(row.cost_cents), balanceAfterCents: Number(row.balance_after_cents), createdAt: Number(row.created_at) })),
- total: Number(total),
- page: safePage,
- pageSize: safePageSize
- };
- };
- const listBillingLedger = async ({ userId = null, page = 1, pageSize = 20, limit = null, types: types2 = null } = {}) => {
- const buildWhere = (params) => {
- const clauses = [];
- if (userId) {
- clauses.push("l.user_id = ?");
- params.push(userId);
- }
- if (Array.isArray(types2) && types2.length) {
- clauses.push(`l.type IN (${types2.map(() => "?").join(", ")})`);
- params.push(...types2);
- }
- return clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
- };
- const mapRow = (row) => ({ id: Number(row.id), userId: row.user_id, username: row.username, type: row.type, amountCents: Number(row.amount_cents), tokens: Number(row.tokens), sessionId: row.session_id, note: row.note, createdAt: Number(row.created_at) });
- if (limit !== null) {
- const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
- const params = [];
- const where2 = buildWhere(params);
- const [rows2] = await pool2.query(`SELECT l.id, l.user_id, u.username, l.type, l.amount_cents, l.tokens, l.session_id, l.note, l.created_at FROM h5_billing_ledger l JOIN h5_users u ON u.id = l.user_id ${where2} ORDER BY l.created_at DESC LIMIT ${safeLimit}`, params);
- return rows2.map(mapRow);
- }
- const safePageSize = Math.min(Math.max(Number(pageSize) || 50, 1), 200);
- const safePage = Math.max(Number(page) || 1, 1);
- const offset = (safePage - 1) * safePageSize;
- const countParams = [];
- const where = buildWhere(countParams);
- const [[{ total }]] = await pool2.query(
- `SELECT COUNT(*) AS total FROM h5_billing_ledger l ${where}`,
- countParams
- );
- const dataParams = [];
- const dataWhere = buildWhere(dataParams);
- const [rows] = await pool2.query(
- `SELECT l.id, l.user_id, u.username, l.type, l.amount_cents, l.tokens,
- l.session_id, l.note, l.created_at
- FROM h5_billing_ledger l JOIN h5_users u ON u.id = l.user_id
- ${dataWhere}
- ORDER BY l.created_at DESC
- LIMIT ${safePageSize} OFFSET ${offset}`,
- dataParams
- );
- return { entries: rows.map(mapRow), total: Number(total), page: safePage, pageSize: safePageSize };
- };
- const getAdminSummary = async () => {
- const since24h = Date.now() - 24 * 60 * 60 * 1e3;
- const [userRows] = await pool2.query(
- `SELECT u.id, u.username, u.display_name, u.role, u.status,
- COALESCE(w.balance_cents, 0) AS balance_cents
- FROM h5_users u
- LEFT JOIN h5_user_wallets w ON w.user_id = u.id`
- );
- let total = 0;
- let active = 0;
- let lowBalance = 0;
- let totalBalanceCents = 0;
- const lowBalanceUsers = [];
- for (const row of userRows) {
- total += 1;
- if (row.status === "active") active += 1;
- const balanceCents = Number(row.balance_cents);
- totalBalanceCents += balanceCents;
- if (row.role === "user" && balanceCents <= 0) {
- lowBalance += 1;
- if (lowBalanceUsers.length < 8) {
- lowBalanceUsers.push({
- id: row.id,
- username: row.username,
- displayName: row.display_name,
- balanceCents
- });
- }
- }
- }
- const [[usage24h]] = await pool2.query(
- `SELECT COUNT(*) AS count, COALESCE(SUM(cost_cents), 0) AS cost_cents
- FROM h5_usage_records
- WHERE created_at >= ?`,
- [since24h]
- );
- const recentUsage = await listUsageRecords({ limit: 8 });
- const recentLedger = await listBillingLedger({ limit: 8 });
- return {
- users: { total, active, lowBalance, totalBalanceCents },
- usage24h: {
- count: Number(usage24h.count),
- costCents: Number(usage24h.cost_cents)
- },
- lowBalanceUsers,
- recentUsage,
- recentLedger
- };
- };
- const syncAdminPassword = async () => {
- const adminUsername = normalizeUsername(process.env.H5_ADMIN_USERNAME ?? "admin");
- const adminPassword = process.env.H5_ADMIN_PASSWORD;
- if (!adminPassword) return;
- const [rows] = await pool2.query(
- `SELECT id FROM h5_users WHERE username = ? AND role = 'admin' LIMIT 1`,
- [adminUsername]
- );
- if (rows.length === 0) return;
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(adminPassword);
- const now = Date.now();
- await pool2.query(
- `UPDATE h5_users SET salt = ?, password_hash = ?, password_algorithm = ?, updated_at = ? WHERE id = ?`,
- [salt, passwordHash, passwordAlgorithm, now, rows[0].id]
- );
- };
- const seedRoleCapabilityDefaults = async () => {
- const now = Date.now();
- for (const [key, allowed] of Object.entries(DEFAULT_USER_CAPABILITIES)) {
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('role', 'user', ?, ?, ?)
- ON DUPLICATE KEY UPDATE capability_key = capability_key`,
- [key, allowed ? 1 : 0, now]
- );
- }
- };
- const upgradeMemoryStoreCapability = async () => {
- const now = Date.now();
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('role', 'user', 'memory_store', 1, ?)
- ON DUPLICATE KEY UPDATE allowed = 1, updated_at = VALUES(updated_at)`,
- [now]
- );
- };
- const upgradeDefaultUserCapabilities = async () => {
- const now = Date.now();
- for (const key of ["skills", "chat_recall"]) {
- if (!DEFAULT_USER_CAPABILITIES[key]) continue;
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('role', 'user', ?, 1, ?)
- ON DUPLICATE KEY UPDATE allowed = 1, updated_at = VALUES(updated_at)`,
- [key, now]
- );
- }
- };
- const upgradeDefaultUserSkills = async () => {
- const now = Date.now();
- for (const [name, enabled] of Object.entries(DEFAULT_USER_SKILLS)) {
- if (!enabled) continue;
- await pool2.query(
- `INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
- VALUES ('role', 'user', ?, 1, ?)
- ON DUPLICATE KEY UPDATE enabled = 1, updated_at = VALUES(updated_at)`,
- [name, now]
- );
- }
- };
- const serializePolicyValue = (key, value) => {
- const def = POLICY_CATALOG.find((item) => item.key === key);
- if (def?.type === "boolean") return value ? "true" : "false";
- return String(value);
- };
- const parsePolicyValue = (key, raw) => {
- const def = POLICY_CATALOG.find((item) => item.key === key);
- if (def?.type === "boolean") return raw === "true" || raw === "1";
- return raw;
- };
- const seedRolePolicyDefaults = async () => {
- const now = Date.now();
- for (const [key, value] of Object.entries(DEFAULT_USER_POLICIES)) {
- await pool2.query(
- `INSERT INTO h5_user_policies (subject_type, subject_id, policy_key, policy_value, updated_at)
- VALUES ('role', 'user', ?, ?, ?)
- ON DUPLICATE KEY UPDATE policy_key = policy_key`,
- [key, serializePolicyValue(key, value), now]
- );
- }
- };
- const listPolicyEntries = async (subjectType, subjectId) => {
- const [rows] = await pool2.query(
- `SELECT policy_key, policy_value
- FROM h5_user_policies
- WHERE subject_type = ? AND subject_id = ?`,
- [subjectType, subjectId]
- );
- return Object.fromEntries(
- rows.map((row) => [row.policy_key, parsePolicyValue(row.policy_key, row.policy_value)])
- );
- };
- const resolveUserPolicies = async (user) => {
- if (!user || user.role === "admin") {
- return { unrestricted: true, policies: {} };
- }
- const roleDefaults = await listPolicyEntries("role", "user");
- const userOverrides = await listPolicyEntries("user", user.id);
- return {
- unrestricted: false,
- policies: resolvePolicies(roleDefaults, userOverrides)
- };
- };
- const listCapabilityGrants = async (subjectType, subjectId) => {
- const [rows] = await pool2.query(
- `SELECT capability_key, allowed
- FROM h5_capability_grants
- WHERE subject_type = ? AND subject_id = ?`,
- [subjectType, subjectId]
- );
- return Object.fromEntries(
- rows.map((row) => [row.capability_key, Boolean(row.allowed)])
- );
- };
- const resolveUserCapabilities = async (user) => {
- if (!user) return { unrestricted: true, capabilities: {} };
- if (user.role === "admin") {
- const skillMap2 = Object.fromEntries(skillCatalog.map((item) => [item.name, true]));
- return {
- unrestricted: true,
- capabilities: Object.fromEntries(catalogKeys().map((key) => [key, true])),
- skills: skillMap2,
- grantedSkills: grantedSkillNames(skillMap2)
- };
- }
- const roleDefaults = await listCapabilityGrants("role", "user");
- const userOverrides = await listCapabilityGrants("user", user.id);
- const capabilities = {};
- for (const key of catalogKeys()) {
- if (key in userOverrides) {
- capabilities[key] = userOverrides[key];
- } else if (key in roleDefaults) {
- capabilities[key] = roleDefaults[key];
- } else {
- capabilities[key] = DEFAULT_USER_CAPABILITIES[key] ?? false;
- }
- }
- const skillMap = await resolveUserSkillMap(user);
- const withSkills = applySkillGrantsToCapabilities(capabilities, skillMap);
- return {
- unrestricted: false,
- capabilities: clampUserCapabilities(withSkills),
- skills: skillMap,
- grantedSkills: grantedSkillNames(skillMap)
- };
- };
- const getAgentSessionPolicy = async (userId, { toolMode = "chat" } = {}) => {
- const user = await getUserById(userId);
- if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728");
- const capabilityState = await resolveUserCapabilities(user);
- const policyState = await resolveUserPolicies(user);
- await syncUserSkillsForUser(user);
- if (capabilityState.unrestricted) {
- return {
- ...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true, toolMode }),
- policies: {},
- unrestricted: true,
- toolMode
- };
- }
- const effectiveCapabilities = applyPoliciesToCapabilities(
- capabilityState.capabilities,
- policyState.policies
- );
- let sandboxMcp = null;
- if (effectiveCapabilities.static_publish || effectiveCapabilities.private_data_space) {
- try {
- const layout = await publishLayoutFor(user, { migrateLegacy: false });
- sandboxMcp = {
- // When goosed runs in a container its filesystem is split from the portal's,
- // so the host paths the portal would otherwise send (node binary, MCP script)
- // are not resolvable. These env overrides let the portal send container-canonical
- // paths instead. Unset (e.g. local dev, co-located native goosed) => fall back to
- // the portal's own paths, so behavior is unchanged.
- serverPath: resolveSandboxMcpServerPath(process.env.GOOSED_MCP_SERVER_PATH),
- sandboxRoot: resolveGoosedSandboxRoot(h5Root, user),
- userId: user.id,
- nodeExecPath: process.env.GOOSED_MCP_NODE_PATH
- };
- } catch (err) {
- console.warn("[getAgentSessionPolicy] sandbox MCP setup failed, falling back:", err?.message);
- }
- }
- return {
- ...buildAgentExtensionPolicy(effectiveCapabilities, {
- unrestricted: false,
- policies: policyState.policies,
- sandboxMcp,
- toolMode
- }),
- capabilities: effectiveCapabilities,
- policies: policyState.policies,
- unrestricted: false,
- toolMode
- };
- };
- const getCodeAgentSessionPolicy = async (userId) => getAgentSessionPolicy(userId, { toolMode: "code" });
- const getRoleCapabilities = async (role = "user") => {
- const roleDefaults = await listCapabilityGrants("role", role);
- const capabilities = {};
- for (const key of catalogKeys()) {
- capabilities[key] = key in roleDefaults ? roleDefaults[key] : DEFAULT_USER_CAPABILITIES[key] ?? false;
- }
- return { role, capabilities: clampUserCapabilities(capabilities) };
- };
- const setRoleCapabilities = async (role, patch) => {
- if (role !== "user") {
- return { ok: false, message: "\u4EC5\u652F\u6301\u914D\u7F6E\u666E\u901A\u7528\u6237\u89D2\u8272\u9ED8\u8BA4\u6743\u9650" };
- }
- const normalized = normalizeCapabilityPatch(patch);
- const now = Date.now();
- for (const [key, allowed] of Object.entries(normalized)) {
- const effectiveAllowed = USER_NON_GRANTABLE_CAPABILITIES.has(key) ? false : allowed;
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('role', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE allowed = VALUES(allowed), updated_at = VALUES(updated_at)`,
- [role, key, effectiveAllowed ? 1 : 0, now]
- );
- }
- return { ok: true, ...await getRoleCapabilities(role) };
- };
- const getUserCapabilities = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const resolved = await resolveUserCapabilities(user);
- const overrides = await listCapabilityGrants("user", userId);
- return {
- ok: true,
- userId,
- role: user.role,
- unrestricted: resolved.unrestricted,
- capabilities: resolved.capabilities,
- overrides
- };
- };
- const setUserCapabilities = async (userId, patch) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.role === "admin") {
- return { ok: false, message: "\u7BA1\u7406\u5458\u4E0D\u53D7\u80FD\u529B\u9650\u5236" };
- }
- const normalized = normalizeCapabilityPatch(patch);
- const now = Date.now();
- for (const [key, allowed] of Object.entries(normalized)) {
- if (!isValidCapabilityKey(key)) continue;
- const effectiveAllowed = USER_NON_GRANTABLE_CAPABILITIES.has(key) ? false : allowed;
- await pool2.query(
- `INSERT INTO h5_capability_grants (subject_type, subject_id, capability_key, allowed, updated_at)
- VALUES ('user', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE allowed = VALUES(allowed), updated_at = VALUES(updated_at)`,
- [userId, key, effectiveAllowed ? 1 : 0, now]
- );
- }
- return getUserCapabilities(userId);
- };
- const clearUserCapabilityOverrides = async (userId) => {
- await pool2.query(
- `DELETE FROM h5_capability_grants WHERE subject_type = 'user' AND subject_id = ?`,
- [userId]
- );
- return getUserCapabilities(userId);
- };
- const getRolePolicies = async (role = "user") => {
- const roleDefaults = await listPolicyEntries("role", role);
- const policies = resolvePolicies(roleDefaults, {});
- return { role, policies };
- };
- const setRolePolicies = async (role, patch) => {
- if (role !== "user") {
- return { ok: false, message: "\u4EC5\u652F\u6301\u914D\u7F6E\u666E\u901A\u7528\u6237\u89D2\u8272\u9ED8\u8BA4\u7B56\u7565" };
- }
- const normalized = normalizePolicyPatch(patch);
- const now = Date.now();
- for (const [key, value] of Object.entries(normalized)) {
- await pool2.query(
- `INSERT INTO h5_user_policies (subject_type, subject_id, policy_key, policy_value, updated_at)
- VALUES ('role', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE policy_value = VALUES(policy_value), updated_at = VALUES(updated_at)`,
- [role, key, serializePolicyValue(key, value), now]
- );
- }
- return { ok: true, ...await getRolePolicies(role) };
- };
- const getUserPolicies = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const policyState = await resolveUserPolicies(user);
- const overrides = await listPolicyEntries("user", userId);
- return {
- ok: true,
- userId,
- role: user.role,
- unrestricted: policyState.unrestricted,
- policies: policyState.policies,
- overrides
- };
- };
- const setUserPolicies = async (userId, patch) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.role === "admin") {
- return { ok: false, message: "\u7BA1\u7406\u5458\u4E0D\u53D7\u7B56\u7565\u9650\u5236" };
- }
- const normalized = normalizePolicyPatch(patch);
- const now = Date.now();
- for (const [key, value] of Object.entries(normalized)) {
- if (!policyKeys().includes(key)) continue;
- await pool2.query(
- `INSERT INTO h5_user_policies (subject_type, subject_id, policy_key, policy_value, updated_at)
- VALUES ('user', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE policy_value = VALUES(policy_value), updated_at = VALUES(updated_at)`,
- [userId, key, serializePolicyValue(key, value), now]
- );
- }
- return getUserPolicies(userId);
- };
- const clearUserPolicyOverrides = async (userId) => {
- await pool2.query(`DELETE FROM h5_user_policies WHERE subject_type = 'user' AND subject_id = ?`, [
- userId
- ]);
- return getUserPolicies(userId);
- };
- const getRoleSkills = async (role = "user") => {
- const roleDefaults = await listSkillGrants("role", role);
- const skills = resolveSkillMap(roleDefaults, {}, skillCatalog);
- return { role, skills };
- };
- const setRoleSkills = async (role, patch) => {
- if (role !== "user") {
- return { ok: false, message: "\u4EC5\u652F\u6301\u914D\u7F6E\u666E\u901A\u7528\u6237\u89D2\u8272\u9ED8\u8BA4\u6280\u80FD" };
- }
- const normalized = normalizeSkillPatch(skillCatalog, patch);
- const now = Date.now();
- for (const [name, enabled] of Object.entries(normalized)) {
- await pool2.query(
- `INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
- VALUES ('role', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), updated_at = VALUES(updated_at)`,
- [role, name, enabled ? 1 : 0, now]
- );
- }
- const [users] = await pool2.query(`SELECT id, username, role, workspace_root FROM h5_users WHERE role = 'user'`);
- for (const row of users) {
- await syncUserSkillsForUser(row);
- }
- return { ok: true, ...await getRoleSkills(role) };
- };
- const getUserSkills = async (userId) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- const skills = await resolveUserSkillMap(user);
- const overrides = await listSkillGrants("user", userId);
- return {
- ok: true,
- userId,
- role: user.role,
- skills,
- grantedSkills: grantedSkillNames(skills),
- overrides
- };
- };
- const setUserSkills = async (userId, patch) => {
- const user = await getUserById(userId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.role === "admin") {
- return { ok: false, message: "\u7BA1\u7406\u5458\u4E0D\u53D7\u6280\u80FD\u9650\u5236" };
- }
- const normalized = normalizeSkillPatch(skillCatalog, patch);
- const now = Date.now();
- for (const [name, enabled] of Object.entries(normalized)) {
- await pool2.query(
- `INSERT INTO h5_user_skill_grants (subject_type, subject_id, skill_name, enabled, updated_at)
- VALUES ('user', ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), updated_at = VALUES(updated_at)`,
- [userId, name, enabled ? 1 : 0, now]
- );
- }
- await syncUserSkillsForUser(user);
- return getUserSkills(userId);
- };
- const clearUserSkillOverrides = async (userId) => {
- await pool2.query(`DELETE FROM h5_user_skill_grants WHERE subject_type = 'user' AND subject_id = ?`, [
- userId
- ]);
- const user = await getUserById(userId);
- if (user) await syncUserSkillsForUser(user);
- return getUserSkills(userId);
- };
- const ensureAdminUser = async () => {
- const adminUsername = normalizeUsername(process.env.H5_ADMIN_USERNAME ?? "admin");
- const adminPassword = process.env.H5_ADMIN_PASSWORD;
- const [rows] = await pool2.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [
- adminUsername
- ]);
- if (rows.length === 0) {
- if (!adminPassword) return;
- await createUser({
- username: adminUsername,
- password: adminPassword,
- displayName: "\u7BA1\u7406\u5458",
- balanceCents: 999999999,
- role: "admin"
- });
- } else {
- const adminId = rows[0].id;
- const now = Date.now();
- const adminLayout = await publishLayoutFor({
- id: adminId,
- username: adminUsername,
- displayName: "\u7BA1\u7406\u5458"
- });
- await pool2.query(`UPDATE h5_users SET workspace_root = ?, updated_at = ? WHERE id = ?`, [
- adminLayout.publishDir,
- now,
- adminId
- ]);
- await pool2.query(`DELETE FROM h5_user_path_grants WHERE user_id = ?`, [adminId]);
- await pool2.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [adminId, adminLayout.publishDir]
- );
- await pool2.query(
- `UPDATE h5_user_wallets SET balance_cents = GREATEST(balance_cents, ?), updated_at = ? WHERE user_id = ?`,
- [999999999, now, adminId]
- );
- }
- if (adminPassword) {
- await syncAdminPassword();
- }
- await seedRoleCapabilityDefaults();
- await upgradeMemoryStoreCapability();
- await upgradeDefaultUserCapabilities();
- await seedRolePolicyDefaults();
- await seedRoleSkillDefaults();
- await upgradeDefaultUserSkills();
- await repairAllUserPublishDirs();
- };
- const PENDING_BIND_TTL_MS = 15 * 60 * 1e3;
- const issueUserSession = async (userId, role, now = Date.now()) => {
- const token = crypto6.randomBytes(32).toString("base64url");
- await storeSession(userId, role, token, now);
- return token;
- };
- const findBindingByOpenid = async (appId, openid) => {
- const [rows] = await pool2.query(
- `SELECT wi.user_id, u.status
- FROM h5_user_wechat_identities wi
- JOIN h5_users u ON u.id = wi.user_id
- WHERE wi.app_id = ? AND wi.openid = ?
- LIMIT 1`,
- [appId, openid]
- );
- return rows[0] ?? null;
- };
- const findBindingByUnionid = async (unionid) => {
- if (!unionid) return null;
- const [rows] = await pool2.query(
- `SELECT wi.user_id, wi.app_id, u.status
- FROM h5_user_wechat_identities wi
- JOIN h5_users u ON u.id = wi.user_id
- WHERE wi.unionid = ?
- LIMIT 1`,
- [unionid]
- );
- return rows[0] ?? null;
- };
- const getWechatBindingForUser = async (userId, appId) => {
- const [rows] = await pool2.query(
- `SELECT id, nickname, avatar_url, last_login_at, created_at
- FROM h5_user_wechat_identities
- WHERE user_id = ? AND app_id = ?
- LIMIT 1`,
- [userId, appId]
- );
- return rows[0] ?? null;
- };
- const getWechatOpenidForUser = async (userId, appId) => {
- const [rows] = await pool2.query(
- `SELECT openid
- FROM h5_user_wechat_identities
- WHERE user_id = ? AND app_id = ?
- LIMIT 1`,
- [userId, appId]
- );
- return rows[0]?.openid ?? null;
- };
- const findWechatUserByOpenid = async (appId, openid) => {
- const [rows] = await pool2.query(
- `SELECT wi.user_id, wi.nickname, u.username, u.slug, u.display_name, u.status
- FROM h5_user_wechat_identities wi
- JOIN h5_users u ON u.id = wi.user_id
- WHERE wi.app_id = ? AND wi.openid = ?
- LIMIT 1`,
- [appId, openid]
- );
- return rows[0] ? {
- userId: rows[0].user_id,
- status: rows[0].status,
- nickname: rows[0].nickname,
- username: rows[0].username,
- slug: rows[0].slug,
- displayName: rows[0].display_name
- } : null;
- };
- const getWechatAgentRoute = async (appId, openid) => {
- const [rows] = await pool2.query(
- `SELECT id, user_id, agent_session_id, status, created_at, updated_at
- FROM h5_wechat_agent_routes
- WHERE app_id = ? AND openid = ?
- LIMIT 1`,
- [appId, openid]
- );
- const row = rows[0];
- if (!row) return null;
- return {
- id: row.id,
- userId: row.user_id,
- agentSessionId: row.agent_session_id,
- status: row.status,
- createdAt: Number(row.created_at ?? 0),
- updatedAt: Number(row.updated_at ?? 0)
- };
- };
- const upsertWechatAgentRoute = async ({
- userId,
- appId,
- openid,
- agentSessionId,
- status = "active",
- now = Date.now()
- }) => {
- const id = crypto6.randomUUID();
- await pool2.query(
- `INSERT INTO h5_wechat_agent_routes
- (id, user_id, app_id, openid, agent_session_id, status, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- ON DUPLICATE KEY UPDATE
- user_id = VALUES(user_id),
- agent_session_id = VALUES(agent_session_id),
- status = VALUES(status),
- updated_at = VALUES(updated_at)`,
- [id, userId, appId, openid, agentSessionId, status, now, now]
- );
- const route = await getWechatAgentRoute(appId, openid);
- return route?.id ?? id;
- };
- const clearWechatAgentRoute = async (appId, openid) => {
- await pool2.query(`DELETE FROM h5_wechat_agent_routes WHERE app_id = ? AND openid = ?`, [
- appId,
- openid
- ]);
- };
- const touchWechatAgentRoute = async (appId, openid, now = Date.now()) => {
- if (!appId || !openid) return;
- await pool2.query(
- `UPDATE h5_wechat_agent_routes SET updated_at = ? WHERE app_id = ? AND openid = ?`,
- [now, appId, openid]
- );
- };
- const recordWechatMpMessage = async ({
- appId,
- openid,
- msgId,
- now = Date.now()
- }) => {
- if (!appId || !openid || !msgId) return { inserted: true };
- const [result] = await pool2.query(
- `INSERT IGNORE INTO h5_wechat_mp_messages
- (app_id, openid, msg_id, status, created_at, updated_at)
- VALUES (?, ?, ?, 'processing', ?, ?)`,
- [appId, openid, String(msgId), now, now]
- );
- if (Number(result?.affectedRows ?? 0) > 0) return { inserted: true };
- const retryCutoff = now - 10 * 60 * 1e3;
- const [retryResult] = await pool2.query(
- `UPDATE h5_wechat_mp_messages
- SET status = 'processing', agent_session_id = NULL, updated_at = ?
- WHERE app_id = ? AND openid = ? AND msg_id = ?
- AND (status = 'failed' OR (status = 'processing' AND updated_at < ?))`,
- [now, appId, openid, String(msgId), retryCutoff]
- );
- return {
- inserted: Number(retryResult?.affectedRows ?? 0) > 0,
- duplicate: Number(retryResult?.affectedRows ?? 0) === 0
- };
- };
- const finishWechatMpMessage = async ({
- appId,
- openid,
- msgId,
- status = "done",
- agentSessionId = null,
- now = Date.now()
- }) => {
- if (!appId || !openid || !msgId) return;
- const safeStatus = status === "failed" ? "failed" : "done";
- await pool2.query(
- `UPDATE h5_wechat_mp_messages
- SET status = ?, agent_session_id = COALESCE(?, agent_session_id), updated_at = ?
- WHERE app_id = ? AND openid = ? AND msg_id = ?`,
- [safeStatus, agentSessionId, now, appId, openid, String(msgId)]
- );
- };
- const insertWechatMpMessageDetail = async ({
- appId,
- openid,
- userId = null,
- msgId = null,
- msgType,
- displayText = "",
- agentText = "",
- mediaId = null,
- mediaUrl = null,
- mediaPublicUrl = null,
- mediaFormat = null,
- locationLat = null,
- locationLng = null,
- locationLabel = null,
- linkUrl = null,
- linkTitle = null,
- rawXmlHash = null,
- rawJson = null,
- now = Date.now()
- }) => {
- if (!appId || !openid || !msgType) return null;
- const id = crypto6.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,
- media_id, media_url, media_public_url, media_format,
- location_lat, location_lng, location_label,
- link_url, link_title, raw_xml_hash, raw_json, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- id,
- appId,
- openid,
- userId,
- msgId ? String(msgId) : null,
- String(msgType),
- displayText || null,
- agentText || null,
- mediaId,
- mediaUrl,
- mediaPublicUrl,
- mediaFormat,
- locationLat,
- locationLng,
- locationLabel,
- linkUrl,
- linkTitle,
- rawXmlHash,
- rawJson ? JSON.stringify(rawJson) : null,
- now
- ]
- );
- return id;
- };
- const bindWechatToUser = async ({
- userId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now = Date.now()
- }) => {
- const existingOpenid = await findBindingByOpenid(appId, openid);
- if (existingOpenid && existingOpenid.user_id !== userId) {
- return { ok: false, message: "\u8BE5\u5FAE\u4FE1\u5DF2\u7ED1\u5B9A\u5176\u4ED6\u8D26\u53F7\uFF0C\u8BF7\u5148\u7528\u8BE5\u8D26\u53F7\u767B\u5F55" };
- }
- const existingUserBind = await getWechatBindingForUser(userId, appId);
- if (existingUserBind) {
- return { ok: false, message: "\u4F60\u7684\u8D26\u53F7\u5DF2\u7ED1\u5B9A\u5176\u4ED6\u5FAE\u4FE1\uFF0C\u9700\u5148\u89E3\u7ED1\u540E\u518D\u8BD5" };
- }
- try {
- await pool2.query(
- `INSERT INTO h5_user_wechat_identities
- (id, user_id, app_id, openid, unionid, nickname, avatar_url, last_login_at, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- crypto6.randomUUID(),
- userId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now,
- now,
- now
- ]
- );
- return { ok: true };
- } catch (err) {
- if (err?.code === "ER_DUP_ENTRY") {
- return { ok: false, message: "\u5FAE\u4FE1\u7ED1\u5B9A\u51B2\u7A81\uFF0C\u8BF7\u91CD\u8BD5" };
- }
- throw err;
- }
- };
- const touchWechatIdentity = async ({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now = Date.now()
- }) => {
- await pool2.query(
- `UPDATE h5_user_wechat_identities
- SET nickname = COALESCE(?, nickname),
- avatar_url = COALESCE(?, avatar_url),
- unionid = COALESCE(?, unionid),
- last_login_at = ?,
- updated_at = ?
- WHERE app_id = ? AND openid = ?`,
- [nickname, avatarUrl, unionid, now, now, appId, openid]
- );
- };
- const pruneWechatPendingBinds = async (now = Date.now()) => {
- await pool2.query(`DELETE FROM h5_wechat_pending_binds WHERE expires_at <= ?`, [now]);
- };
- const createWechatPendingBind = async ({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- returnTo = "/",
- utmSource = null,
- utmMedium = null,
- utmCampaign = null,
- now = Date.now()
- }) => {
- await pruneWechatPendingBinds(now);
- const token = crypto6.randomBytes(24).toString("base64url");
- await pool2.query(
- `INSERT INTO h5_wechat_pending_binds
- (token, app_id, openid, unionid, nickname, avatar_url, return_to,
- utm_source, utm_medium, utm_campaign, expires_at, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- token,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- returnTo,
- utmSource,
- utmMedium,
- utmCampaign,
- now + PENDING_BIND_TTL_MS,
- now
- ]
- );
- return token;
- };
- const getWechatPendingBind = async (token, now = Date.now()) => {
- if (!token) return null;
- await pruneWechatPendingBinds(now);
- const [rows] = await pool2.query(
- `SELECT token, app_id, openid, unionid, nickname, avatar_url, return_to,
- utm_source, utm_medium, utm_campaign, expires_at
- FROM h5_wechat_pending_binds
- WHERE token = ?
- LIMIT 1`,
- [token]
- );
- const row = rows[0];
- if (!row || Number(row.expires_at) <= now) return null;
- return row;
- };
- const consumeWechatPendingBind = async (token) => {
- await pool2.query(`DELETE FROM h5_wechat_pending_binds WHERE token = ?`, [token]);
- };
- const loginBoundWechatUser = async ({
- userId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now = Date.now()
- }) => {
- await touchWechatIdentity({ appId, openid, unionid, nickname, avatarUrl, now });
- const user = await getUserById(userId);
- if (!user || user.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- const token = await issueUserSession(user.id, user.role, now);
- return { ok: true, token, user: publicUser(user), isNewUser: false };
- };
- const generateWechatUsername = async (openid) => {
- const cleaned = String(openid).replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
- const suffix = cleaned.slice(-8) || crypto6.randomBytes(4).toString("hex");
- let candidate = `wx_${suffix}`.slice(0, 32);
- if (!isValidUsername(candidate)) {
- candidate = `wx_${crypto6.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))}${crypto6.randomBytes(2).toString("hex")}`.slice(
- 0,
- 32
- );
- }
- return `wx_${crypto6.randomBytes(6).toString("hex")}`.slice(0, 32);
- };
- const registerViaWechat = async ({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now = Date.now()
- }) => {
- const normalized = await generateWechatUsername(openid);
- const randomPassword = crypto6.randomBytes(24).toString("base64url");
- const { salt, passwordHash, passwordAlgorithm } = createPasswordRecord(randomPassword);
- const userId = crypto6.randomUUID();
- const layout = await publishLayoutFor({ id: userId, username: normalized });
- const workspaceRoot = layout.publishDir;
- const displayName = nickname?.trim() || "\u5FAE\u4FE1\u7528\u6237";
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- await conn.query(
- `INSERT INTO h5_users
- (id, username, slug, email, display_name, salt, password_hash, password_algorithm,
- role, status, plan_type, workspace_root, signup_source, low_balance_gift_eligible,
- low_balance_gift_granted_at, created_at, updated_at)
- VALUES (?, ?, ?, NULL, ?, ?, ?, ?, 'user', 'active', 'free', ?, 'wechat', 1, NULL, ?, ?)`,
- [
- userId,
- normalized,
- normalized,
- displayName,
- salt,
- passwordHash,
- passwordAlgorithm,
- workspaceRoot,
- now,
- now
- ]
- );
- await conn.query(
- `INSERT INTO h5_user_wallets (user_id, balance_cents, tokens_used, updated_at)
- VALUES (?, ?, 0, ?)`,
- [userId, defaultSignupBalanceCents, now]
- );
- await recordSignupBonus(conn, userId, defaultSignupBalanceCents, now);
- await conn.query(
- `INSERT INTO h5_user_path_grants (user_id, path, mode) VALUES (?, ?, 'readwrite')`,
- [userId, workspaceRoot]
- );
- await conn.query(
- `INSERT INTO h5_user_wechat_identities
- (id, user_id, app_id, openid, unionid, nickname, avatar_url, last_login_at, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- crypto6.randomUUID(),
- userId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now,
- now,
- now
- ]
- );
- await initializeDefaultSpace(conn, userId, {
- quotaBytes: Number(process.env.MINDSPACE_FREE_QUOTA_BYTES ?? 5 * 1024 * 1024),
- now
- });
- await conn.commit();
- ensureWorkspace(workspaceRoot);
- ensureUserMemoryProfile(workspaceRoot, {
- userId,
- displayName,
- username: normalized,
- slug: normalized
- });
- if (subscriptionService2) {
- subscriptionService2.grantSubscription(userId, "free", null, null, "\u6CE8\u518C\u8D60\u9001\u514D\u8D39\u5957\u9910").catch(() => {
- });
- }
- const user = await getUserById(userId);
- return { ok: true, user: publicUser(user) };
- } catch (err) {
- await conn.rollback();
- if (err?.code === "ER_DUP_ENTRY") {
- return { ok: false, message: "\u5FAE\u4FE1\u8D26\u53F7\u6CE8\u518C\u51B2\u7A81\uFF0C\u8BF7\u91CD\u8BD5" };
- }
- throw err;
- } finally {
- conn.release();
- }
- };
- const resolveWechatAuth = async ({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- intent = "login",
- bindUserId = null,
- returnTo = "/",
- utmSource = null,
- utmMedium = null,
- utmCampaign = null,
- now = Date.now()
- }) => {
- let binding = await findBindingByOpenid(appId, openid);
- if (!binding && unionid) {
- const unionBinding = await findBindingByUnionid(unionid);
- if (unionBinding) {
- const linked = await bindWechatToUser({
- userId: unionBinding.user_id,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now
- });
- if (!linked.ok) return linked;
- binding = { user_id: unionBinding.user_id, status: unionBinding.status };
- }
- }
- if (binding) {
- if (binding.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- return loginBoundWechatUser({
- userId: binding.user_id,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now
- }).then((result) => result.ok ? { ...result, action: "login" } : result);
- }
- if (intent === "bind" && bindUserId) {
- const user = await getUserById(bindUserId);
- if (!user) return { ok: false, message: "\u7528\u6237\u4E0D\u5B58\u5728" };
- if (user.status === "disabled") {
- return { ok: false, message: "\u8D26\u6237\u5DF2\u7981\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458" };
- }
- const bound = await bindWechatToUser({
- userId: bindUserId,
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now
- });
- if (!bound.ok) return bound;
- const token = await issueUserSession(user.id, user.role, now);
- return {
- ok: true,
- action: "login",
- token,
- user: publicUser(user),
- isNewUser: false,
- bound: true
- };
- }
- if (intent === "register") {
- const registered = await registerViaWechat({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- now
- });
- if (!registered.ok) return registered;
- const token = await issueUserSession(registered.user.id, registered.user.role, now);
- return {
- ok: true,
- action: "login",
- token,
- user: registered.user,
- isNewUser: true
- };
- }
- const pendingToken = await createWechatPendingBind({
- appId,
- openid,
- unionid,
- nickname,
- avatarUrl,
- returnTo,
- utmSource,
- utmMedium,
- utmCampaign,
- now
- });
- return {
- ok: true,
- action: "binding_gate",
- pendingToken,
- wechatProfile: {
- nickname: nickname ?? null,
- avatarUrl: avatarUrl ?? null
- },
- returnTo,
- utmSource,
- utmMedium,
- utmCampaign
- };
- };
- const completeWechatRegister = async ({ pendingToken, now = Date.now() }) => {
- const pending = await getWechatPendingBind(pendingToken, now);
- if (!pending) {
- return { ok: false, message: "\u7ED1\u5B9A\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5FAE\u4FE1\u767B\u5F55" };
- }
- const registered = await registerViaWechat({
- appId: pending.app_id,
- openid: pending.openid,
- unionid: pending.unionid,
- nickname: pending.nickname,
- avatarUrl: pending.avatar_url,
- now
- });
- if (!registered.ok) return registered;
- await consumeWechatPendingBind(pendingToken);
- const token = await issueUserSession(registered.user.id, registered.user.role, now);
- return {
- ok: true,
- token,
- user: registered.user,
- isNewUser: true,
- returnTo: pending.return_to || "/",
- utmSource: pending.utm_source,
- utmMedium: pending.utm_medium,
- utmCampaign: pending.utm_campaign
- };
- };
- const completeWechatBindAccount = async ({
- pendingToken,
- username,
- password,
- ip = "unknown",
- now = Date.now()
- }) => {
- const pending = await getWechatPendingBind(pendingToken, now);
- if (!pending) {
- return { ok: false, message: "\u7ED1\u5B9A\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5FAE\u4FE1\u767B\u5F55" };
- }
- const loginResult = await login({ username, password, ip, now });
- if (!loginResult.ok) return loginResult;
- const bound = await bindWechatToUser({
- userId: loginResult.user.id,
- appId: pending.app_id,
- openid: pending.openid,
- unionid: pending.unionid,
- nickname: pending.nickname,
- avatarUrl: pending.avatar_url,
- now
- });
- if (!bound.ok) return bound;
- await consumeWechatPendingBind(pendingToken);
- return {
- ok: true,
- token: loginResult.token,
- user: loginResult.user,
- isNewUser: false,
- bound: true,
- returnTo: pending.return_to || "/"
- };
- };
- const getWechatBindingStatus = async (userId, appId) => {
- const row = await getWechatBindingForUser(userId, appId);
- if (!row) return { bound: false };
- return {
- bound: true,
- nickname: row.nickname,
- avatarUrl: row.avatar_url,
- lastLoginAt: Number(row.last_login_at),
- boundAt: Number(row.created_at)
- };
- };
- const loginByWechat = async (params) => {
- const result = await resolveWechatAuth({ ...params, intent: "login" });
- if (!result.ok) return result;
- if (result.action === "binding_gate") {
- return { ok: false, message: "\u9700\u8981\u5B8C\u6210\u8D26\u53F7\u7ED1\u5B9A" };
- }
- return result;
- };
- return {
- USER_COOKIE,
- register,
- login,
- loginByWechat,
- resolveWechatAuth,
- completeWechatRegister,
- completeWechatBindAccount,
- getWechatPendingBind,
- getWechatBindingStatus,
- getWechatOpenidForUser,
- setRechargeNotifier(callback) {
- rechargeNotifier = typeof callback === "function" ? callback : null;
- },
- findWechatUserByOpenid,
- getWechatAgentRoute,
- upsertWechatAgentRoute,
- clearWechatAgentRoute,
- touchWechatAgentRoute,
- recordWechatMpMessage,
- finishWechatMpMessage,
- insertWechatMpMessageDetail,
- resetPassword,
- verify,
- revoke,
- revokeAllSessionsForUser,
- getMe,
- listPathGrants,
- resolveWorkingDir,
- getUserPublishLayout,
- isPathAllowed,
- repairAllUserPublishDirs,
- registerAgentSession,
- getSessionNode,
- getSessionTarget,
- unregisterAgentSession,
- ownsSession,
- listOwnedSessionIds,
- setSessionOrigin,
- getSessionOrigins,
- canUseChat,
- getUserById,
- getUserPublic,
- listUsers,
- createUser,
- updateUser,
- purchaseSpaceQuota,
- recharge,
- billSessionUsage,
- listUsageRecords,
- listBillingLedger,
- getAdminSummary,
- ensureAdminUser,
- seedRoleCapabilityDefaults,
- resolveUserCapabilities,
- getAgentSessionPolicy,
- getCodeAgentSessionPolicy,
- getRoleCapabilities,
- setRoleCapabilities,
- getUserCapabilities,
- setUserCapabilities,
- clearUserCapabilityOverrides,
- resolveUserPolicies,
- getRolePolicies,
- setRolePolicies,
- getUserPolicies,
- setUserPolicies,
- clearUserPolicyOverrides,
- getRoleSkills,
- setRoleSkills,
- getUserSkills,
- setUserSkills,
- clearUserSkillOverrides,
- syncUserSkillsForUser,
- capabilityCatalog: CAPABILITY_CATALOG,
- policyCatalog: POLICY_CATALOG,
- skillCatalog,
- publicUser
- };
-}
-function buildUserSessionCookie(token, secure, { domain, maxAge }) {
- const parts = [
- `${USER_COOKIE}=${encodeURIComponent(token)}`,
- "Path=/",
- "HttpOnly",
- "SameSite=Lax",
- `Max-Age=${maxAge}`
- ];
- if (domain) parts.push(`Domain=${domain}`);
- if (secure) parts.push("Secure");
- return parts.join("; ");
-}
-function userSessionCookie(token, secure, domain = resolveCookieDomain()) {
- return buildUserSessionCookie(token, secure, {
- domain,
- maxAge: 7 * 24 * 60 * 60
- });
-}
-function clearUserSessionCookie(secure, domain = resolveCookieDomain()) {
- return buildUserSessionCookie("", secure, { domain, maxAge: 0 });
-}
-function userLoginCookies(token, secure, domain = resolveCookieDomain()) {
- const cookies = [userSessionCookie(token, secure, domain)];
- if (domain) {
- cookies.push(clearUserSessionCookie(secure, null));
- }
- return cookies;
-}
-function resolveCookieDomain() {
- const explicit = String(process.env.H5_COOKIE_DOMAIN ?? "").trim();
- if (explicit) return explicit;
- try {
- const base = String(process.env.H5_PUBLIC_BASE_URL ?? "").trim();
- if (!base) return null;
- const hostname = new URL(base).hostname.toLowerCase();
- if (hostname === "localhost" || hostname.endsWith(".localhost")) {
- return ".localhost";
- }
- if (hostname === "tkmind.cn" || hostname.endsWith(".tkmind.cn")) {
- return ".tkmind.cn";
- }
- } catch {
- }
- return null;
-}
-function resolveCookieDomainForRequest(req) {
- const explicit = String(process.env.H5_COOKIE_DOMAIN ?? "").trim();
- if (explicit) return explicit;
- const hostCandidates = [
- req?.get?.("x-forwarded-host"),
- req?.get?.("host"),
- req?.hostname
- ];
- const origin = req?.get?.("origin");
- if (origin) {
- try {
- hostCandidates.push(new URL(origin).host);
- } catch {
- }
- }
- for (const raw of hostCandidates) {
- const hostname2 = String(raw ?? "").split(":")[0].toLowerCase();
- if (hostname2.endsWith(".localhost")) {
- return ".localhost";
- }
- if (hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || net.isIP(hostname2)) {
- return null;
- }
- }
- const hostname = String(req?.get?.("host") ?? req?.hostname ?? "").split(":")[0].toLowerCase();
- if (!hostname || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || net.isIP(hostname)) {
- return null;
- }
- return resolveCookieDomain();
-}
-
-// wiki-auth.mjs
-import crypto7 from "node:crypto";
-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 = path11.join(DB_DIR, "users.json");
- PAGES_DIR = path11.join(DB_DIR, "pages");
-}
-function ensureDb() {
- 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(fs10.readFileSync(USERS_FILE, "utf-8"));
-}
-function writeUsers(data) {
- ensureDb();
- fs10.writeFileSync(USERS_FILE, JSON.stringify(data, null, 2), "utf-8");
-}
-function safeEqual3(a, b) {
- const ba = Buffer.from(a);
- const bb = Buffer.from(b);
- return ba.length === bb.length && crypto7.timingSafeEqual(ba, bb);
-}
-function createWikiAuth(dataDir) {
- initPaths(dataDir);
- ensureDb();
- const sessions = /* @__PURE__ */ new Map();
- const COOKIE_NAME = "wiki_session";
- function hashPassword2(password, salt) {
- return crypto7.pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
- }
- function register(username, password, displayName) {
- const db = readUsers();
- if (db.users.find((u) => u.username === username)) {
- return { ok: false, message: "\u7528\u6237\u540D\u5DF2\u5B58\u5728" };
- }
- const salt = crypto7.randomBytes(16).toString("hex");
- const hashed = hashPassword2(password, salt);
- const user = {
- id: crypto7.randomUUID(),
- username,
- displayName: displayName || username,
- salt,
- hashedPassword: hashed,
- createdAt: Date.now()
- };
- db.users.push(user);
- writeUsers(db);
- return { ok: true, user: { id: user.id, username: user.username, displayName: user.displayName } };
- }
- function login(username, password) {
- const db = readUsers();
- const user = db.users.find((u) => u.username === username);
- if (!user) return { ok: false, message: "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF" };
- const hashed = hashPassword2(password, user.salt);
- if (!safeEqual3(hashed, user.hashedPassword)) {
- return { ok: false, message: "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF" };
- }
- const token = crypto7.randomBytes(32).toString("base64url");
- sessions.set(token, { userId: user.id, username: user.username, expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1e3 });
- return { ok: true, token, user: { id: user.id, username: user.username, displayName: user.displayName } };
- }
- function verify(token) {
- if (!token) return null;
- const session = sessions.get(token);
- if (!session || session.expiresAt < Date.now()) {
- sessions.delete(token);
- return null;
- }
- session.expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1e3;
- return session;
- }
- function revoke(token) {
- sessions.delete(token);
- }
- function getUser(username) {
- const db = readUsers();
- const user = db.users.find((u) => u.username === username);
- if (!user) return null;
- return { id: user.id, username: user.username, displayName: user.displayName, createdAt: user.createdAt };
- }
- function getAllUsers() {
- const db = readUsers();
- return db.users.map((u) => ({ id: u.id, username: u.username, displayName: u.displayName, createdAt: u.createdAt }));
- }
- function listPages(username) {
- ensureDb();
- 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,
- slug: data.slug,
- updatedAt: data.updatedAt,
- createdAt: data.createdAt,
- tags: data.tags || []
- };
- }).sort((a, b) => b.updatedAt - a.updatedAt);
- }
- function getPage(username, slug) {
- ensureDb();
- 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 = 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,
- title: title || slug,
- content: content || "",
- tags: tags || [],
- username,
- createdAt: existing?.createdAt || Date.now(),
- updatedAt: Date.now()
- };
- fs10.writeFileSync(filePath, JSON.stringify(page, null, 2), "utf-8");
- return page;
- }
- function deletePage(username, slug) {
- ensureDb();
- const filePath = path11.join(PAGES_DIR, username, `${slug}.json`);
- if (fs10.existsSync(filePath)) {
- fs10.unlinkSync(filePath);
- return true;
- }
- return false;
- }
- function searchPages(username, query) {
- const pages = listPages(username);
- const q = query.toLowerCase();
- return pages.filter(
- (p) => p.title.toLowerCase().includes(q) || p.tags.some((t) => t.toLowerCase().includes(q))
- );
- }
- return {
- COOKIE_NAME,
- register,
- login,
- verify,
- revoke,
- getUser,
- getAllUsers,
- listPages,
- getPage,
- savePage,
- deletePage,
- searchPages
- };
-}
-
-// scripts/local-test-config.mjs
-var LOCAL_IP = process.env.LOCAL_TEST_IP ?? "127.0.0.1";
-var DNS_PORT = Number(process.env.LOCAL_TEST_DNS_PORT ?? 5533);
-var H5_HOST = process.env.H5_LOCAL_HOST ?? "h5.localhost";
-var ADMIN_HOST = process.env.ADMIN_LOCAL_HOST ?? "adm.localhost";
-var PLAZA_HOST = process.env.PLAZA_LOCAL_HOST ?? "pla.localhost";
-var OPS_HOST = process.env.OPS_LOCAL_HOST ?? "ops.localhost";
-var TEST_HOSTS = [H5_HOST, ADMIN_HOST, PLAZA_HOST, OPS_HOST];
-var USES_LOCALHOST = TEST_HOSTS.every((host) => host === "localhost" || host.endsWith(".localhost"));
-var PUBLIC_SCHEME = (process.env.LOCAL_TEST_SCHEME ?? (String(process.env.LOCAL_TEST_HTTPS ?? "1") === "0" ? "http" : "https")).replace(/:$/, "");
-var PUBLIC_PORT = Number(
- process.env.LOCAL_TEST_PUBLIC_PORT ?? (USES_LOCALHOST ? 8443 : PUBLIC_SCHEME === "https" ? 443 : 80)
-);
-function publicUrl(host, { path: path35 = "" } = {}) {
- const defaultPort = PUBLIC_SCHEME === "https" ? 443 : 80;
- const portSuffix = PUBLIC_PORT === defaultPort ? "" : `:${PUBLIC_PORT}`;
- 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(/\/$/, "");
-var ADMIN_PUBLIC_BASE = publicUrl(ADMIN_HOST).replace(/\/$/, "");
-var PLAZA_PUBLIC_BASE = (process.env.PLAZA_PUBLIC_BASE ?? publicUrl(PLAZA_HOST)).replace(/\/$/, "");
-var OPS_PUBLIC_BASE = publicUrl(OPS_HOST, { path: "/ops/" }).replace(/\/$/, "") + "/";
-function isLocalDevHostname(hostname) {
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".localhost");
-}
-
-// mindspace-workspace-thumbnails.mjs
-import fs12 from "node:fs";
-import fsPromises from "node:fs/promises";
-import path13 from "node:path";
-
-// mindspace-thumbnails.mjs
-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;
-var REMOTE_COVER_TIMEOUT_MS = 8e3;
-function escapeXml(value) {
- return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
-}
-function titleFromHtml(html) {
- const match = String(html).match(/]*>([^<]+)<\/title>/i);
- return match?.[1]?.trim() ?? "";
-}
-function h1FromHtml(html) {
- const match = String(html).match(/]*>([\s\S]*?)<\/h1>/i);
- if (!match) return "";
- return match[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
-}
-function descriptionFromHtml(html) {
- const meta = String(html).match(/]+name=["']description["'][^>]+content=["']([^"']+)["']/i) ?? String(html).match(/]+content=["']([^"']+)["'][^>]+name=["']description["']/i);
- if (meta?.[1]) return meta[1].trim();
- const paragraph = String(html).match(/
]*>([^<]{4,120})/i);
- return paragraph?.[1]?.trim() ?? "";
-}
-function parseCoverMeta(html) {
- const tag = String(html).match(/]*name=["']mindspace-cover["'][^>]*>/i)?.[0];
- if (!tag) return {};
- const contentMatch = tag.match(/content=(["'])([\s\S]*?)\1/i) ?? tag.match(/content=["']([^"']+)["']/i);
- const raw = contentMatch?.[2] ?? contentMatch?.[1];
- if (!raw) return {};
- try {
- return JSON.parse(raw.replaceAll(""", '"'));
- } catch {
- return {};
- }
-}
-function extractEmoji(text) {
- const match = String(text).match(new RegExp("\\p{Extended_Pictographic}", "u"));
- return match?.[0] ?? "";
-}
-function coverImageFromHtml(html) {
- const source = String(html);
- const og = source.match(/]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i) ?? source.match(/]+content=["']([^"']+)["'][^>]+property=["']og:image["']/i);
- if (og?.[1]) return og[1].trim();
- const hero = source.match(/
]+class=["'][^"']*hero[^"']*["'][^>]+src=["']([^"']+)["']/i) ?? source.match(/
]+src=["']([^"']+)["'][^>]+class=["'][^"']*hero/i);
- if (hero?.[1]) return hero[1].trim();
- const first = source.match(/
]+src=["']([^"']+)["']/i);
- return first?.[1]?.trim() ?? "";
-}
-function svgImageHref(value) {
- const raw = String(value);
- if (raw.startsWith("data:")) return raw;
- return escapeXml(raw);
-}
-function mimeFromImageBytes(buffer) {
- if (buffer[0] === 255 && buffer[1] === 216) return "image/jpeg";
- if (buffer[0] === 137 && buffer[1] === 80) return "image/png";
- if (buffer.length >= 12 && buffer.slice(0, 4).toString() === "RIFF" && buffer.slice(8, 12).toString() === "WEBP") {
- return "image/webp";
- }
- if (buffer[0] === 71 && buffer[1] === 73) return "image/gif";
- return "image/jpeg";
-}
-function bytesToDataUri(buffer, mimeType) {
- return `data:${mimeType};base64,${buffer.toString("base64")}`;
-}
-function bufferToImageDataUri(buffer) {
- if (!buffer?.length) return null;
- if (buffer.length > MAX_COVER_BYTES) return null;
- return bytesToDataUri(buffer, mimeFromImageBytes(buffer));
-}
-async function readLocalImageDataUri(filePath) {
- const buffer = await fs11.readFile(filePath);
- if (buffer.length === 0 || buffer.length > MAX_COVER_BYTES) return null;
- return bytesToDataUri(buffer, mimeFromImageBytes(buffer));
-}
-async function fetchRemoteImageDataUri(url) {
- const response = await fetch(url, {
- signal: AbortSignal.timeout(REMOTE_COVER_TIMEOUT_MS),
- headers: { Accept: "image/*" },
- redirect: "follow"
- });
- if (!response.ok) return null;
- const contentType = response.headers.get("content-type") ?? "";
- if (contentType && !contentType.startsWith("image/")) return null;
- const buffer = Buffer.from(await response.arrayBuffer());
- if (buffer.length === 0 || buffer.length > MAX_COVER_BYTES) return null;
- const mimeType = contentType.startsWith("image/") ? contentType.split(";")[0] : mimeFromImageBytes(buffer);
- return bytesToDataUri(buffer, mimeType);
-}
-function resolveLocalImagePath({ storageRoot, contentStorageKey, contentBaseDir, imageUrl }) {
- if (!imageUrl || /^data:|^https?:/i.test(imageUrl)) return null;
- let baseDir = null;
- if (contentStorageKey) {
- 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 = path12.resolve(contentBaseDir);
- }
- if (!baseDir) return null;
- const target = path12.resolve(baseDir, imageUrl.replace(/^\.\//, ""));
- if (target !== baseDir && !target.startsWith(`${baseDir}${path12.sep}`)) return null;
- return target;
-}
-async function resolveCoverDataUri({
- storageRoot,
- contentStorageKey,
- contentBaseDir,
- imageUrl,
- resolveAssetDataUri
-}) {
- const raw = String(imageUrl ?? "").trim();
- if (!raw) return null;
- if (raw.startsWith("data:")) return raw.length <= MAX_COVER_BYTES * 2 ? raw : null;
- if (/^https?:\/\//i.test(raw)) {
- try {
- return await fetchRemoteImageDataUri(raw);
- } catch {
- return null;
- }
- }
- const assetMatch = raw.match(/\/mindspace\/v1\/assets\/([a-z0-9-]+)(?:\/download)?/i);
- if (assetMatch && resolveAssetDataUri) {
- try {
- return await resolveAssetDataUri(assetMatch[1]);
- } catch {
- return null;
- }
- }
- const localPath = resolveLocalImagePath({ storageRoot, contentStorageKey, contentBaseDir, imageUrl: raw });
- if (!localPath) return null;
- try {
- return await readLocalImageDataUri(localPath);
- } catch {
- return null;
- }
-}
-function colorsFromHtml(html) {
- const colors = [];
- const hexRe = /#(?:[0-9a-f]{3}){1,2}\b/gi;
- let match;
- while ((match = hexRe.exec(String(html))) !== null && colors.length < 6) {
- const normalized = normalizeHex(match[0]);
- if (!normalized) continue;
- if (["#ffffff", "#fff", "#000000", "#000"].includes(normalized)) continue;
- if (!colors.includes(normalized)) colors.push(normalized);
- }
- return colors;
-}
-function normalizeHex(value) {
- const raw = String(value).trim().toLowerCase();
- if (!raw.startsWith("#")) return null;
- if (raw.length === 4) {
- return `#${raw[1]}${raw[1]}${raw[2]}${raw[2]}${raw[3]}${raw[3]}`;
- }
- if (raw.length === 7) return raw;
- return null;
-}
-function darken(hex, amount = 0.28) {
- const color = normalizeHex(hex) ?? "#2f6f57";
- const r = Math.round(parseInt(color.slice(1, 3), 16) * (1 - amount));
- const g = Math.round(parseInt(color.slice(3, 5), 16) * (1 - amount));
- const b = Math.round(parseInt(color.slice(5, 7), 16) * (1 - amount));
- return `#${[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
-}
-function splitTitleLines(title, maxLines = 2) {
- const cleaned = String(title || "\u672A\u547D\u540D\u9875\u9762").replace(/\s+/g, " ").trim();
- if (!cleaned) return ["\u672A\u547D\u540D\u9875\u9762"];
- if (cleaned.includes("|")) {
- return cleaned.split("|").map((part) => part.trim()).filter(Boolean).slice(0, maxLines);
- }
- if (cleaned.length <= 16) return [cleaned];
- const midpoint = Math.ceil(cleaned.length / 2);
- const splitAt = cleaned.lastIndexOf(" ", midpoint) > 8 ? cleaned.lastIndexOf(" ", midpoint) : midpoint;
- return [cleaned.slice(0, splitAt).trim(), cleaned.slice(splitAt).trim()].filter(Boolean);
-}
-function lighten(hex, amount = 0.22) {
- const color = normalizeHex(hex) ?? "#2f6f57";
- const r = Math.min(255, Math.round(parseInt(color.slice(1, 3), 16) + 255 * amount));
- const g = Math.min(255, Math.round(parseInt(color.slice(3, 5), 16) + 255 * amount));
- const b = Math.min(255, Math.round(parseInt(color.slice(5, 7), 16) + 255 * amount));
- return `#${[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
-}
-function inferTagFromContent(html, title = "") {
- const text = `${title}
-${String(html).slice(0, 8e3)}`;
- if (/旅行|旅游|攻略|travel/i.test(text)) return "\u65C5\u884C";
- if (/美食|餐厅|菜谱|料理|food/i.test(text)) return "\u7F8E\u98DF";
- if (/报告|分析|研报|数据|report/i.test(text)) return "\u62A5\u544A";
- if (/普拉提|瑜伽|健身|运动|pilates|yoga/i.test(text)) return "\u8FD0\u52A8";
- if (/618|促销|活动|优惠|限时|大促/i.test(text)) return "\u6D3B\u52A8";
- return null;
-}
-function shouldUseScenicBackground(signals) {
- return /旅行|travel|美食|food|餐|报告|report|分析/i.test(String(signals.tag ?? ""));
-}
-function resolvePhotoPalette(signals) {
- const tag = String(signals.tag ?? "");
- const accent = signals.accent;
- const accent2 = signals.accent2;
- if (/旅行|travel/i.test(tag)) {
- return {
- sky: "#4f8fb8",
- glow: "#f6d7a8",
- horizon: "#e39a4d",
- land: "#24343a",
- shadow: "#0d1518",
- flare: "#ffe9c7",
- bokeh: "#fff8ef"
- };
- }
- if (/美食|food|餐/i.test(tag)) {
- return {
- sky: "#5a2b22",
- glow: "#ffb27a",
- horizon: "#d85f3b",
- land: "#241412",
- shadow: "#120909",
- flare: "#ffd0a8",
- bokeh: "#ffe8d6"
- };
- }
- if (/报告|report|分析/i.test(tag)) {
- return {
- sky: "#3d4f68",
- glow: "#9eb4d8",
- horizon: "#607892",
- land: "#1a2430",
- shadow: "#0a1018",
- flare: "#c8d8ef",
- bokeh: "#eef3fb"
- };
- }
- return {
- sky: lighten(accent, 0.18),
- glow: lighten(accent, 0.34),
- horizon: accent,
- land: darken(accent2, 0.1),
- shadow: darken(accent2, 0.32),
- flare: lighten(accent, 0.42),
- bokeh: "#fff8f2"
- };
-}
-function extractCoverSignals(html, meta = {}) {
- const coverMeta = parseCoverMeta(html);
- const rawTitle = meta.title || h1FromHtml(html) || titleFromHtml(html) || "\u672A\u547D\u540D\u9875\u9762";
- const title = rawTitle.replace(new RegExp("\\p{Extended_Pictographic}", "gu"), "").replace(/\s+/g, " ").trim();
- const colors = colorsFromHtml(html);
- const accent = normalizeHex(coverMeta.accent ?? meta.accent ?? colors[0] ?? "#2f6f57");
- const accent2 = normalizeHex(coverMeta.accent2 ?? colors[1] ?? darken(accent, 0.15));
- return {
- title: title || "\u672A\u547D\u540D\u9875\u9762",
- subtitle: coverMeta.subtitle ?? meta.subtitle ?? descriptionFromHtml(html) ?? "TKMind \u4F5C\u54C1",
- tag: coverMeta.tag ?? meta.tag ?? inferTagFromContent(html, title) ?? "\u7CBE\u9009\u9875\u9762",
- emoji: coverMeta.emoji ?? meta.emoji ?? extractEmoji(rawTitle) ?? extractEmoji(h1FromHtml(html)),
- accent,
- accent2,
- mood: coverMeta.mood ?? meta.mood ?? "photo",
- image: coverMeta.cover ?? coverMeta.image ?? meta.cover ?? meta.image ?? coverImageFromHtml(html)
- };
-}
-function themeBackgroundLayers(signals) {
- const accent = escapeXml(signals.accent);
- const accent2 = escapeXml(signals.accent2);
- const glow = escapeXml(lighten(signals.accent, 0.28));
- return `
-
- `;
-}
-function buildFeedThumbnailSvg(signals, options = {}) {
- const coverDataUri = options.coverDataUri ?? null;
- const hasPhoto = Boolean(coverDataUri);
- const useScenic = !hasPhoto && shouldUseScenicBackground(signals);
- const palette = resolvePhotoPalette(signals);
- const titleLines = splitTitleLines(signals.title);
- const line1 = escapeXml(titleLines[0] ?? signals.title).slice(0, 24);
- const line2 = escapeXml(titleLines[1] ?? "").slice(0, 24);
- const subtitle = escapeXml(signals.subtitle).slice(0, 42);
- const tag = escapeXml(signals.tag).slice(0, 12);
- const sky = escapeXml(palette.sky);
- const glow = escapeXml(palette.glow);
- const horizon = escapeXml(palette.horizon);
- const land = escapeXml(palette.land);
- const shadow = escapeXml(palette.shadow);
- const flare = escapeXml(palette.flare);
- const bokeh = escapeXml(palette.bokeh);
- const titleY2 = line2 ? 652 : 0;
- const scenicLayers = useScenic ? `
-
-
-
-
-
-
- ` : !hasPhoto ? themeBackgroundLayers(signals) : "";
- const photoLayer = hasPhoto ? `` : "";
- const overlayStops = hasPhoto ? `
-
- ` : `
-
- `;
- const vignetteOpacity = hasPhoto ? "0.52" : "0.42";
- const grainOpacity = hasPhoto ? "0.18" : "0.28";
- return `
-`;
-}
-function assetThumbnailKey(userId, assetId) {
- return path12.posix.join("users", userId, "assets", assetId, "thumbnail.svg");
-}
-function pageThumbnailKey(userId, pageId) {
- return path12.posix.join("users", userId, "pages", pageId, "thumbnail.svg");
-}
-async function writeThumbnail(storageRoot, storageKey, svg) {
- 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 fs11.mkdir(path12.dirname(target), { recursive: true });
- await fs11.writeFile(target, svg, "utf8");
- return target;
-}
-function isModernFeedThumbnail(svg) {
- if (!svg) return false;
- return /width="540" height="720"/.test(svg) && /filter id="grain"/.test(svg);
-}
-async function generateHtmlThumbnail(storageRoot, storageKey, html, meta = {}) {
- const signals = extractCoverSignals(html, meta);
- const coverDataUri = await resolveCoverDataUri({
- storageRoot,
- contentStorageKey: meta.contentStorageKey,
- contentBaseDir: meta.contentBaseDir,
- imageUrl: signals.image,
- resolveAssetDataUri: meta.resolveAssetDataUri
- });
- const svg = buildFeedThumbnailSvg(signals, { coverDataUri });
- await writeThumbnail(storageRoot, storageKey, svg);
- return svg;
-}
-async function ensureHtmlThumbnail(storageRoot, storageKey, html, meta = {}) {
- const existing = await readThumbnailIfExists(storageRoot, storageKey);
- if (existing && isModernFeedThumbnail(existing) && !meta.force) {
- return existing;
- }
- return generateHtmlThumbnail(storageRoot, storageKey, html, meta);
-}
-async function ensurePageThumbnail({
- storageRoot,
- pageThumbnailStorageKey,
- html,
- meta = {},
- workspacePublishDir = null,
- workspaceHtmlRelativePath = null
-}) {
- if (workspacePublishDir && workspaceHtmlRelativePath) {
- const sidecar = await readThumbnailIfExists(
- workspacePublishDir,
- workspaceThumbnailRelativePath(workspaceHtmlRelativePath)
- );
- if (sidecar && isModernFeedThumbnail(sidecar)) {
- await writeThumbnail(storageRoot, pageThumbnailStorageKey, sidecar);
- return sidecar;
- }
- }
- return ensureHtmlThumbnail(storageRoot, pageThumbnailStorageKey, html, meta);
-}
-async function readThumbnailIfExists(storageRoot, storageKey) {
- try {
- return await fs11.readFile(path12.resolve(storageRoot, storageKey), "utf8");
- } catch {
- return null;
- }
-}
-function scheduleHtmlThumbnail(storageRoot, storageKey, html, meta = {}) {
- queueMicrotask(() => {
- void ensureHtmlThumbnail(storageRoot, storageKey, html, meta).catch(() => {
- });
- });
-}
-
-// mindspace-workspace-thumbnails.mjs
-function workspaceThumbnailRelativePath(htmlRelativePath) {
- const normalized = String(htmlRelativePath ?? "").replace(/^\/+/, "");
- const dir = path13.posix.dirname(normalized);
- const base = path13.basename(normalized, path13.extname(normalized));
- const thumb = `${base}.thumbnail.svg`;
- return dir === "." ? thumb : path13.posix.join(dir, thumb);
-}
-async function ensureWorkspaceHtmlThumbnail(publishDir, htmlRelativePath, html, meta = {}) {
- 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: path13.dirname(htmlPath)
- });
-}
-async function scanPublishTree(publishRoot) {
- 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 = path13.join(publishRoot, entry.name);
- await scanUserHtmlFiles(userDir);
- }
-}
-async function scanUserHtmlFiles(userDir) {
- const walk = async (dir) => {
- const entries = await fsPromises.readdir(dir, { withFileTypes: true });
- for (const entry of entries) {
- 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 = path13.relative(userDir, full);
- await ensureWorkspaceHtmlThumbnail(userDir, rel).catch(() => {
- });
- }
- };
- await walk(userDir);
-}
-function startWorkspaceThumbnailWatcher(publishRoot) {
- if (!fs12.existsSync(publishRoot)) {
- fs12.mkdirSync(publishRoot, { recursive: true });
- }
- void scanPublishTree(publishRoot);
- const pending = /* @__PURE__ */ new Map();
- const schedule = (userDir, relativePath) => {
- const key = path13.join(userDir, relativePath);
- const existing = pending.get(key);
- if (existing) clearTimeout(existing);
- pending.set(
- key,
- setTimeout(() => {
- pending.delete(key);
- void ensureWorkspaceHtmlThumbnail(userDir, relativePath).catch(() => {
- });
- }, 400)
- );
- };
- const attachUserWatcher = (userDir) => {
- if (!fs12.existsSync(userDir)) return;
- void scanUserHtmlFiles(userDir);
- try {
- fs12.watch(userDir, { recursive: true }, (_event, filename) => {
- if (!filename || !String(filename).endsWith(".html")) return;
- if (String(filename).endsWith(".thumbnail.svg")) return;
- schedule(userDir, filename);
- });
- } catch {
- }
- };
- for (const entry of fs12.readdirSync(publishRoot, { withFileTypes: true })) {
- if (entry.isDirectory() && entry.name !== "wiki" && !entry.name.startsWith(".")) {
- attachUserWatcher(path13.join(publishRoot, entry.name));
- }
- }
- try {
- fs12.watch(publishRoot, (_event, filename) => {
- if (!filename) return;
- const userDir = path13.join(publishRoot, filename);
- if (fs12.existsSync(userDir) && fs12.statSync(userDir).isDirectory()) {
- attachUserWatcher(userDir);
- }
- });
- } catch {
- }
-}
-
-// mindspace-workspace-sync.mjs
-import crypto9 from "node:crypto";
-import fs14 from "node:fs";
-import fsPromises2 from "node:fs/promises";
-import path18 from "node:path";
-
-// mindspace-assets.mjs
-import crypto8 from "node:crypto";
-import fs13 from "node:fs/promises";
-import path17 from "node:path";
-
-// mindspace-scan.mjs
-var SCRIPT_PATTERNS = [
- /`;
-function escapeHtml(text) {
- return String(text ?? "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
-}
-function previewDocument(title, bodyHtml, { downloadUrl = null, extraHead = "", allowScripts = false, bodyClass = "" } = {}) {
- const csp = [
- "default-src 'none'",
- "style-src 'unsafe-inline'",
- "img-src 'self' data: https:",
- "font-src 'self' data:",
- "frame-src 'self'",
- "object-src 'self'",
- "base-uri 'none'",
- "form-action 'none'",
- allowScripts ? "script-src 'unsafe-inline'" : "script-src 'none'"
- ].join("; ");
- const downloadLink = downloadUrl ? `
\u4E0B\u8F7D\u539F\u6587\u4EF6
` : "";
- const bodyAttrs = bodyClass ? ` class="${escapeHtml(bodyClass)}"` : "";
- return `${escapeHtml(title)}${extraHead}${escapeHtml(title)}
${downloadLink}${bodyHtml}`;
-}
-function renderImageLightboxMarkup({ downloadUrl, title, triggerClass = "image-frame" }) {
- const safeUrl = escapeHtml(downloadUrl);
- const safeTitle = escapeHtml(title);
- return `

${IMAGE_LIGHTBOX_SCRIPT}`;
-}
-function wantsInlineImageViewer(req) {
- if (req?.query?.viewer === "1") return true;
- if (req?.query?.viewer === "0") return false;
- const fetchDest = String(req?.get?.("sec-fetch-dest") ?? req?.headers?.["sec-fetch-dest"] ?? "").toLowerCase();
- if (fetchDest === "image") return false;
- if (fetchDest === "document" || fetchDest === "iframe") return true;
- const accept = String(req?.get?.("accept") ?? req?.headers?.accept ?? "");
- return /text\/html/i.test(accept);
-}
-function renderImageAssetViewerHtml({ asset, downloadUrl }) {
- const title = asset.displayName || asset.filename;
- return previewDocument(title, renderImageLightboxMarkup({ downloadUrl, title }), {
- downloadUrl,
- allowScripts: true,
- bodyClass: "image-viewer"
- });
-}
-function renderInlineMarkdown(text) {
- let html = escapeHtml(text);
- html = html.replace(/`([^`]+)`/g, "$1");
- html = html.replace(/\*\*(.+?)\*\*/g, "$1");
- html = html.replace(/\*(.+?)\*/g, "$1");
- html = html.replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1');
- return html;
-}
-function renderMarkdownDocument(text) {
- const lines = String(text ?? "").split("\n");
- const blocks = [];
- let inCode = false;
- let code = [];
- for (const line of lines) {
- if (line.startsWith("```")) {
- if (inCode) {
- blocks.push(`${escapeHtml(code.join("\n"))}
`);
- code = [];
- inCode = false;
- } else {
- inCode = true;
- }
- continue;
- }
- if (inCode) {
- code.push(line);
- continue;
- }
- if (!line.trim()) continue;
- const heading = line.match(/^(#{1,6})\s+(.+)$/);
- if (heading) {
- const level = heading[1].length;
- blocks.push(`${renderInlineMarkdown(heading[2])}`);
- continue;
- }
- blocks.push(`${renderInlineMarkdown(line)}
`);
- }
- if (inCode && code.length) {
- blocks.push(`${escapeHtml(code.join("\n"))}
`);
- }
- return blocks.join("\n");
-}
-function parseCsvRows(text) {
- const rows = [];
- let row = [];
- let cell = "";
- let inQuotes = false;
- for (let i = 0; i < text.length; i += 1) {
- const ch = text[i];
- const next = text[i + 1];
- if (inQuotes) {
- if (ch === '"' && next === '"') {
- cell += '"';
- i += 1;
- } else if (ch === '"') {
- inQuotes = false;
- } else {
- cell += ch;
- }
- continue;
- }
- if (ch === '"') {
- inQuotes = true;
- continue;
- }
- if (ch === ",") {
- row.push(cell);
- cell = "";
- continue;
- }
- if (ch === "\n") {
- row.push(cell);
- rows.push(row);
- row = [];
- cell = "";
- continue;
- }
- if (ch === "\r") continue;
- cell += ch;
- }
- row.push(cell);
- rows.push(row);
- return rows.filter((item) => item.some((value) => String(value ?? "").trim()));
-}
-function renderCsvPreview(text) {
- const rows = parseCsvRows(String(text ?? ""));
- if (rows.length === 0) return "\uFF08\u7A7A\u6587\u4EF6\uFF09
";
- const [head, ...body] = rows;
- const header = `${head.map((cell) => `| ${escapeHtml(cell)} | `).join("")}
`;
- const content = body.slice(0, 200).map((line) => `${line.map((cell) => `| ${escapeHtml(cell)} | `).join("")}
`).join("");
- const tail = body.length > 200 ? `\u4EC5\u5C55\u793A\u524D 200 \u884C
` : "";
- return ``;
-}
-function extractZipEntry2(buffer, targetName) {
- let offset = 0;
- while (offset + 30 <= buffer.length) {
- if (buffer.subarray(offset, offset + 2).toString("ascii") !== "PK") break;
- const compressionMethod = buffer.readUInt16LE(offset + 8);
- const compressedSize = buffer.readUInt32LE(offset + 18);
- const nameLength = buffer.readUInt16LE(offset + 26);
- const extraLength = buffer.readUInt16LE(offset + 28);
- const name = buffer.subarray(offset + 30, offset + 30 + nameLength).toString("utf8");
- const dataStart = offset + 30 + nameLength + extraLength;
- if (name === targetName) {
- const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
- if (compressionMethod === 0) return compressed;
- if (compressionMethod === 8) return zlib2.inflateRawSync(compressed);
- return null;
- }
- offset = dataStart + compressedSize;
- }
- return null;
-}
-function extractDocxText2(buffer) {
- const xmlBuffer = extractZipEntry2(buffer, "word/document.xml");
- if (!xmlBuffer) return "";
- const xml = xmlBuffer.toString("utf8");
- const paragraphs = [];
- for (const block of xml.split("")) {
- const texts = [...block.matchAll(/]*>([\s\S]*?)<\/w:t>/g)].map(
- (match) => match[1].replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"')
- );
- const line = texts.join("");
- if (line.trim()) paragraphs.push(line.trim());
- }
- return paragraphs.join("\n\n");
-}
-function canPreviewAsset(mimeType) {
- return PREVIEWABLE_MIME_TYPES.has(mimeType);
-}
-function renderAssetPreviewHtml({ asset, buffer, downloadUrl }) {
- const title = asset.displayName || asset.filename;
- const mimeType = asset.mimeType;
- if (mimeType === "text/html") {
- const csp = ``;
- const html = buffer.toString("utf8");
- if (/]*>/i.test(html)) {
- return html.replace(/]*)>/i, `${csp}`);
- }
- return `${csp}${html}`;
- }
- if (mimeType === "application/pdf") {
- return previewDocument(
- title,
- ``,
- { downloadUrl }
- );
- }
- if (mimeType.startsWith("image/")) {
- return previewDocument(title, renderImageLightboxMarkup({ downloadUrl, title }), {
- downloadUrl,
- allowScripts: true
- });
- }
- if (mimeType === "text/csv") {
- return previewDocument(title, renderCsvPreview(buffer.toString("utf8")), { downloadUrl });
- }
- if (mimeType === "text/markdown") {
- return previewDocument(title, renderMarkdownDocument(buffer.toString("utf8")), { downloadUrl });
- }
- if (mimeType === "text/plain") {
- return previewDocument(title, `${escapeHtml(buffer.toString("utf8"))}`, { downloadUrl });
- }
- if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
- const text = extractDocxText2(buffer);
- const body = text ? `${text.split(/\n{2,}/).map((paragraph) => `${escapeHtml(paragraph)}
`).join("")}` : '\u65E0\u6CD5\u63D0\u53D6\u6B63\u6587\uFF0C\u8BF7\u4E0B\u8F7D\u539F\u6587\u4EF6\u67E5\u770B\u3002
';
- return previewDocument(title, body, { downloadUrl });
- }
- throw Object.assign(new Error("\u8BE5\u8D44\u4EA7\u4E0D\u652F\u6301\u9884\u89C8"), { code: "preview_not_supported" });
-}
-
-// workspace-storage.mjs
-import path15 from "node:path";
-var WORKSPACE_STORAGE_PREFIX = "workspace://";
-function buildWorkspaceStorageKey(userId, categoryCode, relativeFilename) {
- const category = String(categoryCode ?? "").trim();
- const relativePath = String(relativeFilename ?? "").replace(/\\/g, "/").replace(/^\/+/, "");
- if (!userId || !category || !relativePath) {
- throw new Error("invalid workspace storage key parts");
- }
- if (relativePath.split("/").some((part) => part === ".." || part === ".")) {
- throw new Error("invalid workspace relative path");
- }
- return `${WORKSPACE_STORAGE_PREFIX}${userId}/${category}/${relativePath}`;
-}
-function isWorkspaceStorageKey(storageKey) {
- return String(storageKey ?? "").startsWith(WORKSPACE_STORAGE_PREFIX);
-}
-function resolveWorkspaceStoragePath(h5Root, storageKey) {
- if (!h5Root || !isWorkspaceStorageKey(storageKey)) return null;
- const rest = storageKey.slice(WORKSPACE_STORAGE_PREFIX.length);
- const slash = rest.indexOf("/");
- if (slash <= 0) return null;
- const userId = rest.slice(0, slash);
- const remainder = rest.slice(slash + 1);
- const slash2 = remainder.indexOf("/");
- if (slash2 <= 0) return null;
- const categoryCode = remainder.slice(0, slash2);
- const relativeFilename = remainder.slice(slash2 + 1);
- if (!relativeFilename) return null;
- const workspaceRoot = resolveUserWorkspaceRoot(h5Root, { id: userId });
- 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 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;
-var DEFAULT_IMAGE_MASTER_MAX_SIDE = 2560;
-var DEFAULT_IMAGE_INPUT_MAX_PIXELS = 4e7;
-var JPEG_MIN_QUALITY = 62;
-var WEBP_MIN_QUALITY = 64;
-var MIN_SIDE = 1280;
-function fitDimensions(width, height, maxSide, maxPixels) {
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
- return { width: maxSide, height: maxSide };
- }
- if (width <= maxSide && height <= maxSide && width * height <= maxPixels) {
- return { width, height };
- }
- const scale = Math.min(maxSide / width, maxSide / height, Math.sqrt(maxPixels / (width * height)));
- return {
- width: Math.max(1, Math.round(width * scale)),
- height: Math.max(1, Math.round(height * scale))
- };
-}
-function buildOutputProfile(mimeType) {
- if (mimeType === "image/png") {
- return {
- extension: ".png",
- mimeType: "image/png",
- baseQuality: 90,
- minQuality: 70,
- encode: (pipeline2, quality) => pipeline2.png({
- compressionLevel: 9,
- adaptiveFiltering: true,
- palette: true,
- quality,
- effort: 8
- })
- };
- }
- if (mimeType === "image/webp") {
- return {
- extension: ".webp",
- mimeType: "image/webp",
- baseQuality: 84,
- minQuality: WEBP_MIN_QUALITY,
- encode: (pipeline2, quality) => pipeline2.webp({
- quality,
- alphaQuality: Math.min(100, quality + 8),
- effort: 5
- })
- };
- }
- return {
- extension: ".jpg",
- mimeType: "image/jpeg",
- baseQuality: 84,
- minQuality: JPEG_MIN_QUALITY,
- encode: (pipeline2, quality) => pipeline2.jpeg({
- quality,
- mozjpeg: true,
- chromaSubsampling: "4:4:4"
- })
- };
-}
-function deriveOutputFilename(filename, extension) {
- 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 }) {
- const pipeline2 = sharp(buffer, {
- failOn: "error",
- limitInputPixels: maxPixels,
- sequentialRead: true
- }).rotate().resize({
- width,
- height,
- fit: "inside",
- withoutEnlargement: true
- });
- const encoded = await profile.encode(pipeline2, quality).toBuffer();
- return encoded;
-}
-async function normalizeImageForStorage({
- buffer,
- mimeType,
- filename,
- maxUploadBytes = DEFAULT_IMAGE_UPLOAD_MAX_BYTES,
- maxOutputBytes = DEFAULT_IMAGE_MASTER_MAX_BYTES,
- maxSide = DEFAULT_IMAGE_MASTER_MAX_SIDE,
- maxPixels = DEFAULT_IMAGE_INPUT_MAX_PIXELS
-} = {}) {
- if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
- throw Object.assign(new Error("\u56FE\u7247\u5185\u5BB9\u4E3A\u7A7A"), { code: "invalid_file_size" });
- }
- if (!mimeType?.startsWith("image/")) {
- throw Object.assign(new Error("\u53EA\u652F\u6301\u56FE\u7247\u6587\u4EF6"), { code: "unsupported_file_type" });
- }
- if (buffer.length > maxUploadBytes) {
- throw Object.assign(new Error("\u56FE\u7247\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236"), { code: "file_too_large" });
- }
- const metadata = await sharp(buffer, {
- failOn: "error",
- limitInputPixels: maxPixels,
- sequentialRead: true
- }).metadata();
- if (!metadata.width || !metadata.height) {
- throw Object.assign(new Error("\u65E0\u6CD5\u8BC6\u522B\u56FE\u7247\u5C3A\u5BF8"), { code: "image_metadata_invalid" });
- }
- if (metadata.pages && metadata.pages > 1) {
- throw Object.assign(new Error("\u6682\u4E0D\u652F\u6301\u52A8\u6001\u56FE\u50CF\u4E0A\u4F20"), { code: "unsupported_animated_image" });
- }
- const profile = buildOutputProfile(mimeType);
- const initial = fitDimensions(metadata.width, metadata.height, maxSide, maxPixels);
- let width = initial.width;
- let height = initial.height;
- let quality = profile.baseQuality;
- let candidate = await renderVariant(buffer, metadata, profile, { width, height, quality, maxPixels });
- while (candidate.length > maxOutputBytes) {
- if (quality > profile.minQuality) {
- quality = Math.max(profile.minQuality, quality - 6);
- } else if (Math.max(width, height) > MIN_SIDE) {
- width = Math.max(1, Math.round(width * 0.85));
- height = Math.max(1, Math.round(height * 0.85));
- } else {
- break;
- }
- candidate = await renderVariant(buffer, metadata, profile, { width, height, quality, maxPixels });
- }
- const outputFilename = deriveOutputFilename(filename, profile.extension);
- const output = {
- buffer: candidate,
- mimeType: profile.mimeType,
- filename: outputFilename,
- width,
- height
- };
- const preservesFormat = profile.mimeType === mimeType;
- const alreadyWithinBounds = metadata.width <= maxSide && metadata.height <= maxSide && metadata.width * metadata.height <= maxPixels && buffer.length <= maxOutputBytes;
- if (preservesFormat && alreadyWithinBounds && candidate.length >= buffer.length) {
- return {
- buffer,
- mimeType,
- filename,
- width: metadata.width,
- height: metadata.height
- };
- }
- return output;
-}
-
-// mindspace-assets.mjs
-var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Map([
- [".txt", "text/plain"],
- [".md", "text/markdown"],
- [".csv", "text/csv"],
- [".pdf", "application/pdf"],
- [".png", "image/png"],
- [".jpg", "image/jpeg"],
- [".jpeg", "image/jpeg"],
- [".webp", "image/webp"],
- [".doc", "application/msword"],
- [".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
- [".xls", "application/vnd.ms-excel"],
- [".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
- [".ppt", "application/vnd.ms-powerpoint"],
- [".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"],
- [".html", "text/html"],
- [".htm", "text/html"]
-]);
-var MAX_IMAGE_UPLOAD_BYTES = DEFAULT_IMAGE_UPLOAD_MAX_BYTES;
-var PUBLIC_TEMP_IMAGE_DIR = ".tmp-images";
-var PUBLIC_IMAGE_EXTENSIONS = /* @__PURE__ */ new Map([
- ["image/png", ".png"],
- ["image/jpeg", ".jpg"],
- ["image/webp", ".webp"]
-]);
-function asNumber2(value) {
- return Number(value ?? 0);
-}
-function normalizeFilename(filename) {
- const normalized = String(filename ?? "").normalize("NFKC").trim();
- if (!normalized || normalized === "." || normalized === ".." || normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0") || /[\u0000-\u001f\u007f]/.test(normalized)) {
- throw Object.assign(new Error("\u6587\u4EF6\u540D\u65E0\u6548"), { code: "invalid_filename" });
- }
- return normalized.slice(0, 255);
-}
-function expectedMimeType(filename) {
- return ALLOWED_EXTENSIONS.get(path17.extname(filename).toLowerCase()) ?? null;
-}
-function extractImageDimensions(buffer, mimeType) {
- if (mimeType === "image/png" && buffer.length >= 24) {
- const width = buffer.readUInt32BE(16);
- const height = buffer.readUInt32BE(20);
- return { width, height };
- }
- if (mimeType === "image/jpeg" && buffer.length >= 2) {
- let offset = 2;
- while (offset < buffer.length - 9) {
- if (buffer[offset] !== 255) break;
- const marker = buffer[offset + 1];
- if (marker === 217) break;
- if (marker >= 208 && marker <= 216) {
- offset += 2;
- continue;
- }
- const segmentLength = buffer.readUInt16BE(offset + 2);
- if (marker === 192 || marker === 193 || marker === 194) {
- const height = buffer.readUInt16BE(offset + 5);
- const width = buffer.readUInt16BE(offset + 7);
- return { width, height };
- }
- offset += segmentLength + 2;
- }
- }
- if (mimeType === "image/webp" && buffer.length >= 30) {
- const width = buffer.readUInt32LE(24) + 1;
- const height = buffer.readUInt32LE(28) + 1;
- return { width, height };
- }
- return null;
-}
-function validateImagePixels(buffer, mimeType) {
- if (!mimeType?.startsWith("image/")) return null;
- const dims = extractImageDimensions(buffer, mimeType);
- if (!dims) return { ok: true };
- const { width, height } = dims;
- const megapixels = width * height / 1e6;
- const maxMegapixels = DEFAULT_IMAGE_INPUT_MAX_PIXELS / 1e6;
- if (megapixels > maxMegapixels) {
- throw Object.assign(
- new Error(`\u56FE\u7247\u50CF\u7D20\u8D85\u8FC7\u9650\u5236\uFF08${megapixels.toFixed(1)}MP > ${maxMegapixels.toFixed(0)}MP\uFF09`),
- { code: "image_pixel_exceeded" }
- );
- }
- return { ok: true, width, height };
-}
-function detectMimeType(buffer, filename) {
- const head = buffer.subarray(0, 256).toString("utf8").trimStart().toLowerCase();
- if (head.startsWith(" effectiveMaxBytes) {
- throw Object.assign(
- new Error(mimeType.startsWith("image/") ? "\u56FE\u7247\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236" : "\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236"),
- { code: "file_too_large" }
- );
- }
- if (mimeType.startsWith("image/") && normalizedSize > MAX_IMAGE_UPLOAD_BYTES) {
- throw Object.assign(new Error("\u56FE\u7247\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236"), { code: "file_too_large" });
- }
- return { filename: normalizedFilename, sizeBytes: normalizedSize, expectedMimeType: mimeType };
-}
-function createAssetService(pool2, options = {}) {
- 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,
- conversationPackageRegistry
- });
- const mirrorToUserWorkspace = async (userId, { categoryCode, filename, sourcePath }) => {
- if (!h5Root) return null;
- return mirrorAssetToZone({
- workspaceRoot: resolveUserWorkspaceRoot(h5Root, { id: userId }),
- categoryCode,
- filename,
- sourcePath
- });
- };
- const writePublicImageMirror = async (userId, workspaceRelativePath, sourcePath) => {
- if (!h5Root) return null;
- const workspaceRoot = resolveUserWorkspaceRoot(h5Root, { id: userId });
- 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 = path17.join(workspaceRoot, workspaceRelativePath);
- await fs13.rm(target, { force: true });
- };
- const resolvePublicCategory = async (conn, userId) => {
- const [rows] = await conn.query(
- `SELECT id, space_id, category_code
- FROM h5_space_categories
- WHERE user_id = ? AND category_code = 'public'
- LIMIT 1`,
- [userId]
- );
- return rows[0] ?? null;
- };
- const absoluteStoragePath2 = (storageKey) => {
- 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;
- };
- 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) => {
- if (h5Root && isWorkspaceStorageKey(storageKey)) {
- const workspacePath = resolveWorkspaceStoragePath(h5Root, storageKey);
- if (workspacePath) {
- try {
- await fs13.stat(workspacePath);
- return workspacePath;
- } catch (error) {
- if (error?.code !== "ENOENT") throw error;
- }
- }
- }
- let lastError = null;
- for (const candidate of resolveStoragePathCandidates(storageKey)) {
- const absolutePath = absoluteStoragePath2(candidate);
- try {
- await fs13.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 normalizeStoredAssetImage = async ({ buffer, mimeType, filename }) => {
- if (!mimeType?.startsWith("image/")) {
- return { buffer, mimeType, filename };
- }
- return normalizeStoredImage({
- buffer,
- mimeType,
- filename,
- maxUploadBytes: MAX_IMAGE_UPLOAD_BYTES,
- maxPixels: DEFAULT_IMAGE_INPUT_MAX_PIXELS
- });
- };
- const createUpload = async (userId, input) => {
- const validated = validateUploadRequest({ ...input, maxFileBytes });
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [categories] = await conn.query(
- `SELECT c.id, c.space_id, c.category_code
- FROM h5_space_categories c
- WHERE c.id = ? AND c.user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [input.categoryId, userId]
- );
- const category = categories[0];
- if (!category) {
- throw Object.assign(new Error("\u5206\u7C7B\u4E0D\u5B58\u5728"), { code: "category_not_found" });
- }
- if (!UPLOADABLE_CATEGORY_CODES.includes(category.category_code)) {
- throw Object.assign(new Error("\u8BE5\u5206\u7C7B\u4E0D\u5141\u8BB8\u76F4\u63A5\u4E0A\u4F20"), {
- code: "category_not_uploadable"
- });
- }
- let targetCategory = category;
- if (validated.expectedMimeType.startsWith("image/")) {
- const publicCategory = await resolvePublicCategory(conn, userId);
- if (publicCategory) targetCategory = publicCategory;
- }
- const [spaces] = await conn.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes, status
- FROM h5_user_spaces
- WHERE id = ? AND user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [category.space_id, userId]
- );
- const space = spaces[0];
- if (!space || space.status !== "active") {
- throw Object.assign(new Error("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u7528"), { code: "space_unavailable" });
- }
- const available = asNumber2(space.quota_bytes) - asNumber2(space.used_bytes) - asNumber2(space.reserved_bytes);
- if (available < validated.sizeBytes) {
- throw Object.assign(new Error("\u5269\u4F59\u7A7A\u95F4\u4E0D\u8DB3"), {
- code: "quota_exceeded",
- details: { requiredBytes: validated.sizeBytes, availableBytes: Math.max(0, available) }
- });
- }
- const uploadId = idFactory();
- const now = Date.now();
- const temporaryStorageKey = path17.posix.join("users", userId, "temp", `${uploadId}.upload`);
- await conn.query(
- `UPDATE h5_user_spaces
- SET reserved_bytes = reserved_bytes + ?, updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [validated.sizeBytes, now, space.id, userId]
- );
- 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,
- source_session_id, source_message_id)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'reserved', ?, ?, ?, ?)`,
- [
- uploadId,
- userId,
- targetCategory.space_id,
- targetCategory.id,
- validated.filename,
- validated.sizeBytes,
- input.declaredMimeType || null,
- validated.sizeBytes,
- temporaryStorageKey,
- now + uploadTtlMs,
- now,
- input.sourceSessionId ?? input.source_session_id ?? null,
- input.sourceMessageId ?? input.source_message_id ?? null
- ]
- );
- await conn.commit();
- return {
- id: uploadId,
- filename: validated.filename,
- expectedSize: validated.sizeBytes,
- uploadUrl: `/api/mindspace/v1/uploads/${uploadId}/content`,
- expiresAt: now + uploadTtlMs,
- reservedBytes: validated.sizeBytes
- };
- } catch (error) {
- await conn.rollback();
- throw error;
- } finally {
- conn.release();
- }
- };
- const writeUploadContent = async (userId, uploadId, buffer) => {
- if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
- throw Object.assign(new Error("\u4E0A\u4F20\u5185\u5BB9\u4E3A\u7A7A"), { code: "invalid_file_size" });
- }
- const [rows] = await pool2.query(
- `SELECT id, filename, expected_size, temporary_storage_key, status, expires_at
- FROM h5_upload_sessions
- WHERE id = ? AND user_id = ?
- LIMIT 1`,
- [uploadId, userId]
- );
- const upload = rows[0];
- if (!upload) throw Object.assign(new Error("\u4E0A\u4F20\u4F1A\u8BDD\u4E0D\u5B58\u5728"), { code: "upload_not_found" });
- if (upload.status !== "reserved") {
- throw Object.assign(new Error("\u4E0A\u4F20\u4F1A\u8BDD\u72B6\u6001\u65E0\u6548"), { code: "invalid_upload_state" });
- }
- if (asNumber2(upload.expires_at) <= Date.now()) {
- throw Object.assign(new Error("\u4E0A\u4F20\u4F1A\u8BDD\u5DF2\u8FC7\u671F"), { code: "upload_expired" });
- }
- if (buffer.length !== asNumber2(upload.expected_size)) {
- throw Object.assign(new Error("\u4E0A\u4F20\u5185\u5BB9\u5927\u5C0F\u4E0E\u9884\u671F\u4E0D\u4E00\u81F4"), {
- code: "file_size_mismatch"
- });
- }
- const detectedMimeType = detectMimeType(buffer, upload.filename);
- if (!detectedMimeType) {
- throw Object.assign(new Error("\u65E0\u6CD5\u786E\u8BA4\u6587\u4EF6\u7C7B\u578B"), { code: "unsupported_file_type" });
- }
- const effectiveMaxBytes = detectedMimeType.startsWith("image/") ? MAX_IMAGE_UPLOAD_BYTES : maxFileBytes;
- if (buffer.length > effectiveMaxBytes) {
- throw Object.assign(
- new Error(detectedMimeType.startsWith("image/") ? "\u56FE\u7247\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236" : "\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236"),
- { code: "file_too_large" }
- );
- }
- if (detectedMimeType.startsWith("image/") && buffer.length > MAX_IMAGE_UPLOAD_BYTES) {
- throw Object.assign(new Error("\u56FE\u7247\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236"), { code: "file_too_large" });
- }
- if (detectedMimeType.startsWith("image/")) {
- validateImagePixels(buffer, detectedMimeType);
- }
- const target = absoluteStoragePath2(upload.temporary_storage_key);
- 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 fs13.writeFile(target, buffer);
- });
- const checksum = crypto8.createHash("sha256").update(buffer).digest("hex");
- await pool2.query(
- `UPDATE h5_upload_sessions
- SET actual_size = ?, detected_mime_type = ?, checksum = ?, status = 'uploaded'
- WHERE id = ? AND user_id = ? AND status = 'reserved'`,
- [buffer.length, detectedMimeType, checksum, uploadId, userId]
- );
- return { sizeBytes: buffer.length, mimeType: detectedMimeType, checksum };
- };
- const completeUpload = async (userId, uploadId) => {
- const conn = await pool2.getConnection();
- let temporaryPath;
- let finalPath;
- let publicMirror = null;
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT u.*, c.category_code
- FROM h5_upload_sessions u
- JOIN h5_space_categories c ON c.id = u.category_id AND c.user_id = u.user_id
- WHERE u.id = ? AND u.user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [uploadId, userId]
- );
- const upload = rows[0];
- if (!upload) throw Object.assign(new Error("\u4E0A\u4F20\u4F1A\u8BDD\u4E0D\u5B58\u5728"), { code: "upload_not_found" });
- if (upload.status === "completed" && upload.completed_asset_id) {
- const [existing] = await conn.query(
- `SELECT a.*, c.category_code
- FROM h5_assets a
- JOIN h5_space_categories c ON c.id = a.category_id
- WHERE a.id = ? AND a.user_id = ?`,
- [upload.completed_asset_id, userId]
- );
- await conn.commit();
- return existing[0] ? assetResponse(existing[0]) : null;
- }
- if (upload.status !== "uploaded") {
- throw Object.assign(new Error("\u6587\u4EF6\u5185\u5BB9\u5C1A\u672A\u4E0A\u4F20"), { code: "invalid_upload_state" });
- }
- if (asNumber2(upload.actual_size) !== asNumber2(upload.expected_size) || !upload.detected_mime_type || !upload.checksum) {
- throw Object.assign(new Error("\u4E0A\u4F20\u5185\u5BB9\u4E0D\u5B8C\u6574"), { code: "file_size_mismatch" });
- }
- const assetId = idFactory();
- const versionId = idFactory();
- temporaryPath = absoluteStoragePath2(upload.temporary_storage_key);
- const sourceBuffer = await fs13.readFile(temporaryPath);
- const normalizedImage = await normalizeStoredAssetImage({
- buffer: sourceBuffer,
- mimeType: upload.detected_mime_type,
- filename: upload.filename
- });
- const storedBuffer = normalizedImage.buffer;
- const storedMimeType = normalizedImage.mimeType;
- const storedUploadFilename = normalizedImage.filename;
- const storedSizeBytes = storedBuffer.length;
- const storedChecksum = crypto8.createHash("sha256").update(storedBuffer).digest("hex");
- const scan = runBasicFileScan(storedBuffer, {
- filename: storedUploadFilename,
- mimeType: storedMimeType
- });
- const assetStatus = scan.scanStatus === "passed" ? "ready" : "quarantined";
- const versionScanStatus = scan.scanStatus === "passed" ? "passed" : "blocked";
- const isImage = storedMimeType.startsWith("image/");
- const shouldPublishImage = isImage && scan.scanStatus === "passed";
- let finalStorageKey = upload.temporary_storage_key;
- let workspaceRelativePath = null;
- let storedFilename = storedUploadFilename;
- if (shouldPublishImage) {
- const imagePaths = resolvePublicImagePaths(
- userId,
- assetId,
- storedMimeType,
- storedUploadFilename
- );
- finalStorageKey = imagePaths.storageKey;
- workspaceRelativePath = imagePaths.workspaceRelativePath;
- storedFilename = imagePaths.workspaceRelativePath;
- }
- finalPath = absoluteStoragePath2(finalStorageKey);
- if (finalStorageKey !== upload.temporary_storage_key) {
- await fs13.mkdir(path17.dirname(finalPath), { recursive: true });
- await fs13.writeFile(finalPath, storedBuffer);
- } else if (isImage) {
- await fs13.writeFile(finalPath, storedBuffer);
- }
- if (shouldPublishImage && workspaceRelativePath) {
- publicMirror = await writePublicImageMirror(userId, workspaceRelativePath, finalPath);
- }
- const now = Date.now();
- const visibility = shouldPublishImage ? "public_candidate" : visibilityForCategory(upload.category_code);
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'upload', ?, ?)`,
- [
- assetId,
- userId,
- upload.space_id,
- upload.category_id,
- assetTypeForMime(storedMimeType),
- storedMimeType,
- storedFilename,
- upload.filename,
- versionId,
- storedSizeBytes,
- storedChecksum,
- scan.riskLevel,
- visibility,
- assetStatus,
- 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, ?, ?, ?, ?, ?, '\u521D\u59CB\u4E0A\u4F20', ?, ?)`,
- [
- versionId,
- assetId,
- finalStorageKey,
- storedSizeBytes,
- storedChecksum,
- storedMimeType,
- userId,
- versionScanStatus,
- now
- ]
- );
- await conn.query(
- `UPDATE h5_user_spaces
- SET reserved_bytes = GREATEST(0, reserved_bytes - ?),
- used_bytes = used_bytes + ?,
- updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [upload.reserved_bytes, storedSizeBytes, now, upload.space_id, userId]
- );
- await conn.query(
- `UPDATE h5_upload_sessions
- SET status = 'completed', completed_at = ?, completed_asset_id = ?
- WHERE id = ? AND user_id = ?`,
- [now, assetId, uploadId, userId]
- );
- await conn.commit();
- const result = {
- id: assetId,
- user_id: userId,
- categoryId: upload.category_id,
- categoryCode: upload.category_code,
- assetType: assetTypeForMime(storedMimeType),
- mimeType: storedMimeType,
- filename: upload.filename,
- displayName: upload.filename,
- sizeBytes: storedSizeBytes,
- checksum: storedChecksum,
- riskLevel: scan.riskLevel,
- visibility,
- status: assetStatus,
- scanStatus: versionScanStatus,
- sourceType: "upload",
- createdAt: now,
- updatedAt: now,
- publicUrl: shouldPublishImage ? buildUserImagePublicUrl({
- userId,
- storageKey: finalStorageKey,
- mimeType: storedMimeType,
- originalFilename: storedUploadFilename
- }) : null
- };
- if (finalStorageKey !== upload.temporary_storage_key) {
- 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 fs13.rm(finalPath, { 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;
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT id, space_id, reserved_bytes, temporary_storage_key, status
- FROM h5_upload_sessions
- WHERE id = ? AND user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [uploadId, userId]
- );
- const upload = rows[0];
- if (!upload) throw Object.assign(new Error("\u4E0A\u4F20\u4F1A\u8BDD\u4E0D\u5B58\u5728"), { code: "upload_not_found" });
- if (["completed", "cancelled", "expired"].includes(upload.status)) {
- await conn.commit();
- return { cancelled: upload.status !== "completed" };
- }
- storageKey = upload.temporary_storage_key;
- const now = Date.now();
- await conn.query(
- `UPDATE h5_user_spaces
- SET reserved_bytes = GREATEST(0, reserved_bytes - ?), updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [upload.reserved_bytes, now, upload.space_id, userId]
- );
- await conn.query(
- `UPDATE h5_upload_sessions SET status = 'cancelled' WHERE id = ? AND user_id = ?`,
- [uploadId, userId]
- );
- await conn.commit();
- if (storageKey) await fs13.rm(absoluteStoragePath2(storageKey), { force: true });
- return { cancelled: true };
- } catch (error) {
- await conn.rollback();
- throw error;
- } finally {
- conn.release();
- }
- };
- const listAssets = async (userId, { categoryId, categoryCode, syncWorkspace = true } = {}) => {
- if (syncWorkspace && categoryCode && UPLOADABLE_CATEGORY_CODES.includes(categoryCode)) {
- await workspaceSync.syncUserWorkspace(userId, { categoryCode }).catch((error) => {
- console.warn(
- `[MindSpace] workspace sync failed (${categoryCode}):`,
- error?.message ?? error
- );
- });
- }
- const filters = [`a.user_id = ?`, `a.status <> 'deleted'`];
- const params = [userId];
- if (categoryId) {
- filters.push(`a.category_id = ?`);
- params.push(categoryId);
- }
- if (categoryCode) {
- filters.push(`c.category_code = ?`);
- params.push(categoryCode);
- }
- const [rows] = await pool2.query(
- `SELECT a.*, c.category_code, v.storage_key,
- ANY_VALUE(p.id) AS source_page_id
- FROM h5_assets a
- JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id
- JOIN h5_asset_versions v ON v.id = a.current_version_id
- LEFT JOIN h5_page_versions pv ON pv.bundle_asset_id = a.id
- LEFT JOIN h5_page_records p ON p.id = pv.page_id AND p.status <> 'deleted' AND p.user_id = a.user_id
- WHERE ${filters.join(" AND ")}
- GROUP BY a.id, v.storage_key
- ORDER BY a.updated_at DESC
- LIMIT 100`,
- params
- );
- return rows.map(assetResponse);
- };
- const deleteAsset = async (userId, assetId) => {
- const conn = await pool2.getConnection();
- let mirrorCleanup = null;
- let storageCleanup = null;
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT a.id, a.space_id, a.size_bytes, a.status, a.original_filename, a.mime_type,
- c.category_code, v.storage_key
- FROM h5_assets a
- JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id
- LEFT JOIN h5_asset_versions v ON v.id = a.current_version_id
- WHERE a.id = ? AND a.user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [assetId, userId]
- );
- const asset = rows[0];
- if (!asset || asset.status === "deleted") {
- throw Object.assign(new Error("\u8D44\u4EA7\u4E0D\u5B58\u5728"), { code: "asset_not_found" });
- }
- const [pageReferences] = await conn.query(
- `SELECT p.id, p.title, p.status,
- EXISTS(
- SELECT 1 FROM h5_publish_records pr
- WHERE pr.page_id = p.id AND pr.user_id = p.user_id AND pr.status = 'online'
- ) AS published_online
- FROM h5_page_versions pv
- JOIN h5_page_records p ON p.id = pv.page_id AND p.status <> 'deleted'
- WHERE pv.content_asset_id = ? OR pv.bundle_asset_id = ?
- GROUP BY p.id, p.title, p.status
- LIMIT 10`,
- [assetId, assetId]
- );
- if (pageReferences.length > 0) {
- throw Object.assign(new Error("\u8D44\u4EA7\u6B63\u5728\u88AB\u9875\u9762\u4F7F\u7528\uFF0C\u4E0D\u80FD\u5220\u9664"), {
- code: "asset_in_use",
- details: {
- hint: "\u8BF7\u6253\u5F00\u5173\u8054\u9875\u9762\u5E76\u5220\u9664\uFF1B\u5220\u9664\u9875\u9762\u4F1A\u81EA\u52A8\u4E0B\u7EBF\u516C\u5F00\u94FE\u63A5\u5E76\u5220\u9664\u9875\u9762\u5185\u5BB9\u8D44\u4EA7\uFF0C\u4E4B\u540E\u5373\u53EF\u5220\u9664\u539F\u59CB\u8D44\u6599\u3002",
- references: pageReferences.map((page) => ({
- type: "page",
- id: page.id,
- title: page.title,
- status: page.status,
- publishedOnline: Boolean(page.published_online)
- }))
- }
- });
- }
- const now = Date.now();
- await conn.query(
- `UPDATE h5_assets SET status = 'deleted', deleted_at = ?, updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [now, now, assetId, userId]
- );
- await conn.query(
- `UPDATE h5_user_spaces
- SET used_bytes = GREATEST(0, used_bytes - ?), updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [asset.size_bytes, now, asset.space_id, userId]
- );
- await conn.commit();
- if (h5Root) {
- mirrorCleanup = {
- id: asset.id,
- categoryCode: asset.category_code,
- filename: asset.original_filename,
- mimeType: asset.mime_type
- };
- }
- if (asset.storage_key) storageCleanup = asset.storage_key;
- } catch (error) {
- await conn.rollback();
- throw error;
- } finally {
- conn.release();
- }
- if (mirrorCleanup) {
- try {
- removeZoneMirror({
- workspaceRoot: resolveUserWorkspaceRoot(h5Root, { id: userId }),
- categoryCode: mirrorCleanup.categoryCode,
- filename: mirrorCleanup.filename
- });
- if (/\.html$/i.test(mirrorCleanup.filename)) {
- const thumbName = mirrorCleanup.filename.replace(/\.html$/i, ".thumbnail.svg");
- removeZoneMirror({
- workspaceRoot: resolveUserWorkspaceRoot(h5Root, { id: userId }),
- categoryCode: mirrorCleanup.categoryCode,
- filename: thumbName
- });
- }
- if (mirrorCleanup.mimeType?.startsWith?.("image/")) {
- await removePublicImageMirror(userId, mirrorCleanup.filename);
- }
- } catch {
- }
- }
- if (storageCleanup) {
- try {
- await fs13.rm(absoluteStoragePath2(storageCleanup), { force: true });
- } catch {
- }
- }
- return { deleted: true };
- };
- const readAsset = async (userId, assetId) => {
- const [rows] = await pool2.query(
- `SELECT a.*, c.category_code, v.storage_key, v.scan_status
- FROM h5_assets a
- JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id
- JOIN h5_asset_versions v ON v.id = a.current_version_id
- WHERE a.id = ? AND a.user_id = ? AND a.status <> 'deleted'
- LIMIT 1`,
- [assetId, userId]
- );
- const asset = rows[0];
- if (!asset) throw Object.assign(new Error("\u8D44\u4EA7\u4E0D\u5B58\u5728"), { code: "asset_not_found" });
- assertAssetDownloadable(asset);
- return {
- asset: assetResponse(asset),
- path: await resolveReadableStoragePath(asset.storage_key)
- };
- };
- const readPublicAsset = async (assetId) => {
- const [rows] = await pool2.query(
- `SELECT a.*, c.category_code, v.storage_key, v.scan_status
- FROM h5_assets a
- JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id
- JOIN h5_asset_versions v ON v.id = a.current_version_id
- WHERE a.id = ? AND a.status <> 'deleted'
- LIMIT 1`,
- [assetId]
- );
- const asset = rows[0];
- if (!asset) throw Object.assign(new Error("\u8D44\u4EA7\u4E0D\u5B58\u5728"), { code: "asset_not_found" });
- assertAssetDownloadable(asset);
- return {
- asset: assetResponse(asset),
- path: await resolveReadableStoragePath(asset.storage_key)
- };
- };
- const createChatAsset = async (userId, { categoryCode, buffer, filename, displayName, sourceType = "chat" }) => {
- if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
- throw Object.assign(new Error("\u8D44\u4EA7\u5185\u5BB9\u4E3A\u7A7A"), { code: "invalid_file_size" });
- }
- const normalizedFilename = normalizeFilename(filename);
- const detectedMimeType = detectMimeType(buffer, normalizedFilename);
- if (!detectedMimeType) {
- throw Object.assign(new Error("\u65E0\u6CD5\u786E\u8BA4\u6587\u4EF6\u7C7B\u578B"), { code: "unsupported_file_type" });
- }
- const effectiveMaxBytes = detectedMimeType.startsWith("image/") ? MAX_IMAGE_UPLOAD_BYTES : maxFileBytes;
- if (buffer.length > effectiveMaxBytes) {
- throw Object.assign(
- new Error(detectedMimeType.startsWith("image/") ? "\u56FE\u7247\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236" : "\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236"),
- { code: "file_too_large" }
- );
- }
- if (detectedMimeType.startsWith("image/") && buffer.length > MAX_IMAGE_UPLOAD_BYTES) {
- throw Object.assign(new Error("\u56FE\u7247\u6587\u4EF6\u8D85\u8FC7\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236"), { code: "file_too_large" });
- }
- const normalizedImage = await normalizeStoredAssetImage({
- buffer,
- mimeType: detectedMimeType,
- filename: normalizedFilename
- });
- const storedBuffer = normalizedImage.buffer;
- const storedMimeType = normalizedImage.mimeType;
- const storedFilenameInput = normalizedImage.filename;
- const storedSizeBytes = storedBuffer.length;
- const storedChecksum = crypto8.createHash("sha256").update(storedBuffer).digest("hex");
- const isImage = storedMimeType.startsWith("image/");
- const effectiveCategoryCode = isImage ? "public" : categoryCode;
- const conn = await pool2.getConnection();
- let finalPath;
- let publicMirror = null;
- try {
- await conn.beginTransaction();
- const [categories] = await conn.query(
- `SELECT c.id, c.space_id, c.category_code
- FROM h5_space_categories c
- WHERE c.user_id = ? AND c.category_code = ?
- LIMIT 1
- FOR UPDATE`,
- [userId, effectiveCategoryCode]
- );
- const category = categories[0];
- if (!category) {
- throw Object.assign(new Error("\u5206\u7C7B\u4E0D\u5B58\u5728"), { code: "category_not_found" });
- }
- if (!UPLOADABLE_CATEGORY_CODES.includes(category.category_code) && category.category_code !== "draft") {
- throw Object.assign(new Error("\u8BE5\u5206\u7C7B\u4E0D\u5141\u8BB8\u4FDD\u5B58\u804A\u5929\u8D44\u4EA7"), {
- code: "category_not_uploadable"
- });
- }
- const [spaces] = await conn.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes, status
- FROM h5_user_spaces
- WHERE id = ? AND user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [category.space_id, userId]
- );
- const space = spaces[0];
- if (!space || space.status !== "active") {
- throw Object.assign(new Error("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u7528"), { code: "space_unavailable" });
- }
- const available = asNumber2(space.quota_bytes) - asNumber2(space.used_bytes) - asNumber2(space.reserved_bytes);
- if (available < storedSizeBytes) {
- throw Object.assign(new Error("\u5269\u4F59\u7A7A\u95F4\u4E0D\u8DB3"), {
- code: "quota_exceeded",
- details: { requiredBytes: storedSizeBytes, availableBytes: Math.max(0, available) }
- });
- }
- const assetId = idFactory();
- const versionId = idFactory();
- const scan = runBasicFileScan(storedBuffer, {
- filename: storedFilenameInput,
- mimeType: storedMimeType
- });
- const assetStatus = scan.scanStatus === "passed" ? "ready" : "quarantined";
- const versionScanStatus = scan.scanStatus === "passed" ? "passed" : "blocked";
- const shouldPublishImage = isImage && scan.scanStatus === "passed";
- let finalStorageKey = path17.posix.join("users", userId, "assets", assetId, "versions", versionId);
- let workspaceRelativePath = null;
- let storedFilename = storedFilenameInput;
- if (shouldPublishImage) {
- const imagePaths = resolvePublicImagePaths(
- userId,
- assetId,
- storedMimeType,
- storedFilenameInput
- );
- finalStorageKey = imagePaths.storageKey;
- workspaceRelativePath = imagePaths.workspaceRelativePath;
- storedFilename = imagePaths.workspaceRelativePath;
- }
- finalPath = absoluteStoragePath2(finalStorageKey);
- await fs13.mkdir(path17.dirname(finalPath), { recursive: true });
- await fs13.writeFile(finalPath, storedBuffer, { flag: "wx" });
- if (shouldPublishImage && workspaceRelativePath) {
- publicMirror = await writePublicImageMirror(userId, workspaceRelativePath, finalPath);
- }
- const now = Date.now();
- const visibility = shouldPublishImage ? "public_candidate" : visibilityForCategory(category.category_code);
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [
- assetId,
- userId,
- category.space_id,
- category.id,
- assetTypeForMime(storedMimeType),
- storedMimeType,
- storedFilename,
- displayName || path17.basename(storedFilename),
- versionId,
- storedSizeBytes,
- storedChecksum,
- scan.riskLevel,
- visibility,
- assetStatus,
- sourceType,
- 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, ?, ?, ?, ?, ?, '\u4ECE\u804A\u5929\u4FDD\u5B58', ?, ?)`,
- [
- versionId,
- assetId,
- finalStorageKey,
- storedSizeBytes,
- storedChecksum,
- storedMimeType,
- userId,
- versionScanStatus,
- now
- ]
- );
- await conn.query(
- `UPDATE h5_user_spaces SET used_bytes = used_bytes + ?, updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [storedSizeBytes, now, category.space_id, userId]
- );
- await conn.commit();
- if (storedMimeType === "text/html") {
- scheduleHtmlThumbnail(storageRoot, assetThumbnailKey(userId, assetId), storedBuffer.toString("utf8"), {
- title: displayName || normalizedFilename,
- subtitle: category.category_code.toUpperCase(),
- contentStorageKey: finalStorageKey
- });
- }
- return assetResponse({
- id: assetId,
- user_id: userId,
- category_id: category.id,
- category_code: category.category_code,
- asset_type: assetTypeForMime(storedMimeType),
- mime_type: storedMimeType,
- original_filename: storedFilename,
- display_name: displayName || path17.basename(storedFilename),
- storage_key: finalStorageKey,
- size_bytes: storedSizeBytes,
- checksum: storedChecksum,
- risk_level: scan.riskLevel,
- visibility,
- status: assetStatus,
- scan_status: versionScanStatus,
- source_type: sourceType,
- created_at: now,
- updated_at: now,
- has_thumbnail: storedMimeType === "text/html"
- });
- } catch (error) {
- await conn.rollback();
- if (finalPath) await fs13.rm(finalPath, { force: true }).catch(() => {
- });
- if (publicMirror) await fs13.rm(publicMirror, { force: true }).catch(() => {
- });
- throw error;
- } finally {
- conn.release();
- }
- };
- const renderAssetThumbnail = async (userId, assetId) => {
- const { asset, path: assetPath } = await readAsset(userId, assetId);
- if (asset.mimeType !== "text/html") {
- throw Object.assign(new Error("\u8BE5\u8D44\u4EA7\u4E0D\u652F\u6301\u7F29\u7565\u56FE"), { code: "thumbnail_not_supported" });
- }
- const [versions] = await pool2.query(
- `SELECT storage_key FROM h5_asset_versions
- WHERE asset_id = ? AND version_no = 1
- LIMIT 1`,
- [assetId]
- );
- const html = await fs13.readFile(assetPath, "utf8");
- return ensureHtmlThumbnail(storageRoot, assetThumbnailKey(userId, assetId), html, {
- title: asset.displayName,
- subtitle: asset.categoryCode?.toUpperCase?.() ?? "HTML",
- contentStorageKey: versions[0]?.storage_key
- });
- };
- const renderAssetPreview = async (userId, assetId, { downloadPath = null } = {}) => {
- const { asset, path: assetPath } = await readAsset(userId, assetId);
- if (!canPreviewAsset(asset.mimeType)) {
- throw Object.assign(new Error("\u8BE5\u8D44\u4EA7\u4E0D\u652F\u6301\u9884\u89C8"), { code: "preview_not_supported" });
- }
- 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 fs13.readFile(assetPath);
- const extracted = extractAttachmentText(buffer, asset.mimeType, asset.filename);
- return {
- asset: assetResponse(asset),
- ...extracted,
- charCount: extracted.text.length
- };
- };
- const expireStaleUploads = async (now = Date.now()) => {
- const [rows] = await pool2.query(
- `SELECT id, user_id, space_id, reserved_bytes, temporary_storage_key, status
- FROM h5_upload_sessions
- WHERE status IN ('reserved', 'uploaded') AND expires_at <= ?
- LIMIT 200`,
- [now]
- );
- let expired = 0;
- for (const upload of rows) {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [locked] = await conn.query(
- `SELECT id, reserved_bytes, temporary_storage_key, status
- FROM h5_upload_sessions
- WHERE id = ? AND user_id = ? AND status IN ('reserved', 'uploaded')
- LIMIT 1
- FOR UPDATE`,
- [upload.id, upload.user_id]
- );
- const row = locked[0];
- if (!row) {
- await conn.commit();
- continue;
- }
- await conn.query(
- `UPDATE h5_user_spaces
- SET reserved_bytes = GREATEST(0, reserved_bytes - ?), updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [row.reserved_bytes, now, upload.space_id, upload.user_id]
- );
- await conn.query(
- `UPDATE h5_upload_sessions SET status = 'expired' WHERE id = ? AND user_id = ?`,
- [upload.id, upload.user_id]
- );
- await conn.commit();
- if (row.temporary_storage_key) {
- await fs13.rm(absoluteStoragePath2(row.temporary_storage_key), { force: true });
- }
- expired += 1;
- } catch {
- await conn.rollback();
- } finally {
- conn.release();
- }
- }
- return expired;
- };
- return {
- storageRoot,
- createUpload,
- writeUploadContent,
- completeUpload,
- claimUploadArtifactsForConversation,
- cancelUpload,
- createChatAsset,
- listAssets,
- deleteAsset,
- readAsset,
- readPublicAsset,
- renderAssetPreview,
- extractAssetText,
- renderAssetThumbnail,
- syncWorkspaceAssets: workspaceSync.syncUserWorkspace,
- 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,
- registerUploadArtifactForConversation
-};
-
-// mindspace-workspace-sync.mjs
-var SKIP_FILENAMES = /* @__PURE__ */ new Set(["index.html", ".tkmindhints", ".goosehints", ".ls_output"]);
-var SKIP_ZONE_DIR_NAMES = /* @__PURE__ */ new Set([
- "node_modules",
- ".venv",
- ".venv2",
- "__pycache__",
- ".git",
- "dist",
- "build",
- ".next",
- "target",
- "backend",
- "frontend"
-]);
-var NESTED_SKIP_BASENAMES = /* @__PURE__ */ new Set([
- "README.md",
- "requirements.txt",
- "package.json",
- "package-lock.json",
- "pnpm-lock.yaml",
- "yarn.lock"
-]);
-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;
- const parts = normalized.split("/").filter(Boolean);
- if (parts.some((part) => part === "." || part === "..")) return null;
- if (parts.some((part) => part.startsWith("."))) return null;
- return parts.join("/").slice(0, 255);
-}
-function shouldSyncWorkspaceFilename(filename) {
- const relativePath = normalizeWorkspaceRelativePath(filename);
- if (!relativePath) return false;
- 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;
- if (basename.endsWith(".sh")) return false;
- if (relativePath.includes("/") && NESTED_SKIP_BASENAMES.has(basename)) return false;
- return assetInternals.expectedMimeType(basename) !== null;
-}
-async function walkWorkspaceZoneDir(zoneDir, relativePrefix, files) {
- let entries;
- try {
- entries = await fsPromises2.readdir(zoneDir, { withFileTypes: true });
- } catch {
- return;
- }
- for (const entry of entries) {
- if (entry.name.startsWith(".")) continue;
- 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;
- await walkWorkspaceZoneDir(absolutePath, nextPrefix, files);
- continue;
- }
- if (!entry.isFile()) continue;
- const filename = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
- if (!shouldSyncWorkspaceFilename(filename)) continue;
- const stat = await fsPromises2.stat(absolutePath);
- files.push({
- filename,
- absolutePath,
- sizeBytes: stat.size,
- mtimeMs: stat.mtimeMs
- });
- }
-}
-async function listWorkspaceZoneFiles(workspaceRoot, categoryCode) {
- if (!UPLOAD_ZONE_CODES.includes(categoryCode)) return [];
- const zoneDir = resolveZoneDir(workspaceRoot, categoryCode);
- const files = [];
- await walkWorkspaceZoneDir(zoneDir, "", files);
- return files;
-}
-function createWorkspaceAssetSync({
- pool: pool2,
- storageRoot,
- h5Root,
- maxFileBytes,
- idFactory,
- conversationPackageRegistry = null
-}) {
- void storageRoot;
- const loadExistingAssets = async (userId, categoryId) => {
- const [rows] = await pool2.query(
- `SELECT a.id, a.original_filename, a.checksum, a.size_bytes, a.current_version_id, a.status
- FROM h5_assets a
- WHERE a.user_id = ? AND a.category_id = ? AND a.status <> 'deleted'`,
- [userId, categoryId]
- );
- return new Map(rows.map((row) => [row.original_filename, row]));
- };
- const loadDeletedWorkspaceChecksums = async (userId, categoryId) => {
- const [rows] = await pool2.query(
- `SELECT original_filename, checksum
- FROM h5_assets
- WHERE user_id = ? AND category_id = ? AND status = 'deleted' AND source_type = 'workspace'`,
- [userId, categoryId]
- );
- const byFilename = /* @__PURE__ */ new Map();
- for (const row of rows) {
- const checksums = byFilename.get(row.original_filename) ?? /* @__PURE__ */ new Set();
- checksums.add(row.checksum);
- byFilename.set(row.original_filename, checksums);
- }
- return byFilename;
- };
- const importWorkspaceFile = async (userId, category, file, buffer) => {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [spaces] = await conn.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes, status
- FROM h5_user_spaces
- WHERE id = ? AND user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [category.space_id, userId]
- );
- const space = spaces[0];
- if (!space || space.status !== "active") {
- throw Object.assign(new Error("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u7528"), { code: "space_unavailable" });
- }
- const available = asNumber3(space.quota_bytes) - asNumber3(space.used_bytes) - asNumber3(space.reserved_bytes);
- if (available < buffer.length) {
- throw Object.assign(new Error("\u5269\u4F59\u7A7A\u95F4\u4E0D\u8DB3"), { code: "quota_exceeded" });
- }
- const detectedMimeType = assetInternals.detectMimeType(buffer, file.filename);
- if (!detectedMimeType) {
- throw Object.assign(new Error("\u65E0\u6CD5\u786E\u8BA4\u6587\u4EF6\u7C7B\u578B"), { code: "unsupported_file_type" });
- }
- const scan = runBasicFileScan(buffer, {
- filename: file.filename,
- mimeType: detectedMimeType
- });
- const assetStatus = scan.scanStatus === "passed" ? "ready" : "quarantined";
- const versionScanStatus = scan.scanStatus === "passed" ? "passed" : "blocked";
- const checksum = crypto9.createHash("sha256").update(buffer).digest("hex");
- const assetId = idFactory();
- const versionId = idFactory();
- const finalStorageKey = buildWorkspaceStorageKey(
- userId,
- category.category_code,
- file.filename
- );
- const now = Date.now();
- const visibility = category.category_code === "public" ? "public_candidate" : "private";
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'workspace', ?, ?)`,
- [
- assetId,
- userId,
- category.space_id,
- category.id,
- assetInternals.assetTypeForMime(detectedMimeType),
- detectedMimeType,
- file.filename,
- path18.basename(file.filename, path18.extname(file.filename)) || file.filename,
- versionId,
- buffer.length,
- checksum,
- scan.riskLevel,
- visibility,
- assetStatus,
- 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, ?, ?, ?, ?, ?, '\u4ECE\u5DE5\u4F5C\u533A\u540C\u6B65', ?, ?)`,
- [
- versionId,
- assetId,
- finalStorageKey,
- buffer.length,
- checksum,
- detectedMimeType,
- userId,
- versionScanStatus,
- now
- ]
- );
- await conn.query(
- `UPDATE h5_user_spaces SET used_bytes = used_bytes + ?, updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [buffer.length, now, category.space_id, userId]
- );
- await conn.commit();
- 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;
- } finally {
- conn.release();
- }
- };
- const updateWorkspaceFile = async (userId, category, existing, file, buffer) => {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [spaces] = await conn.query(
- `SELECT id, quota_bytes, used_bytes, reserved_bytes, status
- FROM h5_user_spaces
- WHERE id = ? AND user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [category.space_id, userId]
- );
- const space = spaces[0];
- if (!space || space.status !== "active") {
- throw Object.assign(new Error("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u7528"), { code: "space_unavailable" });
- }
- const sizeDelta = buffer.length - asNumber3(existing.size_bytes);
- const available = asNumber3(space.quota_bytes) - asNumber3(space.used_bytes) - asNumber3(space.reserved_bytes);
- if (sizeDelta > 0 && available < sizeDelta) {
- throw Object.assign(new Error("\u5269\u4F59\u7A7A\u95F4\u4E0D\u8DB3"), { code: "quota_exceeded" });
- }
- const detectedMimeType = assetInternals.detectMimeType(buffer, file.filename);
- if (!detectedMimeType) {
- throw Object.assign(new Error("\u65E0\u6CD5\u786E\u8BA4\u6587\u4EF6\u7C7B\u578B"), { code: "unsupported_file_type" });
- }
- const scan = runBasicFileScan(buffer, {
- filename: file.filename,
- mimeType: detectedMimeType
- });
- const assetStatus = scan.scanStatus === "passed" ? "ready" : "quarantined";
- const versionScanStatus = scan.scanStatus === "passed" ? "passed" : "blocked";
- const checksum = crypto9.createHash("sha256").update(buffer).digest("hex");
- const [versionRows] = await conn.query(
- `SELECT COALESCE(MAX(version_no), 0) AS max_version
- FROM h5_asset_versions
- WHERE asset_id = ?`,
- [existing.id]
- );
- const versionNo = asNumber3(versionRows[0]?.max_version) + 1;
- const versionId = idFactory();
- const finalStorageKey = buildWorkspaceStorageKey(
- userId,
- category.category_code,
- file.filename
- );
- const now = Date.now();
- await conn.query(
- `UPDATE h5_assets
- SET current_version_id = ?, size_bytes = ?, checksum = ?, mime_type = ?,
- asset_type = ?, risk_level = ?, status = ?, updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [
- versionId,
- buffer.length,
- checksum,
- detectedMimeType,
- assetInternals.assetTypeForMime(detectedMimeType),
- scan.riskLevel,
- assetStatus,
- now,
- existing.id,
- userId
- ]
- );
- 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 (?, ?, ?, ?, ?, ?, ?, ?, '\u5DE5\u4F5C\u533A\u6587\u4EF6\u66F4\u65B0', ?, ?)`,
- [
- versionId,
- existing.id,
- versionNo,
- finalStorageKey,
- buffer.length,
- checksum,
- detectedMimeType,
- userId,
- versionScanStatus,
- now
- ]
- );
- if (sizeDelta !== 0) {
- await conn.query(
- `UPDATE h5_user_spaces SET used_bytes = GREATEST(0, used_bytes + ?), updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [sizeDelta, now, category.space_id, userId]
- );
- }
- await conn.commit();
- 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;
- } finally {
- conn.release();
- }
- };
- 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);
- const [categories] = await pool2.query(
- `SELECT c.id, c.space_id, c.category_code
- FROM h5_space_categories c
- WHERE c.user_id = ? AND c.category_code = ?
- LIMIT 1`,
- [userId, categoryCode]
- );
- const category = categories[0];
- if (!category) return { imported: 0, updated: 0, skipped: 0 };
- const existingByName = await loadExistingAssets(userId, category.id);
- const deletedWorkspaceChecksums = await loadDeletedWorkspaceChecksums(userId, category.id);
- let imported = 0;
- let updated = 0;
- let skipped = 0;
- for (const file of files) {
- if (file.sizeBytes <= 0 || file.sizeBytes > maxFileBytes) {
- skipped += 1;
- continue;
- }
- const buffer = await fsPromises2.readFile(file.absolutePath);
- const checksum = crypto9.createHash("sha256").update(buffer).digest("hex");
- if (deletedWorkspaceChecksums.get(file.filename)?.has(checksum)) {
- skipped += 1;
- continue;
- }
- const existing = existingByName.get(file.filename);
- if (existing?.checksum === checksum) {
- skipped += 1;
- continue;
- }
- if (existing) {
- const result = await updateWorkspaceFile(userId, category, existing, file, buffer);
- await registerWorkspaceArtifactForConversation(userId, source, result);
- existing.checksum = checksum;
- updated += 1;
- } else {
- 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, 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, source);
- imported += result.imported;
- updated += result.updated;
- skipped += result.skipped;
- }
- return { imported, updated, skipped };
- };
- return { syncUserWorkspace, syncCategory, listWorkspaceZoneFiles };
-}
-function startWorkspaceAssetSyncWatcher({ publishRoot, syncUserWorkspaceByDirKey }) {
- if (!publishRoot || !syncUserWorkspaceByDirKey) return () => {
- };
- const pending = /* @__PURE__ */ new Map();
- const schedule = (dirKey, categoryCode) => {
- const key = `${dirKey}:${categoryCode ?? "all"}`;
- const existing = pending.get(key);
- if (existing) clearTimeout(existing);
- pending.set(
- key,
- setTimeout(() => {
- pending.delete(key);
- void syncUserWorkspaceByDirKey(dirKey, categoryCode ? { categoryCode } : {}).catch(() => {
- });
- }, 600)
- );
- };
- const watchZoneDir = (dirKey, zoneDir, categoryCode) => {
- try {
- fs14.watch(
- zoneDir,
- { recursive: true },
- (_event, filename) => {
- if (!filename) {
- schedule(dirKey, categoryCode);
- return;
- }
- const normalized = String(filename).replace(/\\/g, "/");
- if (shouldSyncWorkspaceFilename(normalized)) {
- schedule(dirKey, categoryCode);
- }
- }
- );
- } catch {
- }
- };
- const attachUser = async (dirKey) => {
- const userDir = path18.join(publishRoot, dirKey);
- try {
- await fsPromises2.access(userDir);
- } catch {
- return;
- }
- for (const categoryCode of UPLOAD_ZONE_CODES) {
- watchZoneDir(dirKey, resolveZoneDir(userDir, categoryCode), categoryCode);
- }
- };
- for (const entry of fs14.readdirSync(publishRoot, { withFileTypes: true })) {
- if (!entry.isDirectory() || entry.name === "wiki" || entry.name.startsWith(".")) continue;
- void attachUser(entry.name);
- }
- try {
- fs14.watch(publishRoot, (_event, filename) => {
- if (!filename) return;
- const userDir = path18.join(publishRoot, filename);
- if (fs14.existsSync(userDir) && fs14.statSync(userDir).isDirectory()) {
- void attachUser(filename);
- }
- });
- } catch {
- }
- return () => {
- for (const timer of pending.values()) clearTimeout(timer);
- pending.clear();
- };
-}
-
-// api-response.mjs
-import crypto10 from "node:crypto";
-function createRequestId(existing) {
- if (typeof existing === "string" && existing.trim()) return existing.trim();
- return `req_${crypto10.randomUUID()}`;
-}
-function attachRequestId(req, res, next) {
- const requestId = createRequestId(req.get("x-request-id"));
- req.requestId = requestId;
- res.setHeader("X-Request-Id", requestId);
- next();
-}
-function sendJson(res, req, status, body) {
- const requestId = req?.requestId ?? createRequestId();
- return res.status(status).json({ ...body, request_id: requestId });
-}
-function sendData(res, req, data, status = 200) {
- return sendJson(res, req, status, { data });
-}
-function sendError(res, req, status, code, message, details) {
- return sendJson(res, req, status, {
- error: {
- code,
- message,
- ...details ? { details } : {}
- }
- });
-}
-
-// notification-dispatcher.mjs
-function createNotificationDispatcher({ sendWechatTextToUser, logger = console } = {}) {
- const sendWechat = async (userId, text) => {
- if (typeof sendWechatTextToUser !== "function") return false;
- await sendWechatTextToUser(userId, text);
- return true;
- };
- return {
- async sendRechargeSuccess({ userId, title, body, dedupeKey = null }) {
- const sent = await sendWechat(userId, `${title}
-${body}`.trim());
- logger.info?.("Notification dispatch:", {
- type: "recharge_success",
- userId,
- dedupeKey,
- sent
- });
- return sent;
- },
- async sendScheduleNotification({ userId, text }) {
- const sent = await sendWechat(userId, text);
- logger.info?.("Notification dispatch:", {
- type: "schedule_notification",
- userId,
- dedupeKey: null,
- sent
- });
- return sent;
- }
- };
-}
-
-// mindspace-audit.mjs
-function createMindSpaceAuditWriter(pool2) {
- const write = async ({
- userId,
- action,
- objectType,
- objectId,
- ip = null,
- result = "success",
- riskLevel = null,
- now = Date.now()
- }) => {
- if (!pool2) return;
- if (!userId) return;
- await pool2.query(
- `INSERT INTO h5_mindspace_audit_logs
- (user_id, action, object_type, object_id, ip, result, risk_level, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
- [userId, action, objectType, objectId, ip, result, riskLevel, now]
- );
- };
- return { write };
-}
-
-// mindspace-flags.mjs
-function mindspaceFlags(env = process.env) {
- const enabled = env.MINDSPACE_ENABLED !== "false";
- return {
- mindspace_enabled: enabled,
- mindspace_upload_enabled: enabled && env.MINDSPACE_UPLOAD_ENABLED !== "false",
- mindspace_publish_enabled: enabled && env.MINDSPACE_PUBLISH_ENABLED === "true",
- mindspace_private_enabled: false,
- mindspace_agent_jobs_enabled: enabled && env.MINDSPACE_AGENT_JOBS_ENABLED === "true"
- };
-}
-function assertMindSpaceRoute(flags, route) {
- if (!flags.mindspace_enabled) {
- throw Object.assign(new Error("MindSpace \u529F\u80FD\u672A\u542F\u7528"), { code: "feature_disabled" });
- }
- if (route === "upload" && !flags.mindspace_upload_enabled) {
- throw Object.assign(new Error("\u4E0A\u4F20\u529F\u80FD\u672A\u542F\u7528"), { code: "feature_disabled" });
- }
- if (route === "agent" && !flags.mindspace_agent_jobs_enabled) {
- throw Object.assign(new Error("Agent \u4EFB\u52A1\u529F\u80FD\u672A\u542F\u7528"), { code: "feature_disabled" });
- }
-}
-
-// mindspace-pages.mjs
-import crypto12 from "node:crypto";
-import fs17 from "node:fs/promises";
-import path21 from "node:path";
-
-// mindspace-content-scan.mjs
-import crypto11 from "node:crypto";
-var RISK_ORDER = ["none", "low", "medium", "high", "critical"];
-var HTML_TAG_PATTERN = /<[^>]+>/;
-var PRIVATE_URL_PATTERN = /(file:\/\/[^\s"'<>]+|\/api\/mindspace\/v1\/assets\/[a-z0-9-]+(?:\/[a-z-]+)?|\/users\/[^\s"'<>]+)/gi;
-var TEXT_RULES = [
- {
- type: "id_card",
- label: "\u8EAB\u4EFD\u8BC1\u53F7",
- riskLevel: "high",
- blocking: true,
- pattern: /(? `${value.slice(0, 6)}********${value.slice(-4)}`,
- replacement: (value) => `${value.slice(0, 6)}********${value.slice(-4)}`
- },
- {
- type: "bank_card",
- label: "\u94F6\u884C\u5361\u53F7",
- riskLevel: "high",
- blocking: true,
- pattern: /(? `${value.slice(0, 4)} **** **** ${value.slice(-4)}`,
- replacement: (value) => `${value.slice(0, 4)} **** **** ${value.slice(-4)}`
- },
- {
- type: "phone",
- label: "\u624B\u673A\u53F7",
- riskLevel: "medium",
- blocking: false,
- pattern: /(? `${value.slice(0, 3)}****${value.slice(-4)}`,
- replacement: (value) => `${value.slice(0, 3)}****${value.slice(-4)}`
- },
- {
- type: "email",
- label: "\u90AE\u7BB1\u5730\u5740",
- riskLevel: "medium",
- blocking: false,
- pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
- mask: (value) => `${value.slice(0, 2)}***@***`,
- replacement: () => "[\u90AE\u7BB1\u5730\u5740]"
- },
- {
- type: "api_key",
- label: "API Key",
- riskLevel: "critical",
- blocking: true,
- pattern: /\b(?:sk|rk|pk)_(?:test_|live_|proj_)?[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}\b|ghp_[A-Za-z0-9]{20,}\b|AIza[0-9A-Za-z\-_]{20,}\b/g,
- mask: (value) => `${value.slice(0, 4)}********${value.slice(-4)}`,
- replacement: () => "[API_KEY]"
- },
- {
- type: "access_token",
- label: "\u8BBF\u95EE\u4EE4\u724C",
- riskLevel: "critical",
- blocking: true,
- pattern: /\b(?:access[_-]?token|refresh[_-]?token|authorization)\b\s*[:=]?\s*(?:bearer\s+)?[A-Za-z0-9\-_=+.]{16,}/gi,
- mask: (value) => `${value.slice(0, 10)}********`,
- replacement: (value) => value.replace(/[A-Za-z0-9\-_=+.]{16,}$/i, "[TOKEN]")
- },
- {
- type: "private_key",
- label: "\u79C1\u94A5",
- riskLevel: "critical",
- blocking: true,
- pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
- mask: () => "-----BEGIN PRIVATE KEY-----********",
- replacement: () => "[PRIVATE_KEY]"
- },
- {
- type: "external_link",
- label: "\u5916\u90E8\u94FE\u63A5",
- riskLevel: "low",
- blocking: false,
- pattern: /https?:\/\/[^\s<>"')]+/gi,
- mask: (value) => value.slice(0, 80),
- replacement: (value) => value
- }
-];
-var HTML_RULES = [
- {
- type: "html_script",
- label: "\u811A\u672C\u6807\u7B7E",
- riskLevel: "critical",
- blocking: true,
- pattern: /",
- replacement: () => ""
- },
- {
- type: "html_inline_handler",
- label: "\u5185\u8054\u4E8B\u4EF6",
- riskLevel: "high",
- blocking: true,
- pattern: /\son[a-z]+\s*=\s*(['"]).*?\1/gi,
- mask: () => "on*=\u2026",
- replacement: () => ""
- },
- {
- type: "html_javascript_url",
- label: "javascript \u94FE\u63A5",
- riskLevel: "high",
- blocking: true,
- pattern: /(href|src)\s*=\s*(['"])\s*javascript:[\s\S]*?\2/gi,
- mask: (value) => value.slice(0, 40),
- replacement: (_value, match) => `${match[1]}=${match[2]}#${match[2]}`
- },
- {
- type: "html_forbidden_embed",
- label: "\u5D4C\u5165\u5F0F\u5916\u90E8\u5185\u5BB9",
- riskLevel: "high",
- blocking: true,
- pattern: /<(iframe|object|embed)\b[\s\S]*?>[\s\S]*?(?:<\/\1>|$)/gi,
- mask: (value, match) => `<${match[1]}>\u2026${match[1]}>`,
- replacement: () => ""
- },
- {
- type: "html_form_action",
- label: "\u8868\u5355\u63D0\u4EA4",
- riskLevel: "high",
- blocking: true,
- pattern: /",
- replacement: () => ""
- },
- {
- type: "html_meta_refresh",
- label: "\u9875\u9762\u8DF3\u8F6C",
- riskLevel: "medium",
- blocking: true,
- pattern: /]*http-equiv\s*=\s*(['"])refresh\1[^>]*>/gi,
- mask: () => '',
- replacement: () => ""
- },
- {
- type: "private_resource_reference",
- label: "\u79C1\u6709\u8D44\u6E90\u5F15\u7528",
- riskLevel: "high",
- blocking: true,
- pattern: PRIVATE_URL_PATTERN,
- mask: (value) => value.slice(0, 48),
- replacement: () => "#private-resource-redacted"
- }
-];
-var ACKNOWLEDGEABLE_HTML_ACTIVE_TYPES = /* @__PURE__ */ new Set([
- "html_script",
- "html_inline_handler"
-]);
-var TRUSTED_EXTERNAL_HOSTS = /* @__PURE__ */ new Set([
- "fonts.googleapis.com",
- "fonts.gstatic.com",
- "images.unsplash.com"
-]);
-function replacePrivateResourceReferences(content, replacement) {
- const source = String(content ?? "");
- if (!source) return source;
- const replacer = typeof replacement === "function" ? replacement : () => String(replacement ?? "");
- return source.replace(PRIVATE_URL_PATTERN, (...args) => replacer(...args));
-}
-function inferFormat(content, format) {
- if (format === "html") return "html";
- return HTML_TAG_PATTERN.test(String(content ?? "")) ? "html" : "text";
-}
-function isTrustedExternalUrl(value) {
- try {
- return TRUSTED_EXTERNAL_HOSTS.has(new URL(value).hostname);
- } catch {
- return false;
- }
-}
-function riskMax(left, right) {
- return RISK_ORDER.indexOf(right) > RISK_ORDER.indexOf(left) ? right : left;
-}
-function uniqueMatches(content, pattern) {
- return [
- ...new Map(
- [...String(content).matchAll(pattern)].map((match) => [
- match[0].toLowerCase(),
- match
- ])
- ).values()
- ];
-}
-function applyRule(content, rule, findings, redactions, options = {}) {
- let matches = uniqueMatches(content, rule.pattern);
- if (rule.type === "external_link") {
- matches = matches.filter((match) => !isTrustedExternalUrl(match[0]));
- }
- if (!matches.length) return content;
- const blocking = options.allowHtmlActiveContent && ACKNOWLEDGEABLE_HTML_ACTIVE_TYPES.has(rule.type) ? false : rule.blocking;
- findings.push({
- id: crypto11.createHash("sha256").update(`${rule.type}:${matches[0][0]}`).digest("hex").slice(0, 24),
- type: rule.type,
- label: rule.label,
- riskLevel: rule.riskLevel,
- occurrenceCount: matches.length,
- sampleMasked: rule.mask(matches[0][0], matches[0]),
- blocking
- });
- let next = String(content);
- if (typeof rule.replacement === "function") {
- next = next.replace(rule.pattern, (...args) => rule.replacement(...args));
- redactions.push(rule.type);
- }
- return next;
-}
-function finalizeResult(findings) {
- const riskLevel = findings.reduce((highest, finding) => riskMax(highest, finding.riskLevel), "none");
- return {
- status: findings.some((finding) => finding.blocking) ? "blocked" : findings.length ? "warned" : "passed",
- riskLevel,
- findings,
- allowed: !findings.some((finding) => finding.blocking)
- };
-}
-function scanContent(content, options = {}) {
- const format = inferFormat(content, options.format);
- const findings = [];
- const working = String(content ?? "");
- for (const rule of TEXT_RULES) {
- applyRule(working, rule, findings, [], options);
- }
- if (format === "html") {
- for (const rule of HTML_RULES) {
- applyRule(working, rule, findings, [], options);
- }
- }
- return finalizeResult(findings);
-}
-function redactContent(content, options = {}) {
- const format = inferFormat(content, options.format);
- const findings = [];
- const redactions = [];
- let next = String(content ?? "");
- for (const rule of TEXT_RULES) {
- next = applyRule(next, rule, findings, redactions);
- }
- if (format === "html") {
- for (const rule of HTML_RULES) {
- next = applyRule(next, rule, findings, redactions);
- }
- }
- const result = finalizeResult(findings);
- return {
- ...result,
- content: next,
- format,
- redactionCount: redactions.length,
- changed: next !== String(content ?? "")
- };
-}
-
-// mindspace-html-download-links.mjs
-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) {
- const trimmed = String(value ?? "").trim();
- if (!trimmed) return false;
- const pathPart = trimmed.split("#")[0].split("?")[0];
- if (/^([a-z][a-z0-9+.-]*:|\/|#|data:|mailto:|javascript:)/i.test(pathPart)) return false;
- return DOWNLOADABLE_FILE_PATTERN.test(pathPart);
-}
-function splitReferenceParts(relativeRef) {
- const value = String(relativeRef ?? "");
- const match = value.match(/^([^#?]+)(.*)$/);
- return {
- pathPart: match?.[1] ?? value,
- suffix: match?.[2] ?? ""
- };
-}
-function resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef) {
- const { pathPart } = splitReferenceParts(relativeRef);
- const ref = String(pathPart ?? "").replace(/^\/+/, "");
- if (!ref) return ref;
- if (/^(?:public|oa|private)\//.test(ref)) return ref;
- const htmlPath = String(htmlRelativePath ?? "public/index.html").replace(/^\/+/, "");
- const htmlDir = path19.posix.dirname(htmlPath);
- if (!htmlDir || htmlDir === ".") return `public/${ref}`;
- return path19.posix.join(htmlDir, ref).replace(/\\/g, "/");
-}
-function resolveSiblingDocxPath(htmlRelativePath) {
- const htmlPath = String(htmlRelativePath ?? "").replace(/^\/+/, "");
- if (!htmlPath.toLowerCase().endsWith(".html")) return null;
- return htmlPath.replace(/\.html$/i, ".docx");
-}
-function inferWorkspaceHtmlRelativePath({
- snapshotRelativePath = null,
- pageTitle = null,
- htmlContent = null,
- publishDir = null
-} = {}) {
- const fromSnapshot = String(snapshotRelativePath ?? "").trim();
- if (fromSnapshot) {
- const normalized = fromSnapshot.replace(/^\/+/, "");
- if (normalized.toLowerCase().startsWith("public/")) return normalized;
- if (normalized.toLowerCase().endsWith(".html")) return `public/${path19.posix.basename(normalized)}`;
- return normalized;
- }
- 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 fs15.readdirSync(publicDir)) {
- if (!name.toLowerCase().endsWith(".html")) continue;
- const absolutePath = path19.join(publicDir, name);
- try {
- const diskContent = fs15.readFileSync(absolutePath, "utf8");
- if (diskContent === normalizedContent) return `public/${name}`;
- if (title) {
- const titleMatch = diskContent.match(/]*>([^<]+)<\/title>/i);
- if (titleMatch?.[1]?.trim() === title) return `public/${name}`;
- }
- } catch {
- }
- }
- return "public/index.html";
-}
-function buildWorkspaceDownloadLinkIndex(assets = []) {
- const byBasename = /* @__PURE__ */ new Map();
- const byPath = /* @__PURE__ */ new Map();
- for (const asset of assets) {
- const id = asset.id;
- const filename = String(asset.original_filename ?? asset.originalFilename ?? "").replace(/\\/g, "/");
- if (!id || !filename) continue;
- byPath.set(filename, id);
- byBasename.set(path19.posix.basename(filename), id);
- if (filename.startsWith("public/")) {
- byPath.set(filename.slice("public/".length), id);
- } else if (!filename.includes("/")) {
- byPath.set(`public/${filename}`, id);
- }
- }
- return { byBasename, byPath };
-}
-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(path19.posix.basename(normalized)) ?? null;
-}
-function buildAssetDownloadUrl(assetId) {
- return `/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download`;
-}
-function buildPublicWorkspaceFileUrl(publicBaseUrl, publishKey, workspaceRelativePath) {
- const clean = String(workspaceRelativePath ?? "").replace(/^\/+/, "");
- return buildPublicUrl(publicBaseUrl, publishKey, clean);
-}
-function workspaceFileExists(publishDir, workspaceRelativePath) {
- if (!publishDir || !workspaceRelativePath) return false;
- const absolutePath = path19.join(publishDir, ...String(workspaceRelativePath).split("/"));
- try {
- return fs15.existsSync(absolutePath) && fs15.statSync(absolutePath).isFile();
- } catch {
- return false;
- }
-}
-function collectDownloadWorkspaceCandidates(htmlRelativePath, relativeRef, publishDir) {
- const workspacePath = resolveWorkspaceRelativeFilePath(htmlRelativePath, relativeRef);
- const candidates = /* @__PURE__ */ new Set([
- workspacePath,
- path19.posix.join("public", path19.posix.basename(workspacePath))
- ]);
- const siblingDocx = resolveSiblingDocxPath(htmlRelativePath);
- if (siblingDocx) {
- candidates.add(siblingDocx);
- candidates.add(path19.posix.join("public", path19.posix.basename(siblingDocx)));
- }
- return [...candidates];
-}
-function resolveDownloadTargetUrl({
- relativeRef,
- htmlRelativePath = "public/index.html",
- publishDir = null,
- linkIndex = null,
- publicBaseUrl = "",
- publishKey = null,
- preferAssetDownload = true
-}) {
- const { suffix } = splitReferenceParts(relativeRef);
- const candidates = collectDownloadWorkspaceCandidates(htmlRelativePath, relativeRef, publishDir);
- for (const candidate of candidates) {
- const assetId = lookupAssetIdForWorkspacePath(linkIndex, candidate);
- if (preferAssetDownload && assetId) {
- return `${buildAssetDownloadUrl(assetId)}${suffix}`;
- }
- }
- if (publishDir && publishKey) {
- for (const candidate of candidates) {
- if (workspaceFileExists(publishDir, candidate)) {
- return `${buildPublicWorkspaceFileUrl(publicBaseUrl, publishKey, candidate)}${suffix}`;
- }
- }
- }
- for (const candidate of candidates) {
- const assetId = lookupAssetIdForWorkspacePath(linkIndex, candidate);
- if (assetId) {
- return `${buildAssetDownloadUrl(assetId)}${suffix}`;
- }
- }
- return null;
-}
-function rewriteRelativeDownloadLinks(html, options = {}) {
- const source = String(html ?? "");
- if (!source) return { html: source, count: 0 };
- let count = 0;
- const rewritten = source.replace(URL_ATTR_PATTERN, (full, prefix, url, suffix) => {
- if (!isRelativeDownloadReference(url)) return full;
- const resolved = resolveDownloadTargetUrl({ ...options, relativeRef: url });
- if (!resolved) return full;
- count += 1;
- return `${prefix}${resolved}${suffix}`;
- });
- return { html: rewritten, count };
-}
-async function loadWorkspaceDownloadLinkIndex(pool2, userId) {
- const [rows] = await pool2.query(
- `SELECT id, original_filename
- FROM h5_assets
- WHERE user_id = ? AND status <> 'deleted'`,
- [userId]
- );
- return buildWorkspaceDownloadLinkIndex(rows);
-}
-async function prepareHtmlDownloadLinks(pool2, userId, html, options = {}) {
- const linkIndex = options.linkIndex ?? await loadWorkspaceDownloadLinkIndex(pool2, userId);
- return rewriteRelativeDownloadLinks(html, { ...options, linkIndex });
-}
-
-// mindspace-page-tag.mjs
-var MINDSPACE_PAGE_TAG_ATTR = "data-mindspace-page-tag";
-var MINDSPACE_PAGE_TAG_PLATFORM_BRAND = "platform-brand";
-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");
-}
-function normalizePlatformBrandHtml(html) {
- let next = normalizePlatformDomainText(html);
- next = next.replace(/mailto:([^"'>\s]+@tkmind)\.ai/gi, "mailto:$1.cn");
- next = next.replace(
- /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) {
- const normalized = normalizePlatformBrandHtml(html);
- return normalized.replace(SIMPLE_BRAND_ELEMENT_RE, (match, tag, attrs, inner) => {
- const attrStr = attrs ?? "";
- if (attrStr.includes(MINDSPACE_PAGE_TAG_ATTR)) return match;
- if (!PLATFORM_BRAND_INNER_RE.test(inner)) return match;
- const spacer = attrStr ? "" : " ";
- return `<${tag}${attrStr}${spacer}${MINDSPACE_PAGE_TAG_ATTR}="${MINDSPACE_PAGE_TAG_PLATFORM_BRAND}">${inner}${tag}>`;
- });
-}
-function prepareHtmlPageBrandMarkers(html) {
- return ensurePlatformBrandPageTags(html);
-}
-
-// mindspace-page-purge.mjs
-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) {
- return [
- ...new Set(
- [...String(html ?? "").matchAll(PRIVATE_ASSET_URL_PATTERN)].map((match) => match[1]).filter(Boolean)
- )
- ];
-}
-function extractRelativeDownloadRefs(html) {
- const refs = /* @__PURE__ */ new Set();
- for (const match of String(html ?? "").matchAll(URL_ATTR_PATTERN2)) {
- const url = match[2];
- if (isRelativeDownloadReference(url)) {
- refs.add(splitReferenceParts(url).pathPart);
- }
- }
- return [...refs];
-}
-function collectWorkspacePurgeTargets(htmlRelativePath, html) {
- const targets = /* @__PURE__ */ new Set();
- const normalizedHtmlPath = String(htmlRelativePath ?? "").replace(/^\/+/, "");
- if (normalizedHtmlPath) {
- targets.add(normalizedHtmlPath);
- if (normalizedHtmlPath.toLowerCase().endsWith(".html")) {
- targets.add(workspaceThumbnailRelativePath(normalizedHtmlPath));
- const pngThumb = normalizedHtmlPath.replace(/\.html$/i, ".thumbnail.png");
- if (pngThumb !== normalizedHtmlPath) targets.add(pngThumb);
- }
- }
- for (const ref of extractRelativeDownloadRefs(html)) {
- targets.add(resolveWorkspaceRelativeFilePath(normalizedHtmlPath || "public/index.html", ref));
- }
- return [...targets].filter(Boolean);
-}
-async function purgeWorkspacePageArtifacts({
- publishDir,
- htmlRelativePath = null,
- html = ""
-} = {}) {
- if (!publishDir) return { removed: [], skipped: [] };
- const resolvedRoot = path20.resolve(publishDir);
- const removed = [];
- const skipped = [];
- for (const relativeTarget of collectWorkspacePurgeTargets(htmlRelativePath, html)) {
- const absolutePath = path20.resolve(publishDir, ...relativeTarget.split("/"));
- if (absolutePath !== resolvedRoot && !absolutePath.startsWith(`${resolvedRoot}${path20.sep}`)) {
- skipped.push(relativeTarget);
- continue;
- }
- try {
- await fs16.unlink(absolutePath);
- removed.push(relativeTarget);
- } catch (error) {
- if (error?.code !== "ENOENT") skipped.push(relativeTarget);
- }
- }
- return { removed, skipped };
-}
-
-// mindspace-cover-meta.mjs
-function escapeMetaAttribute(value) {
- return String(value ?? "").replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
-}
-function parseMindspaceCoverMeta(html) {
- const tag = String(html).match(/]*name=["']mindspace-cover["'][^>]*>/i)?.[0];
- if (!tag) return null;
- const contentMatch = tag.match(/content=(["'])([\s\S]*?)\1/i) ?? tag.match(/content=["']([^"']+)["']/i);
- const raw = contentMatch?.[2] ?? contentMatch?.[1];
- if (!raw) return null;
- try {
- return JSON.parse(raw.replaceAll(""", '"'));
- } catch {
- return null;
- }
-}
-function upsertMindspaceCoverMeta(html, coverMeta) {
- const payload = { ...parseMindspaceCoverMeta(html) ?? {}, ...coverMeta };
- const metaTag = ``;
- const source = String(html ?? "");
- if (/]*name=["']mindspace-cover["'][^>]*>/i.test(source)) {
- return source.replace(/]*name=["']mindspace-cover["'][^>]*>/i, metaTag);
- }
- if (/]*>/i.test(source)) {
- return source.replace(/]*)>/i, `
- ${metaTag}`);
- }
- if (/]*>/i.test(source)) {
- return source.replace(/]*)>/i, `${metaTag}`);
- }
- return `${metaTag}${source}`;
-}
-function normalizeCoverMetaSuggestion(raw) {
- const payload = raw && typeof raw === "object" ? raw : {};
- const next = {};
- if (payload.tag != null) next.tag = String(payload.tag).trim().slice(0, 12);
- if (payload.emoji != null) next.emoji = String(payload.emoji).trim().slice(0, 4);
- if (payload.accent != null) next.accent = String(payload.accent).trim();
- if (payload.accent2 != null) next.accent2 = String(payload.accent2).trim();
- if (payload.subtitle != null) next.subtitle = String(payload.subtitle).trim().slice(0, 80);
- if (payload.cover != null) next.cover = String(payload.cover).trim().slice(0, 512);
- if (payload.image != null && !next.cover) next.cover = String(payload.image).trim().slice(0, 512);
- if (payload.mood != null) next.mood = String(payload.mood).trim().slice(0, 24);
- return next;
-}
-
-// mindspace-pages.mjs
-var MAX_TITLE_LENGTH = 255;
-var MAX_SUMMARY_LENGTH = 1e3;
-var MAX_CONTENT_BYTES = 1024 * 1024;
-var TEMPLATE_IDS = /* @__PURE__ */ new Set(["editorial", "report", "profile", "knowledge-card", "static-html"]);
-var SAVE_CATEGORY_CODES = /* @__PURE__ */ new Set(["draft", "oa", "private", "public"]);
-var PRIVATE_ASSET_URL_PATTERN2 = /(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi;
-function asNumber4(value) {
- return Number(value ?? 0);
-}
-function parseJsonColumn(value, fallback = {}) {
- if (value == null || value === "") return { ...fallback };
- if (typeof value === "object" && !Buffer.isBuffer(value)) return value;
- try {
- return JSON.parse(String(value));
- } catch {
- return { ...fallback };
- }
-}
-function pageError(message, code, details) {
- return Object.assign(new Error(message), { code, details });
-}
-function normalizeWorkspaceRelativePath2(relativePath) {
- const parts = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== "..");
- if (parts.length === 0) return "";
- if (parts[0].toLowerCase() === "public") return parts.join("/");
- if (parts.length === 1 && parts[0].toLowerCase().endsWith(".html")) return `public/${parts[0]}`;
- return parts.join("/");
-}
-function normalizeText(value, maxLength, fieldName) {
- const text = String(value ?? "").normalize("NFKC").trim();
- if (!text) throw pageError(`${fieldName}\u4E0D\u80FD\u4E3A\u7A7A`, "invalid_page_input");
- if (text.length > maxLength) {
- throw pageError(`${fieldName}\u8D85\u8FC7\u957F\u5EA6\u9650\u5236`, "invalid_page_input");
- }
- return text;
-}
-function normalizePageInput(input) {
- const title = normalizeText(input.title, MAX_TITLE_LENGTH, "\u6807\u9898");
- let content = normalizeText(input.content, MAX_CONTENT_BYTES, "\u9875\u9762\u5185\u5BB9");
- const contentFormat = input.contentFormat === "html" ? "html" : "markdown";
- if (contentFormat === "html") {
- content = prepareHtmlPageBrandMarkers(content);
- }
- const contentBytes = Buffer.byteLength(content, "utf8");
- if (contentBytes > MAX_CONTENT_BYTES) {
- throw pageError("\u9875\u9762\u5185\u5BB9\u8D85\u8FC7 1 MB \u9650\u5236", "page_content_too_large");
- }
- const summary = String(input.summary ?? "").normalize("NFKC").trim().slice(0, MAX_SUMMARY_LENGTH);
- const templateId = contentFormat === "html" ? "static-html" : TEMPLATE_IDS.has(input.templateId) ? input.templateId : "editorial";
- const plainSummary = contentFormat === "html" ? content.replace(/`;
-var EMBED_LAYOUT_OVERRIDES = '';
-function stripPublicationHtmlCspMeta(html) {
- return String(html ?? "").replace(
- /]*>/gi,
- ""
- );
-}
-function preparePublicationHtmlForEmbed(html) {
- let source = stripPublicationHtmlCspMeta(html);
- if (!source.includes('id="plaza-embed-overrides"') && /]*>/i.test(source)) {
- source = source.replace(/]*)>/i, `${EMBED_LAYOUT_OVERRIDES}`);
- }
- return injectPlazaEmbedBootstrap(source);
-}
-function injectPlazaEmbedBootstrap(html) {
- const source = String(html ?? "");
- if (source.includes('id="plaza-embed-bootstrap"')) return source;
- if (/<\/body>/i.test(source)) {
- return source.replace(/<\/body>/i, `${EMBED_BOOTSTRAP}`);
- }
- return `${source}${EMBED_BOOTSTRAP}`;
-}
-
-// mindspace-cleanup.mjs
-import crypto22 from "node:crypto";
-import fs22 from "node:fs/promises";
-import path26 from "node:path";
-var WORKSPACE_TEMP_SKIP = /* @__PURE__ */ new Set([
- ".tkmindhints",
- ".goosehints",
- ".tkmind-profile.json",
- ".agents",
- ".goose"
-]);
-function candidateId(kind, key) {
- return `${kind}:${crypto22.createHash("sha256").update(key).digest("hex").slice(0, 24)}`;
-}
-async function fileSize(targetPath) {
- try {
- const stat = await fs22.stat(targetPath);
- return stat.isFile() ? stat.size : 0;
- } catch {
- return 0;
- }
-}
-async function walkFiles2(rootDir, onFile) {
- let entries;
- try {
- entries = await fs22.readdir(rootDir, { withFileTypes: true });
- } catch {
- return;
- }
- for (const entry of entries) {
- const fullPath = path26.join(rootDir, entry.name);
- if (entry.isDirectory()) {
- if (WORKSPACE_TEMP_SKIP.has(entry.name)) continue;
- await walkFiles2(fullPath, onFile);
- continue;
- }
- if (!entry.isFile()) continue;
- await onFile(fullPath);
- }
-}
-function createCleanupService(pool2, options = {}) {
- 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 = path26.resolve(storageRoot, storageKey);
- if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path26.sep}`)) {
- throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C");
- }
- return resolved;
- };
- const listCandidates = async (userId, username) => {
- const candidates = [];
- const [uploads] = await pool2.query(
- `SELECT id, filename, reserved_bytes, temporary_storage_key, status, expires_at, created_at
- FROM h5_upload_sessions
- WHERE user_id = ? AND status IN ('reserved', 'uploaded', 'expired', 'failed')
- ORDER BY created_at DESC
- LIMIT 100`,
- [userId]
- );
- for (const upload of uploads) {
- const storagePath = upload.temporary_storage_key ? absoluteStoragePath2(upload.temporary_storage_key) : null;
- const exists = storagePath ? await fileSize(storagePath) : 0;
- if (!exists && upload.status === "expired") continue;
- candidates.push({
- id: candidateId("upload", upload.id),
- kind: "stale_upload",
- label: upload.filename,
- path: upload.temporary_storage_key ?? "",
- sizeBytes: exists || Number(upload.reserved_bytes ?? 0),
- createdAt: Number(upload.created_at),
- detail: upload.status === "reserved" ? "\u672A\u5B8C\u6210\u7684\u4E0A\u4F20\u9884\u7559" : upload.status === "uploaded" ? "\u5DF2\u4E0A\u4F20\u4F46\u672A\u5B8C\u6210\u5165\u5E93" : "\u5DF2\u5931\u6548\u7684\u4E0A\u4F20\u4E34\u65F6\u6587\u4EF6",
- refId: upload.id
- });
- }
- 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"
- );
- if (active) return;
- const sizeBytes = await fileSize(fullPath);
- if (!sizeBytes) return;
- candidates.push({
- id: candidateId("tmp", key),
- kind: "orphan_tmp",
- label: path26.basename(fullPath),
- path: key,
- sizeBytes,
- createdAt: null,
- detail: "\u5B64\u7ACB\u7684\u4E34\u65F6\u4E0A\u4F20\u6587\u4EF6",
- refId: key
- });
- });
- 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({
- id: candidateId("workspace", rel),
- kind: "workspace_temp",
- label: rel,
- path: `temp/${username}/${rel}`,
- sizeBytes,
- createdAt: null,
- detail: "Agent \u5DE5\u4F5C\u533A\u4E34\u65F6\u6587\u4EF6",
- refId: rel
- });
- });
- const [staleVersions] = await pool2.query(
- `SELECT pv.id AS version_id, pv.page_id, pv.version_no, pv.content_asset_id, pv.bundle_asset_id,
- p.title, a.size_bytes, a.display_name, pv.created_at,
- COALESCE(bundle.size_bytes, 0) AS bundle_size_bytes
- FROM h5_page_versions pv
- JOIN h5_page_records p ON p.id = pv.page_id AND p.user_id = ? AND p.status <> 'deleted'
- JOIN h5_assets a ON a.id = pv.content_asset_id AND a.user_id = ? AND a.status <> 'deleted'
- LEFT JOIN h5_assets bundle
- ON bundle.id = pv.bundle_asset_id AND bundle.user_id = ? AND bundle.status <> 'deleted'
- WHERE pv.id <> p.current_version_id
- AND NOT EXISTS (
- SELECT 1 FROM h5_publish_records pr
- WHERE pr.page_version_id = pv.id AND pr.user_id = ?
- )
- ORDER BY pv.created_at ASC
- LIMIT 500`,
- [userId, userId, userId, userId]
- );
- for (const row of staleVersions) {
- const contentBytes = Number(row.size_bytes ?? 0);
- const bundleBytes = Number(row.bundle_size_bytes ?? 0);
- candidates.push({
- id: candidateId("page_version", row.version_id),
- kind: "stale_page_version",
- label: `${row.title} \xB7 v${row.version_no}`,
- path: `page/${row.page_id}/version/${row.version_no}`,
- sizeBytes: contentBytes + bundleBytes,
- createdAt: Number(row.created_at),
- detail: "\u9875\u9762\u5386\u53F2\u7248\u672C\uFF08\u975E\u5F53\u524D\u7248\u672C\uFF09",
- refId: row.version_id
- });
- }
- return candidates;
- };
- const runCleanup = async (userId, username, itemIds) => {
- const selected = new Set(itemIds ?? []);
- const candidates = await listCandidates(userId, username);
- const targets = candidates.filter((item) => selected.has(item.id));
- let freedBytes = 0;
- let removedCount = 0;
- for (const item of targets) {
- if (item.kind === "stale_upload") {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT id, space_id, reserved_bytes, temporary_storage_key, status
- FROM h5_upload_sessions
- WHERE id = ? AND user_id = ? AND status IN ('reserved', 'uploaded', 'expired', 'failed')
- LIMIT 1 FOR UPDATE`,
- [item.refId, userId]
- );
- const upload = rows[0];
- if (upload) {
- await conn.query(
- `UPDATE h5_user_spaces
- SET reserved_bytes = GREATEST(0, reserved_bytes - ?), updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [upload.reserved_bytes, Date.now(), upload.space_id, userId]
- );
- await conn.query(
- `UPDATE h5_upload_sessions SET status = 'expired' WHERE id = ? AND user_id = ?`,
- [upload.id, userId]
- );
- await conn.commit();
- if (upload.temporary_storage_key) {
- const target = absoluteStoragePath2(upload.temporary_storage_key);
- const sizeBytes = await fileSize(target);
- await fs22.rm(target, { force: true });
- freedBytes += sizeBytes;
- }
- removedCount += 1;
- } else {
- await conn.rollback();
- }
- } catch {
- await conn.rollback();
- } finally {
- conn.release();
- }
- continue;
- }
- if (item.kind === "orphan_tmp") {
- const target = absoluteStoragePath2(item.refId);
- const sizeBytes = await fileSize(target);
- await fs22.rm(target, { force: true });
- freedBytes += sizeBytes;
- removedCount += 1;
- continue;
- }
- if (item.kind === "workspace_temp") {
- 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 fs22.rm(resolved, { force: true });
- freedBytes += sizeBytes;
- removedCount += 1;
- continue;
- }
- if (item.kind === "stale_page_version") {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT pv.id, pv.page_id, pv.content_asset_id, pv.bundle_asset_id,
- p.current_version_id, p.space_id
- FROM h5_page_versions pv
- JOIN h5_page_records p ON p.id = pv.page_id
- WHERE pv.id = ? AND p.user_id = ?
- LIMIT 1 FOR UPDATE`,
- [item.refId, userId]
- );
- const version = rows[0];
- if (!version || version.id === version.current_version_id) {
- await conn.rollback();
- continue;
- }
- const [pubRows] = await conn.query(
- `SELECT id FROM h5_publish_records WHERE page_version_id = ? AND user_id = ? LIMIT 1`,
- [version.id, userId]
- );
- if (pubRows.length) {
- await conn.rollback();
- continue;
- }
- const now = Date.now();
- let freed = 0;
- const assetsToDelete = [version.content_asset_id].filter(Boolean);
- if (version.bundle_asset_id) {
- const [bundleUse] = await conn.query(
- `SELECT COUNT(*) AS cnt FROM h5_page_versions
- WHERE bundle_asset_id = ? AND id <> ?`,
- [version.bundle_asset_id, version.id]
- );
- if (Number(bundleUse[0]?.cnt ?? 0) === 0) {
- assetsToDelete.push(version.bundle_asset_id);
- }
- }
- for (const assetId of assetsToDelete) {
- const [assetRows] = await conn.query(
- `SELECT id, size_bytes FROM h5_assets
- WHERE id = ? AND user_id = ? AND status <> 'deleted'
- LIMIT 1 FOR UPDATE`,
- [assetId, userId]
- );
- const asset = assetRows[0];
- if (!asset) continue;
- await conn.query(
- `UPDATE h5_assets SET status = 'deleted', deleted_at = ?, updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [now, now, assetId, userId]
- );
- freed += Number(asset.size_bytes ?? 0);
- }
- await conn.query(`DELETE FROM h5_page_versions WHERE id = ?`, [version.id]);
- if (freed > 0) {
- await conn.query(
- `UPDATE h5_user_spaces
- SET used_bytes = GREATEST(0, used_bytes - ?), updated_at = ?
- WHERE id = ? AND user_id = ?`,
- [freed, now, version.space_id, userId]
- );
- }
- await conn.commit();
- freedBytes += freed;
- removedCount += 1;
- } catch {
- await conn.rollback();
- } finally {
- conn.release();
- }
- }
- }
- return { removedCount, freedBytes };
- };
- return { listCandidates, runCleanup };
-}
-
-// mindspace-agent-jobs.mjs
-import crypto23 from "node:crypto";
-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"]);
-var RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
- "worker_crashed",
- "worker_unavailable",
- "model_rate_limited",
- "network_temporary",
- "storage_temporary",
- "job_timed_out",
- "invalid_agent_job_output"
-]);
-function asNumber6(value) {
- return Number(value ?? 0);
-}
-function agentJobError(message, code, details) {
- return Object.assign(new Error(message), { code, details });
-}
-function dedupeStrings(values) {
- return [...new Set((Array.isArray(values) ? values : []).map((value) => String(value).trim()).filter(Boolean))];
-}
-function normalizeInstruction(value) {
- const instruction = String(value ?? "").normalize("NFKC").trim();
- if (!instruction) throw agentJobError("\u4EFB\u52A1\u8BF4\u660E\u4E0D\u80FD\u4E3A\u7A7A", "invalid_agent_job_input");
- if (instruction.length > 4e3) {
- throw agentJobError("\u4EFB\u52A1\u8BF4\u660E\u8D85\u8FC7\u957F\u5EA6\u9650\u5236", "invalid_agent_job_input");
- }
- return instruction;
-}
-function normalizeJobInput(input) {
- const jobType = String(input.jobType ?? "").trim();
- if (!JOB_TYPES.has(jobType)) {
- throw agentJobError("\u4E0D\u652F\u6301\u7684\u4EFB\u52A1\u7C7B\u578B", "invalid_agent_job_input");
- }
- const outputType = String(input.outputType ?? "").trim();
- if (!OUTPUT_TYPES.has(outputType)) {
- throw agentJobError("\u4E0D\u652F\u6301\u7684\u8F93\u51FA\u7C7B\u578B", "invalid_agent_job_input");
- }
- const allowedAssetIds = dedupeStrings(input.allowedAssetIds);
- if (allowedAssetIds.length === 0) {
- throw agentJobError("\u81F3\u5C11\u9700\u8981\u6388\u6743\u4E00\u4E2A\u8F93\u5165\u8D44\u4EA7", "invalid_agent_job_input");
- }
- return {
- jobType,
- instruction: normalizeInstruction(input.instruction),
- outputType,
- allowedAssetIds,
- idempotencyKey: String(input.idempotencyKey ?? "").trim() || null,
- outputCategoryId: input.outputCategoryId ? String(input.outputCategoryId) : null,
- locale: String(input.locale ?? "zh-CN").trim() || "zh-CN",
- timezone: String(input.timezone ?? "Asia/Shanghai").trim() || "Asia/Shanghai",
- capabilities: {
- network: Boolean(input.capabilities?.network),
- shell: Boolean(input.capabilities?.shell),
- createPage: input.capabilities?.createPage !== false
- }
- };
-}
-function hashJobToken(token) {
- return crypto23.createHash("sha256").update(String(token)).digest("hex");
-}
-function jsonValue(value, fallback) {
- if (value == null) return fallback;
- if (typeof value === "object") return value;
- try {
- return JSON.parse(String(value));
- } catch {
- return fallback;
- }
-}
-function jobResponse(row, assets = []) {
- if (!row) return null;
- return {
- id: row.id,
- sessionId: row.session_id ?? null,
- jobType: row.job_type,
- instruction: row.instruction,
- status: row.status,
- outputType: row.output_type,
- outputCategoryId: row.output_category_id,
- outputCategoryCode: row.output_category_code ?? null,
- progress: jsonValue(row.progress_json, { stage: row.status }),
- permissionScope: jsonValue(row.permission_scope, {}),
- userContext: jsonValue(row.user_context_json, {
- locale: "zh-CN",
- timezone: "Asia/Shanghai"
- }),
- resultPageId: row.result_page_id ?? null,
- resultAssetId: row.result_asset_id ?? null,
- errorCode: row.error_code ?? null,
- errorMessage: row.error_message ?? null,
- retryable: RETRYABLE_ERROR_CODES.has(row.error_code),
- 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
- };
-}
-function outputContentFormat(outputType, input) {
- if (input.contentFormat === "html" || outputType === "html_page") return "html";
- return "markdown";
-}
-function progressPayload(input, fallbackStage) {
- const stage = String(input?.stage ?? fallbackStage ?? "").trim() || fallbackStage || "running";
- const payload = { stage };
- const message = String(input?.message ?? "").trim();
- if (message) payload.message = message.slice(0, 500);
- return payload;
-}
-function hasPathAccess(asset) {
- return asset.status !== "deleted" && asset.status !== "quarantined" && asset.scan_status !== "blocked";
-}
-function verifyTokenHash(expectedHash, token) {
- if (!expectedHash || !token) return false;
- const actualHash = hashJobToken(token);
- const expected = Buffer.from(String(expectedHash), "hex");
- const actual = Buffer.from(actualHash, "hex");
- return expected.length === actual.length && crypto23.timingSafeEqual(expected, actual);
-}
-function createAgentJobService(pool2, options = {}) {
- const idFactory = options.idFactory ?? (() => crypto23.randomUUID());
- const nowFactory = options.nowFactory ?? (() => Date.now());
- const tokenTtlMs = Number(options.tokenTtlMs ?? 30 * 60 * 1e3);
- const maxOutputBytes = Number(options.maxOutputBytes ?? 2 * 1024 * 1024);
- const pageService = options.pageService;
- const storageRoot = path27.resolve(options.storageRoot ?? path27.join(process.cwd(), "data", "mindspace"));
- const absoluteStoragePath2 = (storageKey) => {
- 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;
- };
- 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 fs.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 fetchJobAssets = async (jobId) => {
- const [rows] = await pool2.query(
- `SELECT ja.asset_id, ja.asset_version_id, ja.permission, a.display_name, a.mime_type,
- a.status, v.scan_status
- FROM h5_agent_job_assets ja
- JOIN h5_assets a ON a.id = ja.asset_id
- JOIN h5_asset_versions v ON v.id = ja.asset_version_id
- WHERE ja.job_id = ?
- ORDER BY ja.created_at ASC`,
- [jobId]
- );
- return rows.map((row) => ({
- assetId: row.asset_id,
- assetVersionId: row.asset_version_id,
- permission: row.permission,
- displayName: row.display_name,
- mimeType: row.mime_type,
- status: row.status,
- scanStatus: row.scan_status
- }));
- };
- const fetchJobRow = async (jobId, userId = null) => {
- const params = [jobId];
- let sql = `SELECT j.*, c.category_code AS output_category_code
- FROM h5_agent_jobs j
- JOIN h5_space_categories c ON c.id = j.output_category_id
- WHERE j.id = ?`;
- if (userId) {
- sql += " AND j.user_id = ?";
- params.push(userId);
- }
- sql += " LIMIT 1";
- const [rows] = await pool2.query(sql, params);
- return rows[0] ?? null;
- };
- const createJob = async (userId, input) => {
- const normalized = normalizeJobInput(input);
- if (!pageService) {
- throw agentJobError("\u9875\u9762\u670D\u52A1\u672A\u521D\u59CB\u5316", "internal_error");
- }
- if (normalized.idempotencyKey) {
- const [existingRows] = await pool2.query(
- `SELECT j.*, c.category_code AS output_category_code
- FROM h5_agent_jobs j
- JOIN h5_space_categories c ON c.id = j.output_category_id
- WHERE j.user_id = ? AND j.idempotency_key = ?
- LIMIT 1`,
- [userId, normalized.idempotencyKey]
- );
- if (existingRows[0]) {
- const assets = await fetchJobAssets(existingRows[0].id);
- return jobResponse(existingRows[0], assets);
- }
- }
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const categoryParams = normalized.outputCategoryId ? [normalized.outputCategoryId, userId] : [userId];
- const [categoryRows] = await conn.query(
- normalized.outputCategoryId ? `SELECT id, category_code
- FROM h5_space_categories
- WHERE id = ? AND user_id = ?
- LIMIT 1
- FOR UPDATE` : `SELECT id, category_code
- FROM h5_space_categories
- WHERE user_id = ? AND category_code = 'draft'
- LIMIT 1
- FOR UPDATE`,
- categoryParams
- );
- const category = categoryRows[0];
- if (!category) throw agentJobError("\u8F93\u51FA\u5206\u7C7B\u4E0D\u5B58\u5728", "category_not_found");
- if (!["draft", "oa"].includes(category.category_code)) {
- throw agentJobError("\u8BE5\u5206\u7C7B\u4E0D\u5141\u8BB8\u5199\u5165 Agent \u8F93\u51FA", "invalid_agent_job_input");
- }
- const placeholders = normalized.allowedAssetIds.map(() => "?").join(", ");
- const [assetRows] = await conn.query(
- `SELECT a.id, a.current_version_id, a.display_name, a.mime_type, a.status, v.scan_status
- FROM h5_assets a
- JOIN h5_asset_versions v ON v.id = a.current_version_id
- WHERE a.user_id = ? AND a.id IN (${placeholders}) AND a.status <> 'deleted'
- FOR UPDATE`,
- [userId, ...normalized.allowedAssetIds]
- );
- const assetMap = new Map(assetRows.map((row) => [row.id, row]));
- const missingIds = normalized.allowedAssetIds.filter((assetId) => !assetMap.has(assetId));
- if (missingIds.length > 0) {
- throw agentJobError("\u5B58\u5728\u672A\u6388\u6743\u6216\u4E0D\u5B58\u5728\u7684\u8D44\u4EA7", "asset_not_found", {
- assetIds: missingIds
- });
- }
- const blockedAssets = assetRows.filter((row) => !hasPathAccess(row));
- if (blockedAssets.length > 0) {
- throw agentJobError("\u6709\u8F93\u5165\u8D44\u4EA7\u5C1A\u672A\u901A\u8FC7\u5B89\u5168\u68C0\u67E5", "security_scan_required", {
- assetIds: blockedAssets.map((row) => row.id)
- });
- }
- const jobId = idFactory();
- const now = nowFactory();
- await conn.query(
- `INSERT INTO h5_agent_jobs
- (id, user_id, job_type, instruction, permission_scope, user_context_json,
- output_category_id, output_type, status, idempotency_key, progress_json,
- queued_at, expires_at, updated_at, max_output_bytes)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?)`,
- [
- jobId,
- userId,
- normalized.jobType,
- normalized.instruction,
- JSON.stringify({ capabilities: normalized.capabilities }),
- JSON.stringify({ locale: normalized.locale, timezone: normalized.timezone }),
- category.id,
- normalized.outputType,
- normalized.idempotencyKey,
- JSON.stringify({ stage: "queued" }),
- now,
- now + tokenTtlMs,
- now,
- maxOutputBytes
- ]
- );
- for (const assetId of normalized.allowedAssetIds) {
- const asset = assetMap.get(assetId);
- await conn.query(
- `INSERT INTO h5_agent_job_assets
- (id, job_id, asset_id, asset_version_id, permission, created_at)
- VALUES (?, ?, ?, ?, 'read', ?)`,
- [idFactory(), jobId, asset.id, asset.current_version_id, now]
- );
- }
- await conn.commit();
- const created = await fetchJobRow(jobId, userId);
- const assets = await fetchJobAssets(jobId);
- return jobResponse(created, assets);
- } catch (error) {
- await conn.rollback();
- throw error;
- } finally {
- conn.release();
- }
- };
- const getJob = async (userId, jobId) => {
- const row = await fetchJobRow(jobId, userId);
- if (!row) throw agentJobError("\u4EFB\u52A1\u4E0D\u5B58\u5728", "agent_job_not_found");
- const assets = await fetchJobAssets(jobId);
- return jobResponse(row, assets);
- };
- const listJobs = async (userId, { limit = 20, offset = 0 } = {}) => {
- const safeLimit = Math.min(Math.max(Number(limit) || 20, 1), 100);
- const safeOffset = Math.max(Number(offset) || 0, 0);
- const [countRows] = await pool2.query(
- `SELECT COUNT(*) AS total FROM h5_agent_jobs WHERE user_id = ?`,
- [userId]
- );
- const total = Number(countRows[0]?.total ?? 0);
- const [rows] = await pool2.query(
- `SELECT j.*, c.category_code AS output_category_code
- FROM h5_agent_jobs j
- JOIN h5_space_categories c ON c.id = j.output_category_id
- WHERE j.user_id = ?
- ORDER BY COALESCE(j.completed_at, j.started_at, j.queued_at) DESC
- LIMIT ${safeLimit} OFFSET ${safeOffset}`,
- [userId]
- );
- const jobs = await Promise.all(
- rows.map(async (row) => jobResponse(row, await fetchJobAssets(row.id)))
- );
- return {
- items: jobs,
- total,
- offset: safeOffset,
- limit: safeLimit,
- hasMore: safeOffset + jobs.length < total
- };
- };
- const cancelJob = async (userId, jobId) => {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT * FROM h5_agent_jobs
- WHERE id = ? AND user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [jobId, userId]
- );
- const job = rows[0];
- if (!job) throw agentJobError("\u4EFB\u52A1\u4E0D\u5B58\u5728", "agent_job_not_found");
- if (!ACTIVE_JOB_STATUSES.has(job.status)) {
- throw agentJobError("\u5F53\u524D\u72B6\u6001\u4E0D\u5141\u8BB8\u53D6\u6D88\u4EFB\u52A1", "invalid_state_transition");
- }
- const now = nowFactory();
- await conn.query(
- `UPDATE h5_agent_jobs
- SET status = 'cancelled', completed_at = ?, expires_at = ?, updated_at = ?,
- job_token_hash = NULL, heartbeat_at = NULL, progress_json = ?
- WHERE id = ?`,
- [now, now, now, JSON.stringify({ stage: "cancelled" }), jobId]
- );
- await conn.commit();
- return getJob(userId, jobId);
- } catch (error) {
- await conn.rollback();
- throw error;
- } finally {
- conn.release();
- }
- };
- const retryJob = async (userId, jobId) => {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT * FROM h5_agent_jobs
- WHERE id = ? AND user_id = ?
- LIMIT 1
- FOR UPDATE`,
- [jobId, userId]
- );
- const job = rows[0];
- if (!job) throw agentJobError("\u4EFB\u52A1\u4E0D\u5B58\u5728", "agent_job_not_found");
- if (!["failed", "timed_out", "cancelled"].includes(job.status)) {
- throw agentJobError("\u5F53\u524D\u72B6\u6001\u4E0D\u5141\u8BB8\u91CD\u8BD5\u4EFB\u52A1", "invalid_state_transition");
- }
- if (job.status === "failed" && !RETRYABLE_ERROR_CODES.has(job.error_code)) {
- throw agentJobError("\u8BE5\u4EFB\u52A1\u9519\u8BEF\u4E0D\u53EF\u81EA\u52A8\u91CD\u8BD5", "invalid_state_transition");
- }
- const now = nowFactory();
- await conn.query(
- `UPDATE h5_agent_jobs
- SET status = 'queued', progress_json = ?, started_at = NULL, completed_at = NULL,
- error_code = NULL, error_message = NULL, result_page_id = NULL, result_asset_id = NULL,
- job_token_hash = NULL, heartbeat_at = NULL, expires_at = ?, updated_at = ?
- WHERE id = ?`,
- [JSON.stringify({ stage: "queued" }), now + tokenTtlMs, now, jobId]
- );
- await conn.commit();
- return getJob(userId, jobId);
- } catch (error) {
- await conn.rollback();
- throw error;
- } finally {
- conn.release();
- }
- };
- const finalizeClaim = async (conn, job) => {
- const jobId = job.id;
- const token = crypto23.randomBytes(24).toString("base64url");
- const now = nowFactory();
- await conn.query(
- `UPDATE h5_agent_jobs
- SET status = 'running', started_at = COALESCE(started_at, ?), heartbeat_at = ?,
- updated_at = ?, expires_at = ?, progress_json = ?, job_token_hash = ?
- WHERE id = ?`,
- [
- now,
- now,
- now,
- now + tokenTtlMs,
- JSON.stringify({ stage: "preparing_files" }),
- hashJobToken(token),
- jobId
- ]
- );
- await conn.commit();
- const assets = await fetchJobAssets(jobId);
- const permissionScope = jsonValue(job.permission_scope, {});
- const userContext = jsonValue(job.user_context_json, {
- locale: "zh-CN",
- timezone: "Asia/Shanghai"
- });
- return {
- jobId,
- userId: job.user_id,
- jobToken: token,
- instruction: job.instruction,
- userContext,
- allowedAssets: assets.map((asset) => ({
- assetId: asset.assetId,
- versionId: asset.assetVersionId,
- permission: asset.permission,
- displayName: asset.displayName,
- mimeType: asset.mimeType,
- downloadEndpoint: `/api/internal/agent/jobs/${jobId}/assets/${asset.assetId}`
- })),
- output: {
- categoryId: job.output_category_id,
- categoryCode: job.output_category_code,
- allowedTypes: [job.output_type],
- maxBytes: asNumber6(job.max_output_bytes)
- },
- capabilities: permissionScope.capabilities ?? {
- network: false,
- shell: false,
- createPage: true
- },
- expiresAt: now + tokenTtlMs
- };
- };
- const claimNextJob = async () => {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT j.*, c.category_code AS output_category_code
- FROM h5_agent_jobs j
- JOIN h5_space_categories c ON c.id = j.output_category_id
- WHERE j.status = 'queued'
- AND (j.expires_at IS NULL OR j.expires_at >= ?)
- ORDER BY j.queued_at ASC
- LIMIT 1
- FOR UPDATE SKIP LOCKED`,
- [nowFactory()]
- );
- const job = rows[0];
- if (!job) {
- await conn.commit();
- return null;
- }
- return await finalizeClaim(conn, job);
- } catch (error) {
- await conn.rollback();
- throw error;
- } finally {
- conn.release();
- }
- };
- const claimJob = async (jobId) => {
- const conn = await pool2.getConnection();
- try {
- await conn.beginTransaction();
- const [rows] = await conn.query(
- `SELECT j.*, c.category_code AS output_category_code
- FROM h5_agent_jobs j
- JOIN h5_space_categories c ON c.id = j.output_category_id
- WHERE j.id = ?
- LIMIT 1
- FOR UPDATE`,
- [jobId]
- );
- const job = rows[0];
- if (!job) throw agentJobError("\u4EFB\u52A1\u4E0D\u5B58\u5728", "agent_job_not_found");
- if (job.status !== "queued") {
- throw agentJobError("\u4EFB\u52A1\u5F53\u524D\u4E0D\u53EF\u9886\u53D6", "invalid_state_transition");
- }
- 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);
- } catch (error) {
- await conn.rollback();
- throw error;
- } finally {
- conn.release();
- }
- };
- const requireJobToken = async (jobId, token) => {
- const [rows] = await pool2.query(
- `SELECT j.*, c.category_code AS output_category_code
- FROM h5_agent_jobs j
- JOIN h5_space_categories c ON c.id = j.output_category_id
- WHERE j.id = ?
- LIMIT 1`,
- [jobId]
- );
- const job = rows[0];
- if (!job) throw agentJobError("\u4EFB\u52A1\u4E0D\u5B58\u5728", "agent_job_not_found");
- if (job.status !== "running") {
- throw agentJobError("\u4EFB\u52A1\u672A\u5904\u4E8E\u6267\u884C\u4E2D\u72B6\u6001", "invalid_state_transition");
- }
- 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)) {
- throw agentJobError("\u4EFB\u52A1 token \u65E0\u6548", "agent_job_token_invalid");
- }
- return job;
- };
- const getAssetForJob = async (jobId, token, assetId) => {
- await requireJobToken(jobId, token);
- const [rows] = await pool2.query(
- `SELECT a.id, a.display_name, a.mime_type, a.status, v.id AS asset_version_id,
- v.storage_key, v.scan_status
- FROM h5_agent_job_assets ja
- JOIN h5_assets a ON a.id = ja.asset_id
- JOIN h5_asset_versions v ON v.id = ja.asset_version_id
- WHERE ja.job_id = ? AND ja.asset_id = ?
- LIMIT 1`,
- [jobId, assetId]
- );
- const asset = rows[0];
- if (!asset) throw agentJobError("\u4EFB\u52A1\u8D44\u4EA7\u4E0D\u5B58\u5728", "asset_not_found");
- if (!hasPathAccess(asset)) {
- throw agentJobError("\u4EFB\u52A1\u8D44\u4EA7\u5C1A\u672A\u901A\u8FC7\u5B89\u5168\u68C0\u67E5", "security_scan_required");
- }
- return {
- assetId: asset.id,
- assetVersionId: asset.asset_version_id,
- displayName: asset.display_name,
- mimeType: asset.mime_type,
- path: await resolveReadableStoragePath(asset.storage_key)
- };
- };
- const heartbeat = async (jobId, token, input) => {
- const job = await requireJobToken(jobId, token);
- const now = nowFactory();
- const payload = progressPayload(input, "running");
- await pool2.query(
- `UPDATE h5_agent_jobs
- SET heartbeat_at = ?, updated_at = ?, expires_at = ?, progress_json = ?
- WHERE id = ?`,
- [now, now, now + tokenTtlMs, JSON.stringify(payload), jobId]
- );
- return jobResponse({ ...job, progress_json: JSON.stringify(payload), heartbeat_at: now });
- };
- const completeJob = async (jobId, token, input) => {
- const job = await requireJobToken(jobId, token);
- const now = nowFactory();
- if (input?.status === "failed") {
- const errorCode = String(input.errorCode ?? "worker_crashed").trim() || "worker_crashed";
- const errorMessage = String(input.errorMessage ?? "\u4EFB\u52A1\u6267\u884C\u5931\u8D25").trim() || "\u4EFB\u52A1\u6267\u884C\u5931\u8D25";
- await pool2.query(
- `UPDATE h5_agent_jobs
- SET status = 'failed', error_code = ?, error_message = ?, completed_at = ?,
- updated_at = ?, heartbeat_at = NULL, job_token_hash = NULL, progress_json = ?
- WHERE id = ?`,
- [errorCode, errorMessage.slice(0, 1e3), now, now, JSON.stringify({ stage: "failed" }), jobId]
- );
- return getJob(job.user_id, jobId);
- }
- const content = String(input?.content ?? "");
- const outputBytes = Buffer.byteLength(content, "utf8");
- if (!content.trim()) {
- throw agentJobError("\u4EFB\u52A1\u8F93\u51FA\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A", "invalid_agent_job_output");
- }
- if (outputBytes > asNumber6(job.max_output_bytes)) {
- throw agentJobError("\u4EFB\u52A1\u8F93\u51FA\u8D85\u8FC7\u5927\u5C0F\u9650\u5236", "page_content_too_large", {
- maxBytes: asNumber6(job.max_output_bytes)
- });
- }
- const sourceAssetIds = dedupeStrings(input?.sourceAssetIds);
- const boundAssets = await fetchJobAssets(jobId);
- const boundAssetIdSet = new Set(boundAssets.map((asset) => asset.assetId));
- const invalidSourceIds = sourceAssetIds.filter((assetId) => !boundAssetIdSet.has(assetId));
- if (invalidSourceIds.length > 0) {
- throw agentJobError("\u8F93\u51FA\u5F15\u7528\u4E86\u672A\u6388\u6743\u7684\u8F93\u5165\u8D44\u4EA7", "invalid_agent_job_output", {
- assetIds: invalidSourceIds
- });
- }
- const page = await pageService.createFromAgent(
- job.user_id,
- {
- title: input?.title,
- summary: input?.summary,
- content,
- contentFormat: outputContentFormat(job.output_type, input),
- pageType: input?.pageType,
- templateId: input?.templateId,
- categoryCode: job.output_category_code,
- changeNote: `Agent job ${jobId}`
- },
- {
- jobId,
- assetId: sourceAssetIds[0] ?? null,
- assetIds: sourceAssetIds
- }
- );
- await pool2.query(
- `UPDATE h5_agent_jobs
- SET status = 'completed', result_page_id = ?, completed_at = ?, updated_at = ?,
- heartbeat_at = NULL, job_token_hash = NULL, progress_json = ?
- WHERE id = ?`,
- [page.id, now, now, JSON.stringify({ stage: "completed" }), jobId]
- );
- return getJob(job.user_id, jobId);
- };
- const reapStaleJobs = async (maxStaleMs = 5 * 60 * 1e3) => {
- const now = nowFactory();
- const cutoff = now - Math.max(1, maxStaleMs);
- const [result] = await pool2.query(
- `UPDATE h5_agent_jobs
- SET status = 'failed', error_code = 'worker_crashed',
- error_message = 'worker heartbeat timed out',
- completed_at = ?, updated_at = ?, heartbeat_at = NULL, job_token_hash = NULL,
- progress_json = ?
- WHERE status = 'running'
- AND heartbeat_at IS NOT NULL
- AND heartbeat_at < ?`,
- [now, now, JSON.stringify({ stage: "failed" }), cutoff]
- );
- return result?.affectedRows ?? 0;
- };
- return {
- createJob,
- getJob,
- listJobs,
- cancelJob,
- retryJob,
- claimJob,
- claimNextJob,
- reapStaleJobs,
- getAssetForJob,
- heartbeat,
- completeJob
- };
-}
-
-// mindspace-agent-runner.mjs
-import crypto24 from "node:crypto";
-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";
-
-// message-stream.mjs
-function mergeStreamingChunk(previous, incoming) {
- const prev = String(previous ?? "");
- const next = String(incoming ?? "");
- if (!next) return prev;
- if (!prev) return next;
- if (next === prev) return prev;
- if (next.startsWith(prev)) return next;
- if (prev.startsWith(next)) return prev;
- if (prev.endsWith(next)) return prev;
- const maxOverlap = Math.min(prev.length, next.length);
- for (let size = maxOverlap; size > 0; size -= 1) {
- if (prev.endsWith(next.slice(0, size))) {
- return prev + next.slice(size);
- }
- }
- return prev + next;
-}
-function findLastContentIndex(content, type) {
- for (let index = content.length - 1; index >= 0; index -= 1) {
- if (content[index]?.type === type) return index;
- }
- return -1;
-}
-function mergeTextLikeBlock(block, incomingValue, field) {
- block[field] = mergeStreamingChunk(block[field], incomingValue);
-}
-function mergeMessageContent(existingContent, incomingContent) {
- const merged = [...existingContent];
- for (const item of incomingContent) {
- if (item.type === "text") {
- const index = findLastContentIndex(merged, "text");
- if (index >= 0) {
- mergeTextLikeBlock(merged[index], item.text, "text");
- continue;
- }
- merged.push({ ...item });
- continue;
- }
- if (item.type === "thinking") {
- const index = findLastContentIndex(merged, "thinking");
- if (index >= 0) {
- mergeTextLikeBlock(merged[index], item.thinking, "thinking");
- continue;
- }
- merged.push({ ...item });
- continue;
- }
- const tail = merged[merged.length - 1];
- if (tail && JSON.stringify(tail) === JSON.stringify(item)) continue;
- merged.push({ ...item });
- }
- return merged;
-}
-
-// mindspace-agent-runner.mjs
-var insecureDispatcher5 = new Agent5({
- connect: { rejectUnauthorized: false }
-});
-var DEFAULT_TEXT_BYTES = 48 * 1024;
-function isHttpsTarget3(target) {
- return String(target).startsWith("https://");
-}
-function runnerError(message, code, details) {
- return Object.assign(new Error(message), { code, details });
-}
-function createUserMessage(text) {
- return {
- id: crypto24.randomUUID(),
- role: "user",
- created: Math.floor(Date.now() / 1e3),
- content: [{ type: "text", text }],
- metadata: { userVisible: true, agentVisible: true }
- };
-}
-function messageVisibleText(message) {
- return (message?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("");
-}
-function pushMessage(messages, incoming) {
- const last = messages[messages.length - 1];
- if (last?.id && incoming?.id && last.id === incoming.id) {
- return [
- ...messages.slice(0, -1),
- {
- ...last,
- content: mergeMessageContent(last.content, incoming.content)
- }
- ];
- }
- return [...messages, incoming];
-}
-function extractBalancedJsonObject(text, startIndex = 0) {
- let depth = 0;
- let inString = false;
- let escape = false;
- for (let i = startIndex; i < text.length; i += 1) {
- const ch = text[i];
- if (inString) {
- if (escape) {
- escape = false;
- continue;
- }
- if (ch === "\\") {
- escape = true;
- continue;
- }
- if (ch === '"') inString = false;
- continue;
- }
- if (ch === '"') {
- inString = true;
- continue;
- }
- if (ch === "{") depth += 1;
- else if (ch === "}") {
- depth -= 1;
- if (depth === 0) return text.slice(startIndex, i + 1);
- }
- }
- return null;
-}
-function collectJsonCandidates(text) {
- const source = String(text ?? "").trim();
- const candidates = [];
- const seen = /* @__PURE__ */ new Set();
- const push = (value) => {
- const trimmed = String(value ?? "").trim();
- if (!trimmed || seen.has(trimmed)) return;
- seen.add(trimmed);
- candidates.push(trimmed);
- };
- for (const match of source.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
- push(match[1]);
- }
- push(source);
- for (const candidate of [...candidates]) {
- for (let i = 0; i < candidate.length; i += 1) {
- if (candidate[i] !== "{") continue;
- const balanced = extractBalancedJsonObject(candidate, i);
- if (balanced) push(balanced);
- }
- }
- return candidates;
-}
-function parseJsonObject2(text) {
- for (const candidate of collectJsonCandidates(text)) {
- for (const normalized of [candidate, jsonrepair2(candidate)]) {
- try {
- const parsed = JSON.parse(normalized);
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
- return parsed;
- }
- } catch {
- }
- }
- }
- return null;
-}
-function extractJsonObject2(text) {
- const source = String(text ?? "").trim();
- const parsed = parseJsonObject2(source);
- if (parsed) return parsed;
- const start = source.indexOf("{");
- const end = source.lastIndexOf("}");
- if (start < 0 || end <= start) {
- throw runnerError("Agent \u8F93\u51FA\u7F3A\u5C11 JSON \u7ED3\u679C", "invalid_agent_job_output");
- }
- const raw = extractBalancedJsonObject(source, start) ?? source.slice(start, end + 1);
- try {
- return JSON.parse(jsonrepair2(raw));
- } catch (error) {
- throw runnerError("Agent \u8F93\u51FA JSON \u89E3\u6790\u5931\u8D25", "invalid_agent_job_output", {
- raw: raw.slice(0, 2e3),
- cause: error instanceof Error ? error.message : String(error)
- });
- }
-}
-function normalizeStructuredResult(payload) {
- const title = String(payload?.title ?? "").trim();
- const summary = String(payload?.summary ?? "").trim();
- const content = String(payload?.content ?? payload?.markdown ?? payload?.body ?? "").trim();
- const contentFormat = String(payload?.content_format ?? payload?.contentFormat ?? "markdown").trim().toLowerCase();
- if (!title || !content) {
- throw runnerError("Agent \u8F93\u51FA\u7F3A\u5C11\u6807\u9898\u6216\u6B63\u6587", "invalid_agent_job_output");
- }
- return {
- title,
- summary,
- content,
- contentFormat: contentFormat === "html" ? "html" : "markdown",
- pageType: contentFormat === "html" ? "html" : "article",
- templateId: contentFormat === "html" ? "static-html" : "report"
- };
-}
-async function readAssetContext(asset, maxBytes = DEFAULT_TEXT_BYTES) {
- const mimeType = String(asset.mimeType ?? "");
- const textLike = mimeType.startsWith("text/") || mimeType === "application/json" || mimeType.includes("xml") || mimeType.includes("javascript");
- if (!textLike) {
- return {
- assetId: asset.assetId,
- displayName: asset.displayName,
- mimeType,
- excerpt: "",
- 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 fs23.readFile(asset.path);
- return {
- assetId: asset.assetId,
- displayName: asset.displayName,
- mimeType,
- excerpt: buffer.subarray(0, maxBytes).toString("utf8"),
- truncated: buffer.length > maxBytes,
- note: buffer.length > maxBytes ? `\u5185\u5BB9\u5DF2\u622A\u65AD\u5230 ${maxBytes} \u5B57\u8282\u3002` : void 0
- };
-}
-function buildAgentJobPrompt(job, assetContexts) {
- const assetSections = assetContexts.map((asset, index) => {
- const header = `\u8D44\u6599 ${index + 1}: ${asset.displayName} (${asset.mimeType || "unknown"})`;
- const note = asset.note ? `\u8BF4\u660E: ${asset.note}
-` : "";
- const body = asset.excerpt ? `\u5185\u5BB9:
-<<>>
-${asset.excerpt}
-<<>>` : "\u5185\u5BB9: [\u672A\u5185\u5D4C\u6587\u672C\u5185\u5BB9]";
- return `${header}
-${note}${body}`;
- }).join("\n\n");
- return [
- "\u4F60\u6B63\u5728\u4E3A MindSpace \u751F\u6210\u4E00\u4E2A\u9875\u9762\u8349\u7A3F\u3002",
- "\u4F60\u53EA\u80FD\u57FA\u4E8E\u7ED9\u5B9A\u8D44\u6599\u548C\u7528\u6237\u4EFB\u52A1\u751F\u6210\u7ED3\u679C\uFF0C\u4E0D\u80FD\u5047\u8BBE\u989D\u5916\u4E8B\u5B9E\u3002",
- "\u4E0D\u8981\u8BF7\u6C42\u5DE5\u5177\u786E\u8BA4\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\uFF0C\u4E0D\u8981\u8C03\u7528\u5DE5\u5177\u3002",
- "\u6700\u7EC8\u56DE\u590D\u5FC5\u987B\u662F\u4E00\u4E2A\u5408\u6CD5 JSON \u5BF9\u8C61\uFF0C\u4E14\u81F3\u5C11\u5305\u542B title\u3001summary\u3001content \u4E09\u4E2A\u5B57\u6BB5\u3002",
- 'content \u53EF\u4EE5\u662F Markdown \u6216 HTML\uFF1B\u5982\u9700 HTML\uFF0C\u8BF7\u628A content_format \u8BBE\u4E3A "html"\u3002',
- "\u793A\u4F8B\uFF1A",
- '{"title":"\u9875\u9762\u6807\u9898","summary":"\u4E00\u53E5\u8BDD\u6458\u8981","content":"# \u6807\u9898\\n\\n\u6B63\u6587\u6BB5\u843D","content_format":"markdown"}',
- `\u7528\u6237\u4EFB\u52A1: ${job.instruction}`,
- "",
- assetSections
- ].join("\n");
-}
-async function readJsonResponse(response) {
- const text = await response.text();
- if (!response.ok) {
- throw new Error(text || `upstream ${response.status}`);
- }
- return text ? JSON.parse(text) : null;
-}
-async function defaultExecuteSessionReply(apiFetch2, sessionId, requestId, prompt) {
- const eventsResponse = await apiFetch2(`/sessions/${sessionId}/events`, {
- method: "GET",
- headers: { Accept: "text/event-stream" }
- });
- if (!eventsResponse.ok || !eventsResponse.body) {
- const text = await eventsResponse.text().catch(() => "");
- throw runnerError(text || "\u65E0\u6CD5\u5EFA\u7ACB\u4EFB\u52A1\u4E8B\u4EF6\u6D41", "worker_unavailable");
- }
- const replyResponse = await apiFetch2(`/sessions/${sessionId}/reply`, {
- method: "POST",
- body: JSON.stringify({
- request_id: requestId,
- user_message: createUserMessage(prompt)
- })
- });
- if (!replyResponse.ok) {
- const text = await replyResponse.text().catch(() => "");
- throw runnerError(text || "Agent reply \u8BF7\u6C42\u5931\u8D25", "worker_unavailable");
- }
- replyResponse.body?.cancel().catch?.(() => {
- });
- const reader = Readable2.fromWeb(eventsResponse.body);
- const decoder = new TextDecoder();
- let buffer = "";
- let messages = [];
- for await (const chunk of reader) {
- buffer += decoder.decode(chunk, { stream: true });
- const frames = buffer.split("\n\n");
- buffer = frames.pop() ?? "";
- for (const frame of frames) {
- let data = "";
- for (const line of frame.split("\n")) {
- if (line.startsWith("data:")) data += `${line.slice(5).trim()}`;
- }
- if (!data) continue;
- let event;
- try {
- event = JSON.parse(data);
- } catch {
- continue;
- }
- const routingId = event.chat_request_id ?? event.request_id;
- if (routingId && routingId !== requestId) continue;
- if (event.type === "Message" && event.message?.metadata?.userVisible) {
- const hasActionRequired = event.message.content?.some((item) => item.type === "actionRequired");
- if (hasActionRequired) {
- throw runnerError("\u4EFB\u52A1\u6267\u884C\u9700\u8981\u4EBA\u5DE5\u786E\u8BA4\uFF0CRunner \u5F53\u524D\u65E0\u6CD5\u81EA\u52A8\u5904\u7406", "worker_unavailable");
- }
- messages = pushMessage(messages, event.message);
- } else if (event.type === "UpdateConversation") {
- messages = (event.conversation ?? []).filter((item) => item.metadata?.userVisible);
- } else if (event.type === "Error") {
- throw runnerError(event.error || "\u4EFB\u52A1\u6267\u884C\u5931\u8D25", "worker_unavailable");
- } else if (event.type === "Finish") {
- const assistant = [...messages].reverse().find((item) => item.role === "assistant");
- return {
- text: messageVisibleText(assistant),
- tokenState: event.token_state ?? null
- };
- }
- }
- }
- throw runnerError("\u4EFB\u52A1\u4E8B\u4EF6\u6D41\u63D0\u524D\u7ED3\u675F", "worker_unavailable");
-}
-function createMindSpaceAgentRunner({
- apiTarget,
- apiSecret,
- userAuth: userAuth2,
- agentJobService,
- experienceService = null,
- executeSessionReply: executeSessionReply2 = defaultExecuteSessionReply,
- apiFetchImpl = null
-}) {
- const apiFetch2 = apiFetchImpl ?? (async (pathname, init = {}) => {
- 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";
- }
- return undiciFetch5(url, {
- ...init,
- headers,
- dispatcher: isHttpsTarget3(apiTarget) ? insecureDispatcher5 : void 0
- });
- });
- const runJob = async (jobId, preClaimed = null) => {
- let claim = null;
- let sessionId = null;
- try {
- claim = preClaimed ?? await agentJobService.claimJob(jobId);
- const gate = await userAuth2.canUseChat(claim.userId);
- if (!gate.ok) {
- throw runnerError(gate.message || "\u5F53\u524D\u7528\u6237\u65E0\u6CD5\u6267\u884C Agent \u4EFB\u52A1", "worker_unavailable");
- }
- const workingDir = await userAuth2.resolveWorkingDir(claim.userId);
- const sessionPolicy = await userAuth2.getAgentSessionPolicy(claim.userId);
- const publishLayout = await userAuth2.getUserPublishLayout(claim.userId);
- const startSession = await readJsonResponse(
- await apiFetch2("/agent/start", {
- method: "POST",
- body: JSON.stringify({
- working_dir: workingDir,
- enable_context_memory: sessionPolicy.enableContextMemory,
- ...sessionPolicy.extensionOverrides ? { extension_overrides: sessionPolicy.extensionOverrides } : {}
- })
- })
- );
- sessionId = startSession?.id;
- if (!sessionId) {
- throw runnerError("Agent \u4F1A\u8BDD\u542F\u52A8\u5931\u8D25", "worker_unavailable");
- }
- await userAuth2.registerAgentSession(claim.userId, sessionId);
- await reconcileAgentSession(
- (pathname, init) => apiFetch2(pathname, init),
- sessionId,
- {
- workingDir,
- sessionPolicy,
- sandboxConstraints: publishLayout?.constraints ?? null,
- userContext: publishLayout ? {
- userId: claim.userId,
- displayName: publishLayout.displayName,
- username: publishLayout.username,
- slug: publishLayout.slug
- } : null
- }
- );
- const assetContexts = [];
- for (const asset of claim.allowedAssets) {
- const localAsset = await agentJobService.getAssetForJob(jobId, claim.jobToken, asset.assetId);
- assetContexts.push(await readAssetContext(localAsset));
- }
- let prompt = buildAgentJobPrompt(claim, assetContexts);
- const relevant = await retrieveExperience(claim.instruction);
- if (relevant) prompt = `${relevant}
-
-${prompt}`;
- const requestId = crypto24.randomUUID();
- const reply = await executeSessionReply2(apiFetch2, sessionId, requestId, prompt);
- const parsed = normalizeStructuredResult(extractJsonObject2(reply.text));
- if (reply.tokenState) {
- await userAuth2.billSessionUsage(claim.userId, sessionId, reply.tokenState, requestId);
- }
- const completed = await agentJobService.completeJob(jobId, claim.jobToken, {
- title: parsed.title,
- summary: parsed.summary,
- content: parsed.content,
- contentFormat: parsed.contentFormat,
- pageType: parsed.pageType,
- templateId: parsed.templateId,
- sourceAssetIds: claim.allowedAssets.map((asset) => asset.assetId)
- });
- await recordExperience(claim, sessionId, parsed);
- return completed;
- } catch (error) {
- if (claim?.jobToken) {
- await agentJobService.completeJob(jobId, claim.jobToken, {
- status: "failed",
- errorCode: error?.code ?? "worker_unavailable",
- errorMessage: error instanceof Error ? error.message : String(error)
- }).catch(() => {
- });
- }
- throw error;
- }
- };
- async function retrieveExperience(instruction) {
- if (!experienceService || !instruction) return "";
- try {
- const hits = await experienceService.search(instruction, { limit: 3 });
- if (!hits.length) return "";
- const lines = hits.map((h) => `- ${h.title}: ${h.body}`).join("\n");
- return `# \u76F8\u5173\u7ECF\u9A8C\uFF08\u4F9B\u53C2\u8003\uFF0C\u6765\u81EA\u5386\u53F2\u4EFB\u52A1\uFF09
-${lines}`;
- } catch {
- return "";
- }
- }
- async function recordExperience(claim, sessionId, parsed) {
- if (!experienceService) return;
- try {
- const title = String(parsed?.title ?? claim.instruction ?? "").slice(0, 200);
- const summary = String(parsed?.summary ?? "").trim();
- if (!title || !summary) return;
- await experienceService.record({
- kind: "task_outcome",
- title,
- body: summary,
- sourceSessionId: sessionId,
- sourceUserId: claim.userId
- });
- } catch {
- }
- }
- return { runJob };
-}
-
-// mindspace-chat-save.mjs
-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 = 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";
- if (mimeType === "image/webp") return "webp";
- if (mimeType === "image/gif") return "gif";
- return "bin";
-}
-function publicTempImageFilename2(assetId, mimeType, fallbackFilename = "") {
- return `${assetId}.${extensionForMime3(mimeType, fallbackFilename)}`;
-}
-function absoluteStoragePath(storageRoot, storageKey) {
- 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 = 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({
- pool: pool2,
- storageRoot,
- h5Root,
- userId,
- html,
- htmlRelativePath,
- writeBack = false
-}) {
- const matches = [...String(html).matchAll(PRIVATE_ASSET_URL_PATTERN3)];
- if (matches.length === 0) {
- return { html, count: 0, changed: false };
- }
- const assetIds = [...new Set(matches.map((match) => match[1]).filter(Boolean))];
- const [assets] = await pool2.query(
- `SELECT a.id, a.mime_type, a.original_filename, 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 publishDir = resolvePublishDir(h5Root, { id: userId });
- 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 = path28.join(tmpDir, filename);
- try {
- const sourcePath = absoluteStoragePath(storageRoot, asset.storage_key);
- await fs24.mkdir(tmpDir, { recursive: true });
- await fs24.copyFile(sourcePath, target);
- urlMap.set(asset.id, workspaceTempImageRelativeUrl(htmlRelativePath, filename));
- } catch {
- }
- }
- if (urlMap.size === 0) {
- return { html, count: 0, changed: false };
- }
- let count = 0;
- const result = String(html).replace(PRIVATE_ASSET_URL_PATTERN3, (value, assetId) => {
- const nextUrl = urlMap.get(assetId);
- if (!nextUrl) return value;
- count += 1;
- return nextUrl;
- });
- if (count === 0 || result === html) {
- return { html, count: 0, changed: false };
- }
- if (writeBack) {
- const htmlPath = path28.join(publishDir, htmlRelativePath);
- await fs24.writeFile(htmlPath, result, "utf8");
- }
- return { html: result, count, changed: true };
-}
-async function repairMissingHtmlAssetReferences({ pool: pool2, userId, sourceContent, html }) {
- const collectIds = (text) => [
- ...new Set([...String(text).matchAll(PRIVATE_ASSET_URL_PATTERN3)].map((match) => match[1]))
- ];
- const htmlIds = collectIds(html);
- const messageIds = collectIds(sourceContent);
- if (htmlIds.length === 0 || messageIds.length === 0) {
- return { html, changed: false };
- }
- const [rows] = await pool2.query(
- `SELECT id FROM h5_assets
- WHERE user_id = ? AND id IN (?) AND mime_type LIKE 'image/%' AND status <> 'deleted'`,
- [userId, [.../* @__PURE__ */ new Set([...htmlIds, ...messageIds])]]
- );
- const existing = new Set(rows.map((row) => row.id));
- const invalidHtmlIds = htmlIds.filter((id) => !existing.has(id));
- const validMessageIds = messageIds.filter((id) => existing.has(id));
- if (invalidHtmlIds.length === 0 || validMessageIds.length === 0) {
- return { html, changed: false };
- }
- const idMap = /* @__PURE__ */ new Map();
- for (let index = 0; index < invalidHtmlIds.length; index += 1) {
- const replacement = validMessageIds[index];
- if (!replacement) break;
- idMap.set(invalidHtmlIds[index], replacement);
- }
- if (idMap.size === 0) {
- return { html, changed: false };
- }
- let changed = false;
- const nextHtml = String(html).replace(PRIVATE_ASSET_URL_PATTERN3, (value, assetId) => {
- const replacement = idMap.get(assetId);
- if (!replacement) return value;
- changed = true;
- return value.replace(assetId, replacement);
- });
- return { html: nextHtml, changed };
-}
-function createAssetDataUriResolver(pool2, storageRoot, userId) {
- const cache = /* @__PURE__ */ new Map();
- return async (assetId) => {
- const key = String(assetId ?? "").trim();
- if (!key) return null;
- if (cache.has(key)) return cache.get(key);
- const [rows] = await pool2.query(
- `SELECT 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 = ? AND a.mime_type LIKE 'image/%'
- AND a.status <> 'deleted'
- LIMIT 1`,
- [userId, key]
- );
- const asset = rows[0];
- if (!asset) {
- cache.set(key, null);
- return null;
- }
- try {
- 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);
- return dataUri;
- } catch {
- cache.set(key, null);
- return null;
- }
- };
-}
-var URL_PATTERN = /https?:\/\/[^\s<>"')\]]+\/(?:MindSpace|temp)\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi;
-function decodePathSegment2(segment) {
- try {
- return decodeURIComponent(segment);
- } catch {
- return segment;
- }
-}
-function encodeUrlPath2(relativePath) {
- return String(relativePath ?? "").split("/").filter(Boolean).map((part) => encodeURIComponent(part)).join("/");
-}
-function normalizeStaticHtmlRelativePath2(relativePath) {
- const parts = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== "..");
- if (parts.length === 0) return "";
- if (parts[0].toLowerCase() === "public") return ["public", ...parts.slice(1)].join("/");
- if (parts.length === 1 && parts[0].toLowerCase().endsWith(".html")) return `public/${parts[0]}`;
- return parts.join("/");
-}
-function canonicalizeStaticPageUrl2(publicUrl2, originalRelativePath, canonicalRelativePath) {
- if (!canonicalRelativePath || canonicalRelativePath === String(originalRelativePath ?? "").replace(/^\/+/, "")) {
- return publicUrl2;
- }
- const suffix = encodeUrlPath2(originalRelativePath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- return String(publicUrl2).replace(new RegExp(`${suffix}$`), encodeUrlPath2(canonicalRelativePath));
-}
-function extractStaticPageLinks(content, { userId, username } = {}) {
- const text = String(content ?? "");
- const links = [];
- const seen = /* @__PURE__ */ new Set();
- const normalizedUserId = userId ? String(userId).trim().toLowerCase() : null;
- const normalizedUsername = username ? String(username).trim().toLowerCase() : null;
- for (const match of text.matchAll(URL_PATTERN)) {
- const owner = decodePathSegment2(match[1]).toLowerCase();
- const originalRelativePath = decodePathSegment2(match[2]);
- const relativePath = normalizeStaticHtmlRelativePath2(originalRelativePath);
- if (normalizedUserId) {
- if (owner !== normalizedUserId && owner !== normalizedUsername) continue;
- } else if (normalizedUsername && owner !== normalizedUsername) {
- continue;
- }
- const key = `${owner}/${relativePath}`;
- if (seen.has(key)) continue;
- seen.add(key);
- links.push({
- publicUrl: canonicalizeStaticPageUrl2(match[0], originalRelativePath, relativePath),
- owner,
- relativePath,
- filename: path28.basename(relativePath)
- });
- }
- return links;
-}
-function buildWorkspaceAssetUrl(userId, relativePath) {
- const key = String(userId ?? "").trim();
- const clean = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== "..").map((part) => encodeURIComponent(part)).join("/");
- if (!key || !clean) return null;
- return `/${PUBLISH_ROOT_DIR}/${encodeURIComponent(key)}/${clean}`;
-}
-function buildWorkspaceThumbnailUrl(userId, htmlRelativePath) {
- const thumbRel = workspaceThumbnailRelativePath(htmlRelativePath);
- return buildWorkspaceAssetUrl(userId, thumbRel);
-}
-function resolvePublishHtmlAbsolutePath(h5Root, userId, relativePath) {
- const key = String(userId ?? "").trim().toLowerCase();
- if (!PUBLISH_KEY_UUID.test(key)) {
- throw Object.assign(new Error("\u65E0\u6548\u7684\u7528\u6237 ID"), { code: "invalid_page_path" });
- }
- const clean = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== "..").join("/");
- 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 = 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 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: 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 fs24.readdir(publishRoot, { withFileTypes: true });
- } catch {
- return null;
- }
- for (const entry of entries) {
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
- 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(
- path28.join(publishRoot, entry.name),
- basename,
- maxDepth,
- depth + 1
- );
- if (found) return found;
- }
- return null;
-}
-async function collectPublishHtmlPaths(publishRoot, results, maxDepth = 6, depth = 0) {
- if (depth > maxDepth || results.length >= 200) return;
- let entries;
- try {
- 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 = path28.join(publishRoot, entry.name);
- if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
- results.push(full);
- if (results.length >= 200) return;
- }
- }
- for (const entry of entries) {
- if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
- await collectPublishHtmlPaths(path28.join(publishRoot, entry.name), results, maxDepth, depth + 1);
- if (results.length >= 200) return;
- }
-}
-function levenshteinDistance(left, right) {
- if (left === right) return 0;
- if (!left) return right.length;
- if (!right) return left.length;
- const prev = Array.from({ length: right.length + 1 }, (_, index) => index);
- const next = new Array(right.length + 1);
- for (let i = 0; i < left.length; i += 1) {
- next[0] = i + 1;
- for (let j = 0; j < right.length; j += 1) {
- const cost = left[i] === right[j] ? 0 : 1;
- next[j + 1] = Math.min(
- next[j] + 1,
- prev[j + 1] + 1,
- prev[j] + cost
- );
- }
- for (let j = 0; j <= right.length; j += 1) {
- prev[j] = next[j];
- }
- }
- return prev[right.length];
-}
-function htmlNameForSimilarity(relativePath) {
- return path28.basename(String(relativePath ?? ""), ".html").toLowerCase();
-}
-function isAcceptableSimilarHtmlMatch(requested, candidate, score, nextScore = Infinity) {
- if (!requested || !candidate || requested === candidate) return false;
- const longest = Math.max(requested.length, candidate.length);
- const allowedDistance = longest >= 18 ? 2 : 1;
- if (score > allowedDistance) return false;
- return nextScore > score;
-}
-async function resolveClosestHtmlRelativePath(rootDir, relativePath) {
- const normalized = normalizeStaticHtmlRelativePath2(relativePath);
- if (!normalized.toLowerCase().endsWith(".html")) return null;
- const requestedName = htmlNameForSimilarity(normalized);
- const candidates = [];
- await collectPublishHtmlPaths(rootDir, candidates);
- const ranked = candidates.map((absolute) => {
- const candidateRelative = path28.relative(rootDir, absolute).split(path28.sep).join("/");
- const candidateName = htmlNameForSimilarity(candidateRelative);
- return {
- relativePath: candidateRelative,
- score: levenshteinDistance(requestedName, candidateName)
- };
- }).sort((left, right) => left.score - right.score || left.relativePath.localeCompare(right.relativePath));
- if (ranked.length === 0) return null;
- const best = ranked[0];
- const nextBestScore = ranked[1]?.score ?? Infinity;
- if (!isAcceptableSimilarHtmlMatch(requestedName, htmlNameForSimilarity(best.relativePath), best.score, nextBestScore)) {
- return null;
- }
- return best.relativePath;
-}
-async function findPublishHtml(h5Root, userId, relativePath) {
- const normalized = normalizeStaticHtmlRelativePath2(relativePath);
- const basename = path28.basename(normalized);
- const candidates = [
- normalized,
- path28.posix.join("public", basename),
- basename
- ].filter((value, index, list) => value && list.indexOf(value) === index);
- for (const candidate of candidates) {
- try {
- return await readPublishHtml(h5Root, userId, candidate);
- } catch {
- }
- }
- const publishRoot = path28.resolve(
- h5Root,
- PUBLISH_ROOT_DIR,
- String(userId ?? "").trim().toLowerCase()
- );
- const absolute = await walkPublishHtmlByBasename(publishRoot, basename);
- if (absolute) {
- const resolvedRelativePath = path28.relative(publishRoot, absolute).split(path28.sep).join("/");
- return readPublishHtml(h5Root, userId, resolvedRelativePath);
- }
- const similarRelativePath = await resolveClosestHtmlRelativePath(publishRoot, normalized);
- if (similarRelativePath) {
- return readPublishHtml(h5Root, userId, similarRelativePath);
- }
- throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u94FE\u63A5\u9875\u9762\u5185\u5BB9"), { code: "static_page_not_found" });
-}
-function buildWorkspaceBaseHref(userId, htmlRelativePath) {
- const key = String(userId ?? "").trim();
- 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}/` : ""}`;
-}
-function injectHtmlBaseHref(html, baseHref) {
- const safeBase = String(baseHref ?? "").replace(/"/g, "%22");
- if (!safeBase) return html;
- if (/]*href="[^"]*"[^>]*>/i, ``);
- }
- if (/]*>/i.test(html)) {
- return html.replace(/]*>/i, (match) => `${match}
-`);
- }
- return `${html}`;
-}
-function buildChatSavePreviewQuery({
- sessionId,
- messageId: messageId2,
- selectedLinkIndex = 0,
- previewTitle,
- previewSummary
-} = {}) {
- const params = new URLSearchParams({
- session_id: String(sessionId ?? ""),
- message_id: String(messageId2 ?? ""),
- selected_link_index: String(selectedLinkIndex)
- });
- const title = String(previewTitle ?? "").trim();
- const summary = String(previewSummary ?? "").trim();
- if (title) params.set("preview_title", title);
- if (summary) params.set("preview_summary", summary);
- return params;
-}
-function buildChatSavePreviewFrameUrl(input) {
- return `/api/mindspace/v1/pages/chat-save-preview?${buildChatSavePreviewQuery(input).toString()}`;
-}
-function buildChatSaveThumbnailUrl(input) {
- return `/api/mindspace/v1/pages/chat-save-thumbnail?${buildChatSavePreviewQuery(input).toString()}`;
-}
-function titleFromHtml2(html) {
- const match = String(html).match(/]*>([^<]+)<\/title>/i);
- return match?.[1]?.trim() ?? "";
-}
-function summaryFromHtml(html) {
- const stripped = String(html).replace(/`;
- return appendBeforeHeadClose(source, script);
-}
-function renderWechatSharePreviewHtml(preview, { note = "" } = {}) {
- const safe = (value) => String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- const title = safe(preview.title || "\u9875\u9762\u6807\u9898");
- const description = safe(preview.description || "");
- const siteName = safe(preview.siteName || PLATFORM_SITE_NAME);
- const imageUrl = safe(preview.imageUrl || "");
- const iconUrl = safe(preview.iconUrl || PLATFORM_BRAND_ICON_PATH);
- const pageUrl = safe(preview.pageUrl || "");
- const noteHtml = note ? `${safe(note)}
` : "";
- return `
-
-
-
-
- \u5FAE\u4FE1\u5206\u4EAB\u9884\u89C8 \xB7 ${title}
-
-
-
-
-
\u5FAE\u4FE1\u94FE\u63A5\u5361\u7247\u9884\u89C8
-
\u672C\u5730\u6A21\u62DF\u7684\u662F\u300C\u7C98\u8D34\u94FE\u63A5\u540E\u770B\u5230\u7684\u5361\u7247\u300D\uFF0C\u4E0D\u662F JS-SDK \u5185\u90E8\u5206\u4EAB\u5F39\u5C42\u3002${pageUrl ? `\u6E90\u94FE\u63A5\uFF1A${pageUrl}` : ""}
- ${noteHtml}
-
-
-
${title}
- ${description ? `
${description}
` : ""}
-
-
- ${imageUrl ? `
` : `\u65E0\u56FE
`}
-
-
-
-
-`;
-}
-
-// mindspace-thumbnail-png.mjs
-import fs27 from "node:fs";
-import { Resvg } from "@resvg/resvg-js";
-var RENDER_WIDTH = 540;
-function rasterizeThumbnailSvgToPng(svg) {
- const resvg = new Resvg(svg, {
- background: "white",
- fitTo: { mode: "width", value: RENDER_WIDTH },
- font: { loadSystemFonts: true }
- // CJK + serif via the host font stack (PingFang/Georgia on macOS)
- });
- return resvg.render().asPng();
-}
-function thumbnailPngPathForSvg(svgAbsPath) {
- return svgAbsPath.replace(/\.svg$/i, ".png");
-}
-function ensureThumbnailPng(svgAbsPath) {
- if (!fs27.existsSync(svgAbsPath)) return null;
- const pngPath = thumbnailPngPathForSvg(svgAbsPath);
- try {
- const svgStat = fs27.statSync(svgAbsPath);
- if (fs27.existsSync(pngPath) && fs27.statSync(pngPath).mtimeMs >= svgStat.mtimeMs) {
- return pngPath;
- }
- const svg = fs27.readFileSync(svgAbsPath, "utf8");
- fs27.writeFileSync(pngPath, rasterizeThumbnailSvgToPng(svg));
- return pngPath;
- } catch {
- return fs27.existsSync(pngPath) ? pngPath : null;
- }
-}
-
-// 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 {
- html: source.replace(/<\/body>/i, `${markup}`),
- scriptHashes: [PUBLIC_FILE_SHARE_SCRIPT_HASH]
- };
- }
- return {
- html: `${source}${markup}`,
- scriptHashes: [PUBLIC_FILE_SHARE_SCRIPT_HASH]
- };
-}
-function extractOgImageUrl(html) {
- return String(html ?? "").match(/]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i)?.[1] || String(html ?? "").match(/]+content=["']([^"']+)["'][^>]+property=["']og:image["']/i)?.[1] || "";
-}
-function extractShareMetaFromPageHtml(html) {
- const signals = extractCoverSignals(String(html ?? ""));
- return {
- title: detectPublishedPageTitle(html),
- subtitle: signals.subtitle
- };
-}
-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 "";
- const isLocalHost = /^(localhost|127\.0\.0\.1|\[::1\]|192\.168\.|10\.|100\.)/i.test(host);
- const fwdProto = (req.headers["x-forwarded-proto"] || "").toString().split(",")[0].trim();
- const proto = isLocalHost ? fwdProto || req.protocol || "http" : "https";
- return `${proto}://${host}`;
-}
-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, longImageUrl }) {
- const iframeSrc = escapePublicHtml(iframeUrl);
- const safeShareUrl = escapePublicHtml(shareUrl);
- const safeTitle = escapePublicHtml(title);
- const serializedShareUrl = JSON.stringify(shareUrl).replace(/
-
-
-
-
-
- ${safeTitle}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- \u5206\u4EAB\u94FE\u63A5
- ${safeShareUrl}
-
-
-
-
-
-
-`;
-}
-async function sendPublishedPage(req, res, result, { embed = false, raw = false } = {}) {
- let html = result.html;
- const origin = resolveRequestOrigin(req);
- const originalPath = req.originalUrl || req.url || "";
- const sharePath = removeQueryParam(removeQueryParam(originalPath, "download"), "export");
- const pageUrl = originalPath ? new URL(sharePath, origin || "http://localhost").toString().split("#")[0] : "";
- const pageDirUrl = pageUrl ? `${pageUrl.slice(0, pageUrl.lastIndexOf("/") + 1)}` : "";
- const wechatShare = !embed && isWechatUserAgent(req.get("user-agent") || "");
- if (embed) {
- html = preparePublicationHtmlForEmbed(html);
- allowPlazaEmbedFrame(res);
- } else if (raw) {
- html = stripPublicationHtmlCspMeta(html);
- }
- if (!embed) {
- try {
- html = injectOgTags(html, { origin, pageUrl, pageDirUrl });
- if (wechatShare) html = injectWechatShareBridge(html, { pageUrl });
- } catch {
- }
- }
- const isFullHtml = /^\s*]/i.test(html);
- if (!embed && !raw && isFullHtml && isLongImageDownloadRequest(req.query)) {
- try {
- const rawUrl = new URL(appendQueryParam(sharePath || originalPath, "view", "raw"), origin || "http://localhost");
- const image = await renderLongImageBuffer({ url: rawUrl.toString() });
- const longImageUrl = new URL(
- appendQueryParam(sharePath || originalPath, "download", "long-image"),
- origin || "http://localhost"
- ).toString();
- await registerPublishedLongImageArtifactForConversation({
- result,
- image,
- canonicalUrl: longImageUrl
- });
- res.set("Content-Type", "image/png");
- res.set("Content-Disposition", 'attachment; filename="mindspace-public-page.long.png"');
- res.set("Cache-Control", "no-store");
- return res.send(image);
- } catch (error) {
- return res.status(500).type("text/plain; charset=utf-8").send(`\u957F\u56FE\u751F\u6210\u5931\u8D25\uFF1A${error?.message || "\u672A\u77E5\u9519\u8BEF"}`);
- }
- }
- const canWrapWithShell = !embed && !raw && isFullHtml && result.publication?.accessMode !== "password";
- if (canWrapWithShell) {
- const title = detectPublishedPageTitle(html);
- const rawUrl = appendQueryParam(sharePath || originalPath, "view", "raw");
- const longImageUrl = appendQueryParam(sharePath || originalPath, "download", "long-image");
- let shellHtml = publishedPageShellHtml({
- iframeUrl: rawUrl,
- shareUrl: pageUrl,
- title,
- longImageUrl
- });
- try {
- shellHtml = injectOgTags(shellHtml, {
- origin,
- pageUrl,
- pageDirUrl,
- fallbackImageUrl: extractOgImageUrl(html),
- meta: extractShareMetaFromPageHtml(html)
- });
- if (wechatShare) shellHtml = injectWechatShareBridge(shellHtml, { pageUrl });
- } catch {
- }
- res.set("Content-Type", "text/html; charset=utf-8");
- res.set(
- "Content-Security-Policy",
- wechatShare ? "default-src 'none'; style-src 'unsafe-inline'; img-src data: https:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' https://cdn.jsdelivr.net https://res.wx.qq.com; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" : "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"
- );
- res.set(
- "Cache-Control",
- result.publication.accessMode === "public" ? "public, max-age=60" : "private, no-store"
- );
- return res.send(shellHtml);
- }
- res.set("Content-Type", "text/html; charset=utf-8");
- res.set("Content-Security-Policy", publishedPageCsp(html, { embed, raw, wechatShare }));
- res.set(
- "Cache-Control",
- result.publication.accessMode === "public" ? "public, max-age=60" : "private, no-store"
- );
- return res.send(html);
-}
-function passwordGateHtml(action) {
- return `
-
-
-
-
-
- \u53D7\u4FDD\u62A4\u9875\u9762
-
-
-
-
-
-`;
-}
-function escapePublicHtml(value) {
- return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
-}
-function publicHomepageHtml(data) {
- const cards = data.pages.map(
- (page) => `
-
- ${escapePublicHtml(page.templateId)}
- ${Number(page.viewCount)} \u6B21\u6D4F\u89C8
-
- ${escapePublicHtml(page.title)}
- ${escapePublicHtml(page.summary || "\u6682\u65E0\u6458\u8981")}
-
-`
- ).join("");
- return `
-
-
-
-
-
- ${escapePublicHtml(data.owner.displayName)} \xB7 MindSpace
-
-
-
-
-
- MindSpace Public
- ${escapePublicHtml(data.owner.displayName)}
- \u8FD9\u91CC\u5C55\u793A ${escapePublicHtml(data.owner.displayName)} \u5F53\u524D\u516C\u5F00\u53D1\u5E03\u4E14\u5728\u7EBF\u7684 MindSpace \u9875\u9762\u3002
-
- ${Number(data.pageCount)} \u4E2A\u516C\u5F00\u9875\u9762
- ${Number(data.totalViews)} \u6B21\u7D2F\u8BA1\u6D4F\u89C8
- /u/${escapePublicHtml(data.owner.slug)}
-
-
- ${data.pages.length ? `` : '\u8FD9\u4E2A\u4E3B\u9875\u8FD8\u6CA1\u6709\u516C\u5F00\u9875\u9762\u3002\u7A0D\u540E\u518D\u6765\u770B\u770B\uFF0C\u6216\u8005\u76F4\u63A5\u8BBF\u95EE\u4F5C\u8005\u5206\u4EAB\u7ED9\u4F60\u7684\u4E13\u5C5E\u94FE\u63A5\u3002'}
-
-
-`;
-}
-async function resolvePublishedRoute(req, res, password = null) {
- await userAuthReady;
- if (!mindSpacePublications) return res.status(503).send("MindSpace \u672A\u542F\u7528");
- try {
- const viewer = req.userSession && userAuth ? await userAuth.getMe(req.userToken) : null;
- const result = await mindSpacePublications.resolvePublic(
- req.params.ownerSlug,
- req.params.urlSlug,
- viewer?.id,
- password,
- {
- userAgent: req.get("user-agent"),
- referrer: req.get("referer")
- }
- );
- return await sendPublishedPage(req, res, result, {
- embed: isPlazaEmbedRequest(req.query),
- raw: String(req.query.view ?? "").toLowerCase() === "raw"
- });
- } catch (error) {
- if (error?.code === "publication_password_required") {
- return res.status(password ? 403 : 200).send(passwordGateHtml(req.originalUrl));
- }
- if (error?.code === "publication_login_required") {
- return res.status(401).send("\u8BF7\u5148\u767B\u5F55 TKMind \u540E\u518D\u8BBF\u95EE\u6B64\u9875\u9762");
- }
- if (error?.code === "publication_not_found") return res.status(404).send("\u9875\u9762\u4E0D\u5B58\u5728\u6216\u5DF2\u4E0B\u7EBF");
- return res.status(500).send("\u9875\u9762\u52A0\u8F7D\u5931\u8D25");
- }
-}
-app.use(async (req, res, next) => {
- const thumbnailMatch = /^\/u\/([^/]+)\/pages\/([^/]+)\.thumbnail\.png$/.exec(req.path);
- if (!thumbnailMatch) return next();
- const ownerSlug = thumbnailMatch[1];
- const urlSlug = thumbnailMatch[2];
- await userAuthReady;
- if (!authPool || !mindSpacePages) return res.status(503).send("MindSpace \u672A\u542F\u7528");
- try {
- const [rows] = await authPool.query(
- `SELECT pr.user_id, pr.page_id
- FROM h5_publish_records pr
- JOIN h5_users u ON u.id = pr.user_id
- WHERE COALESCE(u.slug, u.username) = ?
- AND pr.url_slug = ?
- AND pr.status = 'online'
- ORDER BY pr.published_at DESC
- LIMIT 1`,
- [ownerSlug, urlSlug]
- );
- const row = rows[0];
- if (!row) return res.status(404).send("\u7F29\u7565\u56FE\u4E0D\u5B58\u5728");
- const svg = await mindSpacePages.renderThumbnail(row.user_id, row.page_id);
- res.set("Content-Type", "image/png");
- res.set("Cache-Control", "public, max-age=300");
- return res.send(rasterizeThumbnailSvgToPng(svg));
- } catch (error) {
- return res.status(500).send("\u7F29\u7565\u56FE\u52A0\u8F7D\u5931\u8D25");
- }
-});
-app.get("/u/:ownerSlug/pages/:urlSlug", async (req, res) => {
- return resolvePublishedRoute(req, res);
-});
-app.get("/u/:ownerSlug", async (req, res) => {
- await userAuthReady;
- if (!mindSpacePublications) return res.status(503).send("MindSpace \u672A\u542F\u7528");
- try {
- const data = await mindSpacePublications.getPublicHomepage(req.params.ownerSlug);
- res.set("Content-Type", "text/html; charset=utf-8");
- res.set(
- "Content-Security-Policy",
- "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"
- );
- res.set("Cache-Control", "public, max-age=60");
- return res.send(publicHomepageHtml(data));
- } catch (error) {
- if (error?.code === "publication_not_found") return res.status(404).send("\u4E3B\u9875\u4E0D\u5B58\u5728");
- return res.status(500).send("\u4E3B\u9875\u52A0\u8F7D\u5931\u8D25");
- }
-});
-app.post(
- "/u/:ownerSlug/pages/:urlSlug",
- express2.urlencoded({ extended: false, limit: "2kb" }),
- async (req, res) => resolvePublishedRoute(req, res, req.body?.password)
-);
-app.get("/s/:token", async (req, res) => {
- await userAuthReady;
- if (!mindSpacePublications) return res.status(503).send("MindSpace \u672A\u542F\u7528");
- try {
- const viewer = req.userSession && userAuth ? await userAuth.getMe(req.userToken) : null;
- return await sendPublishedPage(
- req,
- res,
- await mindSpacePublications.resolvePrivateLink(req.params.token, viewer?.id, {
- userAgent: req.get("user-agent"),
- referrer: req.get("referer")
- }),
- {
- embed: isPlazaEmbedRequest(req.query),
- raw: String(req.query.view ?? "").toLowerCase() === "raw"
- }
- );
- } catch (error) {
- if (error?.code === "publication_not_found") return res.status(404).send("\u9875\u9762\u4E0D\u5B58\u5728\u6216\u5DF2\u4E0B\u7EBF");
- return res.status(500).send("\u9875\u9762\u52A0\u8F7D\u5931\u8D25");
- }
-});
-var USERNAME_SLUG = /^[a-z0-9_]{2,32}$/;
-async function resolvePublishDirKey(segment) {
- const lower = String(segment ?? "").trim().toLowerCase();
- if (!lower) return null;
- if (PUBLISH_KEY_UUID.test(lower)) return lower;
- if (USERNAME_SLUG.test(lower)) {
- if (authPool) {
- const [rows] = await authPool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [lower]);
- if (rows[0]?.id) return String(rows[0].id).toLowerCase();
- }
- return lower;
- }
- return null;
-}
-async function sendLongImageDownloadIfRequested(req, res, filePath) {
- if (!isLongImageDownloadRequest(req.query)) return false;
- const longImagePath = longImagePathForHtml(filePath);
- try {
- await renderLongImage({ htmlPath: filePath, outputPath: longImagePath });
- res.set("Cache-Control", "no-store");
- res.download(longImagePath, path34.basename(longImagePath), (err) => {
- if (err && !res.headersSent) res.status(404).json({ message: "\u957F\u56FE\u6587\u4EF6\u4E0D\u5B58\u5728" });
- });
- } catch (error) {
- res.status(500).type("text/plain; charset=utf-8").send(`\u957F\u56FE\u751F\u6210\u5931\u8D25\uFF1A${error?.message || "\u672A\u77E5\u9519\u8BEF"}`);
- }
- return true;
-}
-async function sendPublishFile(req, res, filePath) {
- if (!filePath.toLowerCase().endsWith(".html")) {
- res.sendFile(filePath, (err) => {
- if (err && !res.headersSent) res.status(404).json({ message: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
- });
- return;
- }
- if (await sendLongImageDownloadIfRequested(req, res, filePath)) return;
- let html;
- try {
- html = fs32.readFileSync(filePath, "utf8");
- } catch {
- res.status(404).json({ message: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
- return;
- }
- const embed = isPlazaEmbedRequest(req.query);
- if (embed) {
- html = preparePublicationHtmlForEmbed(html);
- allowPlazaEmbedFrame(res);
- res.set("Content-Security-Policy", publishedPageCsp(html, { embed }));
- }
- const host = (req.headers["x-forwarded-host"] || req.headers.host || "").toString().split(",")[0].trim();
- const isLocalHost = /^(localhost|127\.0\.0\.1|\[::1\]|192\.168\.|10\.|100\.)/i.test(host);
- const fwdProto = (req.headers["x-forwarded-proto"] || "").toString().split(",")[0].trim();
- const proto = isLocalHost ? fwdProto || req.protocol || "http" : "https";
- const origin = host ? `${proto}://${host}` : "";
- const cleanPath = req.originalUrl.split("?")[0].split("#")[0];
- const servedName = path34.basename(filePath);
- const urlLast = decodeURIComponent(cleanPath.split("/").filter(Boolean).pop() ?? "");
- const isImplicitIndex = urlLast.toLowerCase() !== servedName.toLowerCase();
- const pageUrl = origin ? `${origin}${isImplicitIndex && !cleanPath.endsWith("/") ? `${cleanPath}/` : cleanPath}` : "";
- const pageDirUrl = !origin ? "" : isImplicitIndex ? `${origin}${cleanPath.endsWith("/") ? cleanPath : `${cleanPath}/`}` : `${origin}${cleanPath.slice(0, cleanPath.lastIndexOf("/") + 1)}`;
- let fallbackImageUrl = "";
- const svgSibling = filePath.replace(/\.[^./]+$/, ".thumbnail.svg");
- if (pageDirUrl && fs32.existsSync(svgSibling)) {
- const pngName = path34.basename(thumbnailPngPathForSvg(svgSibling));
- fallbackImageUrl = `${pageDirUrl}${pngName}`;
- }
- try {
- html = injectOgTags(html, { origin, pageUrl, pageDirUrl, fallbackImageUrl });
- const wechatShare = !embed && isWechatUserAgent(req.get("user-agent") || "");
- if (wechatShare) {
- html = injectWechatShareBridge(html, { pageUrl });
- }
- const shareInjection = !embed ? injectPublicFileShareButton(html) : { html, scriptHashes: [] };
- html = shareInjection.html;
- res.set("Content-Security-Policy", publishedPageCsp(html, {
- embed,
- wechatShare,
- scriptHashes: shareInjection.scriptHashes
- }));
- } catch {
- res.set("Content-Security-Policy", publishedPageCsp(html, { embed }));
- }
- res.set("Content-Type", "text/html; charset=utf-8");
- res.send(html);
-}
-var MISPLACED_PUBLIC_HTML_NAME = /^[a-z0-9][a-z0-9._-]{0,127}\.html$/i;
-async function recoverMisplacedPublicHtml(targetDir, resolvedRoot, rest) {
- if (rest.length !== 2 || rest[0] !== PUBLIC_ZONE_DIR) return null;
- const filename = rest[1];
- if (!MISPLACED_PUBLIC_HTML_NAME.test(filename)) return null;
- const destination = path34.resolve(targetDir, PUBLIC_ZONE_DIR, filename);
- if (!destination.startsWith(`${resolvedRoot}${path34.sep}`)) return null;
- const candidates = [
- path34.resolve(__dirname5, filename),
- path34.resolve(__dirname5, PUBLIC_ZONE_DIR, filename)
- ];
- for (const candidate of candidates) {
- if (candidate === destination) continue;
- if (!candidate.startsWith(`${__dirname5}${path34.sep}`)) continue;
- if (!fs32.existsSync(candidate) || !fs32.statSync(candidate).isFile()) continue;
- fs32.mkdirSync(path34.dirname(destination), { recursive: true });
- fs32.copyFileSync(candidate, destination);
- await ensureWorkspaceHtmlThumbnail(targetDir, `${PUBLIC_ZONE_DIR}/${filename}`).catch(() => {
- });
- console.warn(
- `[MindSpace] recovered misplaced public HTML ${path34.relative(__dirname5, candidate)} -> ${path34.relative(__dirname5, destination)}`
- );
- return destination;
- }
- return null;
-}
-function decodePathSegment3(segment) {
- try {
- return decodeURIComponent(segment);
- } catch {
- return segment;
- }
-}
-async function serveUserPublishFile(req, res, next) {
- const parts = req.path.split("/").filter(Boolean);
- if (parts.length < 1) {
- res.status(404).json({ message: "\u672A\u627E\u5230\u9875\u9762" });
- return;
- }
- const dirKey = await resolvePublishDirKey(parts[0]);
- if (!dirKey) {
- res.status(404).json({ message: "\u672A\u627E\u5230\u9875\u9762" });
- return;
- }
- if (parts[0].toLowerCase() !== dirKey && USERNAME_SLUG.test(parts[0])) {
- const rest2 = parts.slice(1).map(encodeURIComponent).join("/");
- const target = `/${PUBLISH_ROOT_DIR}/${dirKey}${rest2 ? `/${rest2}` : "/"}`;
- res.redirect(301, target);
- return;
- }
- const [username, ...rest] = [dirKey, ...parts.slice(1).map(decodePathSegment3)];
- const targetDir = path34.join(__dirname5, PUBLISH_ROOT_DIR, username);
- const resolvedRoot = path34.resolve(targetDir);
- const filePath = path34.join(targetDir, ...rest);
- const resolvedPath = path34.resolve(filePath);
- if (!resolvedPath.startsWith(`${resolvedRoot}${path34.sep}`) && resolvedPath !== resolvedRoot) {
- res.status(403).json({ message: "\u7981\u6B62\u8BBF\u95EE" });
- return;
- }
- if (!fs32.existsSync(targetDir)) {
- res.status(404).json({ message: "\u7528\u6237\u4E0D\u5B58\u5728" });
- return;
- }
- if (/\.thumbnail\.png$/i.test(resolvedPath)) {
- const svgSibling = resolvedPath.replace(/\.png$/i, ".svg");
- if (fs32.existsSync(svgSibling)) {
- const pngPath = ensureThumbnailPng(svgSibling);
- if (pngPath && fs32.existsSync(pngPath)) {
- res.set("Cache-Control", "public, max-age=300");
- res.sendFile(pngPath);
- return;
- }
- }
- }
- if (!fs32.existsSync(resolvedPath)) {
- if (rest.length === 1 && rest[0].toLowerCase().endsWith(".html")) {
- const publicFallback = path34.resolve(targetDir, PUBLIC_ZONE_DIR, rest[0]);
- if (publicFallback.startsWith(`${resolvedRoot}${path34.sep}`) && fs32.existsSync(publicFallback) && fs32.statSync(publicFallback).isFile()) {
- const canonical = `/${PUBLISH_ROOT_DIR}/${encodeURIComponent(username)}/${PUBLIC_ZONE_DIR}/${encodeURIComponent(rest[0])}`;
- res.redirect(301, canonical);
- return;
- }
- }
- if (rest.length === 2 && rest[0] === PUBLIC_ZONE_DIR && rest[1].toLowerCase().endsWith(".html")) {
- const similarRelativePath = await resolveClosestHtmlRelativePath(targetDir, `${PUBLIC_ZONE_DIR}/${rest[1]}`);
- if (similarRelativePath) {
- const canonical = `/${PUBLISH_ROOT_DIR}/${encodeURIComponent(username)}/${similarRelativePath.split("/").map((part) => encodeURIComponent(part)).join("/")}`;
- res.redirect(301, canonical);
- return;
- }
- }
- const recoveredPublicHtml = await recoverMisplacedPublicHtml(targetDir, resolvedRoot, rest);
- if (recoveredPublicHtml) {
- await sendPublishFile(req, res, recoveredPublicHtml);
- return;
- }
- res.status(404).json({ message: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
- return;
- }
- if (fs32.statSync(resolvedPath).isDirectory()) {
- const indexPath = path34.join(resolvedPath, "index.html");
- if (fs32.existsSync(indexPath)) {
- await sendPublishFile(req, res, indexPath);
- return;
- }
- res.status(404).json({ message: "\u76EE\u5F55\u4E2D\u6CA1\u6709 index.html" });
- return;
- }
- await sendPublishFile(req, res, resolvedPath);
-}
-app.use("/temp", (req, res) => {
- res.redirect(301, `/${PUBLISH_ROOT_DIR}${req.url}`);
-});
-app.use("/user", async (req, res, next) => {
- await userAuthReady;
- const parts = req.path.split("/").filter(Boolean);
- if (parts.length < 1) return next();
- const [username, ...rest] = parts;
- const targetDir = path34.join(USERS_ROOT, username);
- const filePath = path34.join(targetDir, ...rest);
- const resolvedRoot = path34.resolve(targetDir);
- const resolvedPath = path34.resolve(filePath);
- if (!resolvedPath.startsWith(resolvedRoot + path34.sep) && resolvedPath !== resolvedRoot) {
- return res.status(403).json({ message: "\u7981\u6B62\u8BBF\u95EE" });
- }
- if (!fs32.existsSync(targetDir)) {
- return res.status(404).json({ message: "\u7528\u6237\u4E0D\u5B58\u5728" });
- }
- if (!fs32.existsSync(resolvedPath)) {
- return res.status(404).json({ message: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
- }
- if (fs32.statSync(resolvedPath).isDirectory()) {
- const indexPath = path34.join(resolvedPath, "index.html");
- if (fs32.existsSync(indexPath)) {
- return res.sendFile(indexPath);
- }
- return res.status(404).json({ message: "\u76EE\u5F55\u4E2D\u6CA1\u6709 index.html" });
- }
- res.sendFile(resolvedPath, (err) => {
- if (err) res.status(404).json({ message: "\u6587\u4EF6\u4E0D\u5B58\u5728" });
- });
-});
-app.get(`/${PUBLISH_ROOT_DIR}/wiki/*`, (_req, res) => {
- res.sendFile(path34.join(__dirname5, PUBLISH_ROOT_DIR, "wiki", "index.html"));
-});
-app.get("/temp/wiki/*", (req, res) => {
- res.redirect(301, `/${PUBLISH_ROOT_DIR}/wiki${req.url.slice("/temp/wiki".length)}`);
-});
-app.use(
- "/plaza-covers",
- express2.static(path34.join(__dirname5, "public/plaza-covers"), { maxAge: "7d" })
-);
-app.use("/brand", express2.static(path34.join(__dirname5, "public/brand"), { maxAge: "7d" }));
-app.get("/dev/wechat-share-preview", async (req, res) => {
- try {
- const rawTarget = String(req.query.url ?? req.query.path ?? "").trim();
- if (!rawTarget) {
- return res.status(400).type("text/plain; charset=utf-8").send("\u7F3A\u5C11 url \u53C2\u6570\uFF0C\u4F8B\u5982 ?url=/MindSpace/john/public/demo.html");
- }
- const origin = resolveRequestOrigin(req) || `http://${HOST}:${PORT}`;
- const targetUrl = rawTarget.startsWith("http") ? new URL(rawTarget) : new URL(rawTarget.startsWith("/") ? rawTarget : `/${rawTarget}`, origin);
- const upstream = await fetch(targetUrl.toString(), {
- headers: {
- "user-agent": req.get("user-agent") || "tkmind-wechat-share-preview",
- accept: "text/html,*/*"
- },
- redirect: "follow"
- });
- if (!upstream.ok) {
- return res.status(upstream.status).type("text/plain; charset=utf-8").send(`\u9875\u9762\u8BF7\u6C42\u5931\u8D25\uFF1AHTTP ${upstream.status}`);
- }
- let html = await upstream.text();
- const pageUrl = targetUrl.toString().split("#")[0];
- const pageDirUrl = `${pageUrl.slice(0, pageUrl.lastIndexOf("/") + 1)}`;
- html = injectOgTags(html, { origin: targetUrl.origin, pageUrl, pageDirUrl });
- const preview = extractSharePreviewMeta(html, {
- origin: targetUrl.origin,
- pageUrl,
- pageDirUrl
- });
- res.set("Content-Type", "text/html; charset=utf-8");
- res.set("Cache-Control", "no-store");
- return res.send(
- renderWechatSharePreviewHtml(preview, {
- note: "\u6B64\u9884\u89C8\u8BFB\u53D6\u9875\u9762\u6700\u7EC8 HTML \u4E2D\u7684 Open Graph \u6807\u7B7E\uFF0C\u53EF\u7528\u6765\u5BF9\u7167\u5FAE\u4FE1\u91CC\u7C98\u8D34\u94FE\u63A5\u540E\u7684\u5361\u7247\u6548\u679C\u3002"
- })
- );
- } catch (err) {
- return res.status(500).type("text/plain; charset=utf-8").send(String(err?.message ?? err));
- }
-});
-app.use("/dev", express2.static(path34.join(__dirname5, "public/dev"), { maxAge: 0 }));
-app.get(/^\/MP_verify_[A-Za-z0-9]+\.txt$/, (req, res) => {
- const fileName = path34.basename(req.path);
- const filePath = path34.join(__dirname5, "public", fileName);
- if (!fs32.existsSync(filePath)) return res.status(404).end();
- res.type("text/plain").sendFile(filePath);
-});
-app.use("/auth", (_req, res) => {
- res.status(404).json({ message: "\u63A5\u53E3\u4E0D\u5B58\u5728\uFF0C\u8BF7\u91CD\u542F\u540E\u7AEF\u670D\u52A1\uFF08node server.mjs \u6216 pnpm dev\uFF09" });
-});
-app.use("/admin-api", (_req, res) => {
- res.status(404).json({ message: "\u63A5\u53E3\u4E0D\u5B58\u5728\uFF0C\u8BF7\u91CD\u542F\u540E\u7AEF\u670D\u52A1\uFF08node server.mjs \u6216 pnpm dev\uFF09" });
-});
-app.use(express2.static(path34.join(__dirname5, "dist"), { index: "index.html" }));
-app.get("*", (_req, res) => {
- res.sendFile(path34.join(__dirname5, "dist", "index.html"));
-});
-userAuthReady.then((enabled) => {
- if (isDatabaseConfigured() && !enabled && process.env.NODE_ENV === "production") {
- console.error("Refusing to start portal without user auth while database is configured");
- process.exit(1);
- }
- app.listen(PORT, HOST, () => {
- console.log(`TKMind H5 @ http://${HOST}:${PORT}`);
- console.log(`Proxy -> ${API_TARGETS.join(", ")}`);
- console.log(`Auth -> ${enabled ? "multi-user (MySQL)" : legacyAuth ? "legacy password" : "disabled"}`);
- console.log(`Wiki @ http://${HOST}:${PORT}/${PUBLISH_ROOT_DIR}/wiki`);
- });
-});
diff --git a/.runtime/portal/skills/code-playground/SKILL.md b/.runtime/portal/skills/code-playground/SKILL.md
deleted file mode 100644
index aa68e64..0000000
--- a/.runtime/portal/skills/code-playground/SKILL.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-name: code-playground
-description: 代码展示与运行技能:在聊天中展示代码并支持 JavaScript 沙盒执行(需启用 codeplayground 扩展)
----
-
-# 代码演练场
-
-使用 `codeplayground` MCP 扩展在聊天中展示可运行的代码片段。
-
-## 何时使用
-
-- 生成代码示例后,让用户直接在聊天里运行验证
-- 展示算法、数据处理逻辑
-- JavaScript 演示(可执行);其他语言(展示+复制)
-
-## 前提
-
-Settings → Extensions 中启用 **Code Playground** 扩展。
-
-## 工具
-
-`show_playground(title, code, language, description?)` — 展示代码并可选执行
-
-## 支持语言
-
-- **JavaScript** — 有「运行」按钮,`console.log` 输出显示在下方
-- 其他语言(`rust`、`python`、`typescript` 等)— 展示+语法高亮+复制按钮
-
-## 规则
-
-1. JS 执行在沙盒 iframe 中,无网络访问,无 DOM 操作
-2. 代码不超过 200 行,超出拆分展示
-3. 有副作用(写文件、网络请求)的代码标注「仅供参考,不可直接运行」
diff --git a/.runtime/portal/skills/diff-viewer/SKILL.md b/.runtime/portal/skills/diff-viewer/SKILL.md
deleted file mode 100644
index f2bd60b..0000000
--- a/.runtime/portal/skills/diff-viewer/SKILL.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: diff-viewer
-description: 代码对比可视化技能:以高亮 diff 形式展示文件修改前后(需启用 diffviewer 扩展)
----
-
-# Diff 查看器
-
-使用 `diffviewer` MCP 扩展在聊天中渲染代码修改对比。
-
-## 何时使用
-
-- 用户要看代码改动前后对比
-- 展示重构、bug 修复、配置变更的效果
-- 比 markdown 代码块更直观地呈现变更
-
-## 前提
-
-Settings → Extensions 中启用 **Diff Viewer** 扩展。
-
-## 工具
-
-`show_diff(title, old_content, new_content, language?)` — 渲染带行号的 diff
-
-## 规则
-
-1. `language` 填文件类型(`rust`、`typescript`、`python` 等),用于语法提示
-2. 内容太大时只截取关键改动部分,加说明
-3. 配合 `git_diff` 工具使用效果最佳
diff --git a/.runtime/portal/skills/docx-generate/SKILL.md b/.runtime/portal/skills/docx-generate/SKILL.md
deleted file mode 100644
index ea2a3e6..0000000
--- a/.runtime/portal/skills/docx-generate/SKILL.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-name: docx-generate
-description: 在工作区内用 Python 标准库(zipfile + XML)生成 Word .docx,无需 python-docx 或 docx_tool
----
-
-# Word 文档生成(.docx)
-
-## 重要说明
-
-- **平台没有 `docx_tool` / `update_doc`**,不要编造或调用不存在的工具
-- **`write_file` 不能写二进制 .docx**,不要用 write_file 假装生成 Word
-- **禁止**在 HTML 里用 `data:application/...;base64,...` 内嵌 docx(极易被截断损坏,下载后乱码)
-- 需要公网下载时:先用本脚本生成 `.docx` 落盘,再在 HTML 里用**相对路径**链接(如 `public/方案.docx`)
-- 生产沙箱通常**不能 pip install**,优先用本技能自带的 **stdlib 脚本**
-- 生成后必须用 **`list_dir oa/`**(或目标目录)确认文件已落盘,再告诉用户
-
-## 何时使用
-
-- 用户要 Word / docx / .doc 文档(输出 `.docx`)
-- 需要保存到 `oa/`、`private/`、`public/` 等分区
-
-## 推荐命令
-
-技能目录内有 `generate_docx.py`(仅依赖 Python 3 标准库):
-
-```bash
-python3 .agents/skills/docx-generate/generate_docx.py --json - --output oa/报告.docx <<'EOF'
-{
- "title": "文档标题",
- "sections": [
- {
- "heading": "一、章节标题",
- "paragraphs": ["段落一", "段落二"],
- "table": {
- "headers": ["列1", "列2"],
- "rows": [["A", "B"], ["C", "D"]]
- }
- }
- ]
-}
-EOF
-```
-
-然后:
-
-```bash
-list_dir oa
-```
-
-## JSON 字段
-
-| 字段 | 说明 |
-|------|------|
-| `title` | 文档主标题(可选) |
-| `sections[]` | 章节数组 |
-| `sections[].heading` | 章节标题 |
-| `sections[].paragraphs` | 字符串段落列表 |
-| `sections[].table.headers` | 表头 |
-| `sections[].table.rows` | 表格行 |
-
-**禁止**把 `` 等 OOXML 标签写进 `paragraphs` 文本里;表格只能走 `table` 字段。
-
-## 公网下载页(HTML + docx)
-
-用户要「打开链接下载 Word」时:
-
-1. 用本脚本生成 docx(建议 `public/文件名.docx` 或 `oa/文件名.docx` 再复制到 `public/`)
-2. 用 `static-page-publish` 写下载页,`href` 指向**同目录相对路径**:
-
-```html
-下载 Word 文档
-```
-
-3. **禁止** `href="data:...;base64,..."` 嵌入 docx
-
-## 备选方案
-
-1. **用户要在线查看、可分享**:用 `static-page-publish` 技能写 `public/xxx.html`
-2. **环境有 python-docx**(工作区 `.venv` 已装):仍可用,但生成后必须 `list_dir` 验证
-3. **禁止**在未验证文件存在时宣称「已生成 docx」
diff --git a/.runtime/portal/skills/docx-generate/generate_docx.py b/.runtime/portal/skills/docx-generate/generate_docx.py
deleted file mode 100644
index e0a3748..0000000
--- a/.runtime/portal/skills/docx-generate/generate_docx.py
+++ /dev/null
@@ -1,214 +0,0 @@
-#!/usr/bin/env python3
-"""Generate a minimal valid .docx using only Python stdlib (zipfile + XML)."""
-
-from __future__ import annotations
-
-import argparse
-import json
-import sys
-import zipfile
-from pathlib import Path
-from xml.sax.saxutils import escape
-
-W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
-R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
-CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types"
-REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
-CP_NS = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
-DC_NS = "http://purl.org/dc/elements/1.1"
-
-CONTENT_TYPES = f"""
-
-
-
-
-
-"""
-
-ROOT_RELS = f"""
-
-
-
-"""
-
-DOCUMENT_RELS = f"""
-"""
-
-
-def sanitize_text(text: str) -> str:
- """Strip emoji and XML-illegal control chars (Word/iOS rejects these)."""
- cleaned = str(text or "")
- cleaned = cleaned.encode("utf-8", "ignore").decode("utf-8")
- cleaned = "".join(
- ch
- for ch in cleaned
- if not (0xD800 <= ord(ch) <= 0xDFFF)
- and (ord(ch) == 0x9 or ord(ch) == 0xA or ord(ch) == 0xD or ord(ch) >= 0x20)
- and not (0x1F000 <= ord(ch) <= 0x1FAFF or 0x2600 <= ord(ch) <= 0x27BF)
- )
- return cleaned.strip()
-
-
-def xml_text(text: str) -> str:
- return escape(sanitize_text(text))
-
-
-def run(text: str, bold: bool = False) -> str:
- props = "" if bold else ""
- return (
- f'{props}'
- f'{xml_text(text)}'
- )
-
-
-def paragraph(parts: str, style: str | None = None) -> str:
- ppr = f'' if style else ""
- return f"{ppr}{parts}"
-
-
-def heading(text: str) -> str:
- cleaned = sanitize_text(text)
- if not cleaned:
- return ""
- return paragraph(run(cleaned, bold=True))
-
-
-def body_paragraph(text: str) -> str:
- cleaned = sanitize_text(text)
- if not cleaned:
- return ""
- return paragraph(run(cleaned))
-
-
-def is_ooxml_fragment(text: str) -> bool:
- stripped = str(text or "").strip()
- return stripped.startswith(" str:
- col_count = max(len(headers), max((len(r) for r in rows), default=0))
- if col_count == 0:
- return ""
-
- col_width = max(1800, 9000 // col_count)
-
- def cell(text: str) -> str:
- return (
- f''
- f"{paragraph(run(text))}"
- )
-
- def table_row(cells: list[str]) -> str:
- padded = cells + [""] * (col_count - len(cells))
- return f"{''.join(cell(c) for c in padded[:col_count])}"
-
- grid_cols = "".join(f'' for _ in range(col_count))
- tbl_pr = (
- ""
- ''
- ""
- ''
- ''
- ''
- ''
- ''
- ''
- ""
- ""
- )
- parts = ["", tbl_pr, f"{grid_cols}"]
- if headers:
- parts.append(table_row(headers))
- parts.extend(table_row(r) for r in rows)
- parts.append("")
- return "".join(parts)
-
-
-def build_document_xml(title: str, sections: list[dict]) -> str:
- body: list[str] = []
- if title:
- block = heading(title)
- if block:
- body.append(block)
-
- for section in sections:
- heading_text = section.get("heading") or section.get("title")
- if heading_text:
- block = heading(str(heading_text))
- if block:
- body.append(block)
- for para in section.get("paragraphs") or []:
- text = str(para).strip()
- if text and not is_ooxml_fragment(text):
- block = body_paragraph(text)
- if block:
- body.append(block)
- table = section.get("table")
- if isinstance(table, dict):
- block = table_block(
- list(table.get("headers") or []),
- [list(r) for r in (table.get("rows") or [])],
- )
- if block:
- body.append(block)
-
- sect_pr = (
- f''
- f''
- )
- inner = "".join(body) + sect_pr
- return (
- f''
- f''
- f"{inner}"
- )
-
-
-def core_xml(title: str) -> str:
- return (
- f''
- f''
- f"{xml_text(title)}"
- f""
- )
-
-
-def write_docx(output: Path, title: str, sections: list[dict]) -> None:
- output.parent.mkdir(parents=True, exist_ok=True)
- document_xml = build_document_xml(title, sections)
- with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as zf:
- zf.writestr("[Content_Types].xml", CONTENT_TYPES)
- zf.writestr("_rels/.rels", ROOT_RELS)
- zf.writestr("word/_rels/document.xml.rels", DOCUMENT_RELS)
- zf.writestr("word/document.xml", document_xml)
- zf.writestr("docProps/core.xml", core_xml(title))
-
-
-def load_payload(args: argparse.Namespace) -> dict:
- if args.json:
- source = sys.stdin.read() if args.json == "-" else Path(args.json).read_text(encoding="utf-8")
- return json.loads(source)
- if args.title:
- return {"title": args.title, "sections": [{"paragraphs": args.paragraph or []}]}
- raise SystemExit("需要 --json (或 - 读 stdin),或 --title")
-
-
-def main() -> None:
- parser = argparse.ArgumentParser(description="用 Python 标准库生成 .docx(无需 python-docx)")
- parser.add_argument("--output", required=True, help="输出路径,如 oa/报告.docx")
- parser.add_argument("--json", help="JSON 内容文件;传 - 则从 stdin 读取")
- parser.add_argument("--title", help="仅标题 + --paragraph 时的文档标题")
- parser.add_argument("--paragraph", action="append", help="配合 --title 追加段落")
- args = parser.parse_args()
-
- payload = load_payload(args)
- title = str(payload.get("title") or "")
- sections = list(payload.get("sections") or [])
- output = Path(args.output)
- write_docx(output, title, sections)
- size = output.stat().st_size
- print(f"已生成 {output}({size} 字节)")
-
-
-if __name__ == "__main__":
- main()
diff --git a/.runtime/portal/skills/form-builder/SKILL.md b/.runtime/portal/skills/form-builder/SKILL.md
deleted file mode 100644
index 829d9cb..0000000
--- a/.runtime/portal/skills/form-builder/SKILL.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-name: form-builder
-description: 动态表单技能:向用户展示结构化输入表单,收集多字段信息(需启用 formbuilder 扩展)
----
-
-# 表单收集
-
-使用 `formbuilder` MCP 扩展在聊天中展示交互式表单,替代多轮逐一询问。
-
-## 何时使用
-
-- 需要收集 3 个以上结构化字段
-- 有下拉选项、必填校验的场景
-- 配置向导、信息录入、参数填写
-
-## 前提
-
-Settings → Extensions 中启用 **Form Builder** 扩展。
-
-## 工具
-
-`show_form(title, description?, fields)` — 渲染表单并等待用户提交
-
-## 字段类型
-
-| type | 说明 |
-|------|------|
-| `text` | 单行文本 |
-| `number` | 数字 |
-| `textarea` | 多行文本 |
-| `select` | 下拉选择(需提供 `options`) |
-| `checkbox` | 勾选框 |
-
-## 规则
-
-1. 用户提交后,结果以 JSON 回传,解析后继续任务
-2. `required: true` 的字段前端会校验,不会提交空值
-3. 表单不超过 8 个字段,太多拆成多步
diff --git a/.runtime/portal/skills/git/SKILL.md b/.runtime/portal/skills/git/SKILL.md
deleted file mode 100644
index f7acaee..0000000
--- a/.runtime/portal/skills/git/SKILL.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-name: git
-description: Git 操作技能:查看状态、diff、提交历史、blame、提交代码
----
-
-# Git 操作
-
-使用内置 `git` 平台扩展完成常见版本控制任务。
-
-## 何时使用
-
-- 用户询问代码改动、提交历史、谁修改了某行
-- 用户要提交代码、查看 diff、切换分支
-
-## 工具
-
-| 工具 | 用途 |
-|------|------|
-| `git_status` | 查看工作区状态和当前分支 |
-| `git_diff` | 查看未暂存或已暂存的改动 |
-| `git_log` | 查看提交历史 |
-| `git_blame` | 查看指定文件某几行的最后修改人 |
-| `git_commit` | 暂存指定文件并提交(自动添加 -s 签名) |
-
-## 规则
-
-1. `git_commit` 前先用 `git_status` 确认改动范围,避免误提交
-2. 提交信息用英文、简短、说明 why 而非 what
-3. 不要提交 `.env`、密钥、大二进制文件
-4. `git_diff` 的 `staged: true` 查看已暂存内容,默认查看未暂存
diff --git a/.runtime/portal/skills/kanban/SKILL.md b/.runtime/portal/skills/kanban/SKILL.md
deleted file mode 100644
index d36de83..0000000
--- a/.runtime/portal/skills/kanban/SKILL.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-name: kanban
-description: 看板可视化技能:将任务列表渲染为多列看板(需启用 kanban 扩展)
----
-
-# 看板展示
-
-使用 `kanban` MCP 扩展在聊天中渲染任务看板。
-
-## 何时使用
-
-- 用户要查看项目进度、任务分布
-- 展示待办/进行中/已完成三阶段状态
-- 多个任务需要按优先级可视化
-
-## 前提
-
-Settings → Extensions 中启用 **Kanban Board** 扩展。
-
-## 工具
-
-`show_kanban(title, columns)` — 渲染 N 列看板
-
-## 数据结构
-
-```
-columns: [
- { name: "待处理", cards: [
- { id: "1", title: "修复登录 bug", priority: "high", description: "..." }
- ]},
- { name: "进行中", cards: [...] },
- { name: "已完成", cards: [...] }
-]
-```
-
-## 优先级
-
-`high`(红)/ `medium`(黄)/ `low`(灰)
-
-## 规则
-
-1. 列数不限,但超过 5 列会很挤,建议合并
-2. 每列卡片不超过 20 张,否则截断并说明总数
diff --git a/.runtime/portal/skills/long-image-download/SKILL.md b/.runtime/portal/skills/long-image-download/SKILL.md
deleted file mode 100644
index df7c7d4..0000000
--- a/.runtime/portal/skills/long-image-download/SKILL.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-name: long-image-download
-description: 用 Playwright 将 MindSpace HTML 页面生成同名 PNG 长图,并返回可打开图片链接与附件下载链接
----
-
-# 长图下载(Playwright)
-
-## 何时使用
-
-- 用户要求“长图下载”“保存长图”“页面图片格式”“整页 PNG”
-- 已经或即将用 `static-page-publish` 生成 `public/*.html`
-
-## 必须流程
-
-1. 先确保 HTML 已经用 `write_file` / `edit_file` 写入 `public/<页面名>.html`
-2. 调用 sandbox-fs 工具 `generate_long_image`:
-
-```json
-{
- "html_path": "public/report.html",
- "output_path": "public/report.long.png"
-}
-```
-
-3. 用 `list_dir public` 确认 `<页面名>.long.png` 已存在
-4. 回复用户时同时给:
- - 页面链接:`[页面标题](.../public/report.html)`
- - 长图预览链接:`[长图预览](.../public/report.long.png)`
- - 长图下载链接:`[下载长图](.../public/report.html?download=long-image)`
-
-## 规则
-
-- `generate_long_image` 使用平台 Playwright,不要用 shell、自写截图脚本、`html2canvas` 或 `.thumbnail.svg` 冒充长图
-- `.thumbnail.svg` 只是信息流封面,不是整页长图
-- 长图文件名推荐与 HTML 同名:`report.html` -> `report.long.png`
-- 没有确认 `.long.png` 已生成时,不要声称“长图已生成”
diff --git a/.runtime/portal/skills/product-campaign-page/SKILL.md b/.runtime/portal/skills/product-campaign-page/SKILL.md
deleted file mode 100644
index c1ffe14..0000000
--- a/.runtime/portal/skills/product-campaign-page/SKILL.md
+++ /dev/null
@@ -1,72 +0,0 @@
----
-name: product-campaign-page
-description: 商品宣传 / 活动页技能:基于商品链接、主题、图片素材和卖点,生成可购买跳转的 H5 商品宣传页或活动落地页
----
-
-# 商品宣传 / 活动页
-
-本技能用于帮助电商商家、品牌主、达人或运营人员,把一个商品链接、商品信息和图片素材重新包装成更适合传播与转化的商品宣传页、活动页或 H5 落地页。
-
-## 何时使用
-
-- 用户要「做商品宣传页 / 活动页 / 产品落地页 / 带购买按钮的页面」
-- 用户提供商品链接,希望包装成更好看的推广页面
-- 用户上传商品图、Logo、模特图、详情图,希望生成页面文案和视觉结构
-- 用户要做促销、上新、种草、达人推荐、节日活动或私域转化页面
-
-## 必问信息
-
-如果用户没有一次性给全,优先只问这 3 个问题:
-
-1. 商品链接是什么?购买按钮会跳转到这个链接。
-2. 希望页面是什么主题或风格?例如高级感、夏日清爽、科技感、国潮、节日促销、达人种草。
-3. 有没有要上传的商品图、Logo、模特图、详情图或参考图?
-
-需要生成完整页面时,再补问:
-
-- 商品名称
-- 目标人群
-- 3-5 个真实卖点
-- 价格、优惠、限时活动、赠品或库存信息
-- 品牌色、禁用词、平台限制或合规要求
-
-## 工作流
-
-1. 先整理输入
- - 把商品链接作为唯一购买跳转地址。
- - 从用户文本和图片中提取商品名、主题、卖点、价格与素材线索。
- - 不要编造价格、销量、认证、功效、明星背书或平台承诺。
-
-2. 定义包装方向
- - 给出一句清晰的商品定位。
- - 说明目标人群、核心痛点、购买理由和活动钩子。
- - 如果主题不明确,给用户 2-3 个可选方向。
-
-3. 设计页面结构
- - 首屏必须直接出现商品或活动主张。
- - 至少包含:Hero、卖点区、使用场景或信任背书、活动优惠、底部购买 CTA。
- - 主 CTA 和底部 CTA 都必须跳转到用户提供的商品链接。
-
-4. 生成页面或文案
- - 如果用户只要策划,输出页面结构、标题、副标题、卖点文案、CTA 文案和视觉方向。
- - 如果用户要 H5 页面,使用 `static-page-publish` 技能在用户专属 MindSpace 发布目录生成静态 HTML,并返回 Markdown 可点击链接。
- - 生成 HTML 时必须写入与商品主题一致的 `mindspace-cover` 元数据,便于信息流生成封面。
-
-## 页面标准
-
-- 首屏要看到商品名、核心利益点和购买按钮。
-- 页面视觉必须服务商品主题,不要套用无关模板。
-- 文案要具体,避免「高端大气」「品质保证」这类空泛表达。
-- 有图片时优先使用用户上传素材;没有图片时明确标注需要补充的图片位置。
-- 活动信息未知时用可替换占位,不要虚构折扣、倒计时或库存。
-- 不处理支付,不模仿第三方平台收银台,只跳转原始商品链接。
-
-## 推荐页面结构
-
-1. Hero 首屏:商品名、主标题、副标题、商品图、立即购买按钮。
-2. 场景痛点:用户为什么需要它。
-3. 核心卖点:3-5 个真实利益点。
-4. 使用场景:生活方式、达人推荐、对比或人群场景。
-5. 活动优惠:价格、优惠券、赠品、限时利益点。
-6. 信任背书:评价、售后、品牌说明;没有真实信息时不编造。
-7. 底部 CTA:再次引导跳转到原商品链接。
diff --git a/.runtime/portal/skills/schedule-assistant/SKILL.md b/.runtime/portal/skills/schedule-assistant/SKILL.md
deleted file mode 100644
index a6bd44b..0000000
--- a/.runtime/portal/skills/schedule-assistant/SKILL.md
+++ /dev/null
@@ -1,56 +0,0 @@
----
-name: schedule-assistant
-description: 处理待办、提醒、日程类消息;先澄清缺失时间,再调用受限 schedule 工具写入真实记录。
----
-
-# 日程助手
-
-这个 skill 只处理待办、提醒、日程、行程、闹钟类需求。目标是让回复和真实数据写入一致。
-
-## 适用范围
-
-- 创建待办
-- 创建带时间的日程或事件
-- 给已有事项创建提醒
-- 查询当前用户的事项列表
-
-## 边界
-
-1. 只能处理日程相关问题,不接管页面生成、知识问答、代码任务等其他能力。
-2. 只能操作当前用户的数据,不能猜测或引用其他用户记录。
-3. 没有成功调用工具前,不能说“已经设置好了”“已经加上了”。
-4. 信息不完整时先追问,不要为了显得聪明而臆造时间。
-
-## 可用工具
-
-- `schedule_create_item`
-- `schedule_create_reminder`
-- `schedule_list_items`
-
-## 时间写入(必守)
-
-1. **禁止**自行估算 Unix 毫秒时间戳;模型算 epoch 极易出错。
-2. 创建事项时用 `startLocal` / `endLocal` / `dueLocal`,格式 **`YYYY-MM-DD HH:mm`**(用户时区下的墙上时钟)。
-3. 创建提醒时用 `remindLocal`,格式同上;不要只传 `remindAt`。
-4. 用户说「明天 / 后天」时,必须结合会话里的「当前日期」锚点推算具体日期后再写入 local 字段。
-5. 写入后可用 `schedule_list_items` 核对返回的时间是否正确。
-
-## 工作规则
-
-1. 先判断用户是在创建、查询,还是补充提醒。
-2. 如果缺标题、事项时间、提醒时间,先追问最小必要信息。
-3. 创建事项时:
- - 纯待办用 `kind: task`
- - 明确约会/会议/出发等时间安排可用 `kind: event`
-4. 需要提醒时:
- - 先创建事项
- - 再调用 `schedule_create_reminder`
- - 只有两个工具都成功,才对用户确认成功
-5. 如果提示里给出了 `sourceMessageId`,调用 `schedule_create_item` 时必须原样传入。
-6. 查询时调用 `schedule_list_items`,只按结果回答,不要编造“已经存在”。
-
-## 回复要求
-
-- 简短、直接
-- 成功时说明创建了什么,以及是否带提醒
-- 失败时明确说没写入成功,并给出下一步
diff --git a/.runtime/portal/skills/search/SKILL.md b/.runtime/portal/skills/search/SKILL.md
deleted file mode 100644
index ac954e3..0000000
--- a/.runtime/portal/skills/search/SKILL.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: search
-description: 代码搜索技能:在代码库中搜索文本、符号、文件名(基于 ripgrep)
----
-
-# 代码搜索
-
-使用内置 `search` 平台扩展在工作区内快速定位代码。
-
-## 何时使用
-
-- 用户要找某个函数/变量/类定义在哪里
-- 用户要找包含某段文字的文件
-- 用户要找符合某种命名规律的文件
-
-## 工具
-
-| 工具 | 用途 |
-|------|------|
-| `search_text` | 按文本/正则搜索,支持 glob 过滤、大小写控制 |
-| `search_files` | 按文件名通配符查找文件 |
-| `search_symbol` | 精确匹配完整单词(适合找函数/变量名) |
-
-## 规则
-
-1. 优先用 `search_symbol` 找符号,精确度更高
-2. `search_text` 的 `file_glob` 可缩小范围(如 `*.rs`、`src/**`)
-3. 结果超出 `max_results` 时提示用户缩小范围
diff --git a/.runtime/portal/skills/static-page-publish/SKILL.md b/.runtime/portal/skills/static-page-publish/SKILL.md
deleted file mode 100644
index ec3f63c..0000000
--- a/.runtime/portal/skills/static-page-publish/SKILL.md
+++ /dev/null
@@ -1,112 +0,0 @@
----
-name: static-page-publish
-description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报告与页面(TKMind H5 通用技能)
----
-
-# 静态页面 / 报告发布
-
-本技能为 **TKMind H5 多用户环境** 的通用发布流程。安装到用户工作区后,内容会按用户替换为专属目录与公网前缀。
-
-## 何时使用
-
-- 用户要「生成网页 / HTML 报告 / 可视化页面 / 分享链接」
-- 用户提到「放到 MindSpace」「给个能打开的链接」
-
-## 规则摘要
-
-0. 可以用 `apps__create_app` 设计/预览页面,但那一步只是在 Apps 窗口内生成交互式 App,**还没有公网链接**;只要用户要「可访问的链接」「分享出去」,最后必须把内容 `write_file` 落到 `public/*.html`,按下方「回复格式」给出真实链接,不要停在 App 阶段就回复链接
-1. 只在**当前用户工作区**(会话 `working_dir`)内读写与搜索,从 `.` 开始
-2. 查找 CSV/文档时只用相对路径(如 `oa/report.csv`),**禁止**去上级目录、MindSpace 根目录、其它用户目录或主机路径搜索
-3. 读 CSV/列目录:用工作区内的 `shell`(`ls oa/`、`cat file.csv`)或 `tree`;**禁止**用公网 URL 代替
-4. **禁止**用 `shell` / `cat` / `heredoc` / `echo` / `cp` 写入 `public/*.html`;HTML 必须用 `write_file` / `edit_file`(shell 在容器内执行,公网链接会 404)
-5. 公网链接**仅**用于让用户浏览器打开已发布的 HTML,不能用来列目录或读数据文件
-6. 静态文件保存即可访问,**无需重启**
-7. 默认只生成 HTML;不要在没有明确需求时强制生成 Word、PDF、长图等伴生文件
-8. 只有用户明确要求 **Word/PDF 等二进制下载** 时:文件单独落盘(如 `public/方案.docx`),链接用相对路径;**禁止**在 HTML 内用 `data:...;base64,...` 嵌入 docx(易截断损坏)
-9. 只有用户明确要求**长图下载**时:必须先 `load_skill` → `long-image-download`,调用 `generate_long_image` 生成同目录 `public/<页面名>.long.png`,再返回长图预览与下载链接;禁止把 `.thumbnail.svg` 当成长图
-
-详细约束以工作区内的 `.goosehints` 与 `.agents/skills/static-page-publish/SKILL.md` 为准。
-
-## 推荐工作流
-
-1. 确认需求(标题、章节、视觉风格、是否需要 hero 图)
-2. `write_file` 创建 `public/页面.html`(需要调整已有页面时用 `edit_file`;需要时在同目录或 `assets/` 放主图)
-3. 在 `` 写入 **mindspace-cover**(必须与页面主题一致,见下文)
-4. 保存后服务端**立即**生成 `<文件名>.thumbnail.svg`(Agent 交互阶段即生效)
-5. 按「回复格式」返回**可点击**公网链接
-6. 若用户明确要求 Word/docx 下载,必须用 `generate_docx`(sandbox-fs 工具)生成 `public/<同名>.docx`,再确认链接目标已落盘
-7. 若用户明确要求长图下载,必须用 `long-image-download` 生成 `public/<同名>.long.png`,并确认文件存在
-
-## 按需伴生下载文件
-
-- 默认不生成伴生文件;只有用户明确要求下载附件时才生成
-- `` 等相对下载链接,目标文件必须已在 HTML 同目录或子目录
-- 推荐 `public/report.html` + `public/report.docx`;**禁止** HTML 链接名与磁盘文件名不一致
-- 生成 Word 时必须调用 sandbox-fs 的 `generate_docx`;**禁止**用 `computercontroller` / shell 生成生产下载文件作为交付依据
-- 从 `oa/` 引用文档时,先 **复制** 到 `public/` 再写链接
-- 交付前 `list_dir public/` 自检;可跑 `npm run check:mindspace-public-links`
-
-## 回复格式(必须)
-
-向用户交付页面时,**必须使用 Markdown 可点击链接**:
-
-```markdown
-[马来西亚旅游攻略](https://goo.tkmind.cn/MindSpace/<用户ID>/public/malaysia-travel-guide.html)
-```
-
-要求:
-
-- **必须**使用 `[页面标题](完整URL)`,不要只给裸 URL 或「点这里」
-- 页面写入 `public/` 时,URL **必须**包含 `/public/` 路径段(与磁盘路径一致)
-- 域名严格按本节模板拼接(`https://goo.tkmind.cn/MindSpace/<用户ID>/public/...`);拿不到真实前缀时,先给相对路径 `public/xxx.html` 说明,不要自己猜一个域名
-- 标题用页面真实主题名
-- 可同时给出相对路径(如 `public/malaysia-travel-guide.html`)
-- 说明:保存即生效,无需重启
-- 若生成了长图,同时给 `[长图预览](.../public/malaysia-travel-guide.long.png)` 和 `[下载长图](.../public/malaysia-travel-guide.html?download=long-image)`
-
-## 信息流预览图(必须)
-
-每个 HTML 必须在 `` 包含与**页面主题一致**的元数据。系统据此生成 **精美的 3:4 信息流封面**(工作区 `*.thumbnail.svg` +「我的空间」卡片 + 保存弹窗预览):
-
-```html
-
-
-```
-
-| 字段 | 要求 |
-|------|------|
-| `tag` | 与主题一致:旅行 / 美食 / 报告 / **运动** / **活动** 等 |
-| `accent` / `accent2` | 页面主色,与 hero/背景 CSS 一致 |
-| `subtitle` | 一句话卖点;未写时用 description |
-| `cover` / `image` | **必须**指向高质量主图(相对 HTML 或 `https://`);见下文 |
-| `emoji` | 可选;也可写在 title 中 |
-
-### 精美预览图(必须达标)
-
-保存 HTML 后,系统会**立即**生成 `<文件名>.thumbnail.svg` 作为卡片封面。要产出**可在信息流中直接展示的精美封面**,必须:
-
-1. **视觉类页面**(旅行、美食、活动、运动、品牌、产品、促销等)**必须**在 `assets/` 放置高质量 hero 主图(建议宽度 ≥1200px),并在 `cover` 字段引用(如 `assets/hero.jpg`)
-2. **纯文字报告**可仅用配色 + tag,但仍须保证 `accent` / `subtitle` 与页面风格一致
-3. `tag`、`accent`、`accent2`、`subtitle` 必须与页面实际视觉一致;**禁止**省略 mindspace-cover 或填无关默认值
-4. 若缺少 hero 主图,封面会退化为简陋默认图,**视为未达标**
-
-**禁止**省略 mindspace-cover 或填写与页面无关的通用配色;促销/运动/品牌页必须写明 `tag`、`accent` 和 `cover`。
-
-本地对比示例:`node scripts/thumbnail-preview-demo.mjs` → `/thumbnail-demo/`
-
-## 平台页脚标记(必须)
-
-页脚平台品牌行**必须**使用 `data-mindspace-page-tag="platform-brand"`,显示为 **TKMind · 智趣**,**禁止**使用邮箱或 `tkmind.ai`:
-
-```html
-TKMind · 智趣
-```
-
-带 `data-mindspace-page-tag` 的区域为平台固定信息:用户在编辑模式中不可见、不可改;预览与发布后正常显示。
-
-## 按需附带文件下载(Word / PDF)
-
-- 只有用户明确要求 Word / PDF 下载时才生成二进制文件
-- 二进制文件用 `docx-generate` 脚本或平台允许的方式**单独生成**,保存到 `public/`(或 `oa/` 再复制到 `public/`)
-- 下载按钮示例:`下载文档`(与 HTML 同目录时用文件名即可)
-- **禁止** `` 内嵌 docx/pdf
diff --git a/.runtime/portal/skills/table-viewer/SKILL.md b/.runtime/portal/skills/table-viewer/SKILL.md
deleted file mode 100644
index 978f050..0000000
--- a/.runtime/portal/skills/table-viewer/SKILL.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-name: table-viewer
-description: 表格可视化技能:将数据渲染为可排序、筛选的交互式表格(需启用 tableviewer 扩展)
----
-
-# 表格查看器
-
-使用 `tableviewer` MCP 扩展在聊天中渲染交互式数据表格。
-
-## 何时使用
-
-- 用户展示结构化数据(数据库查询结果、CSV、对比列表)
-- 数据超过 5 行且有多列时,优先用表格而非 markdown
-
-## 前提
-
-Settings → Extensions 中启用 **Table Viewer** 扩展。
-
-## 工具
-
-`show_table(title, columns, rows)` — 渲染可排序/筛选表格
-
-## 示例
-
-```
-show_table(
- title: "销售数据",
- columns: ["月份", "销售额", "环比增长"],
- rows: [["1月", 12000, "+5%"], ["2月", 13500, "+12.5%"]]
-)
-```
-
-## 规则
-
-1. `columns` 与每行数据顺序必须一致
-2. 数值类型保持为数字(不要转成字符串),排序才准确
-3. 超过 500 行时提示用户过滤后再展示
diff --git a/.runtime/portal/skills/test-runner/SKILL.md b/.runtime/portal/skills/test-runner/SKILL.md
deleted file mode 100644
index 87c188a..0000000
--- a/.runtime/portal/skills/test-runner/SKILL.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: test-runner
-description: 测试运行技能:运行 cargo test 并解析通过/失败结果
----
-
-# 测试运行
-
-使用内置 `test_runner` 平台扩展运行项目测试并分析结果。
-
-## 何时使用
-
-- 用户要验证改动是否破坏了现有测试
-- 用户要跑某个 crate 或特定测试用例
-- 需要查看有哪些测试可运行
-
-## 工具
-
-| 工具 | 用途 |
-|------|------|
-| `run_tests` | 运行测试,可按包名和测试名过滤,支持超时 |
-| `list_tests` | 列出所有可用测试(不执行) |
-
-## 规则
-
-1. 修复 bug 后**必须**运行相关测试确认
-2. 用 `test_filter` 只跑受影响的测试,避免全量耗时
-3. 默认超时 120 秒,大型集成测试可适当调高
-4. 看到 FAILED 行后,重点关注 `failures:` 区段的具体错误
diff --git a/.runtime/portal/skills/timeline/SKILL.md b/.runtime/portal/skills/timeline/SKILL.md
deleted file mode 100644
index c6cd64e..0000000
--- a/.runtime/portal/skills/timeline/SKILL.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-name: timeline
-description: 时间轴可视化技能:将事件序列渲染为交互式时间线(需启用 timeline 扩展)
----
-
-# 时间轴展示
-
-使用 `timeline` MCP 扩展在聊天中渲染时间线。
-
-## 何时使用
-
-- 展示项目里程碑、发版历史
-- git log 可视化
-- 事件序列、历史脉络梳理
-
-## 前提
-
-Settings → Extensions 中启用 **Timeline** 扩展。
-
-## 工具
-
-`show_timeline(title, events)` — 渲染垂直时间轴
-
-## 数据结构
-
-```
-events: [
- {
- date: "2024-01-15",
- title: "v1.0 发布",
- description: "首个正式版本上线",
- category: "发版",
- color: "#5c98f9" // 可选,不填自动按 category 着色
- }
-]
-```
-
-## 规则
-
-1. `date` 格式灵活(`2024-01`、`2024-01-15`、`Q1 2024` 均可)
-2. 同一 `category` 自动使用同一颜色
-3. 事件超过 30 个时只展示关键节点,其余省略
diff --git a/.runtime/portal/skills/web/SKILL.md b/.runtime/portal/skills/web/SKILL.md
deleted file mode 100644
index 057c3c6..0000000
--- a/.runtime/portal/skills/web/SKILL.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-name: web
-description: 网页抓取与搜索技能:访问网页、查阅文档、搜索互联网
----
-
-# 网页访问
-
-使用内置 `web` 平台扩展获取网页内容和搜索信息。
-
-## 何时使用
-
-- 用户要查某个 API 文档、库的用法
-- 用户要搜索最新新闻、技术资料
-- 需要验证某个 URL 的内容
-
-## 工具
-
-| 工具 | 用途 |
-|------|------|
-| `fetch_url` | 抓取指定 URL 的内容,可提取纯文本 |
-| `web_search` | 通过 DuckDuckGo 搜索,返回标题/摘要/链接列表 |
-
-## 规则
-
-1. 优先用 `web_search` 探索,再用 `fetch_url` 读取具体页面
-2. `extract_text: true`(默认)获取可读文本,`false` 获取原始 HTML
-3. 不要访问不明来源的链接,向用户确认后再访问
-4. 官方文档优先于第三方博客
-
-## 国内网络环境(建议)
-
-- 生产环境访问不了 `google.com`,优先用本技能的 `web_search`(DuckDuckGo)/`fetch_url`,避免直接拿 `computercontroller__web_scrape` 抓 Google 页面来回重试
-- `web_search` 连续几次没有可用结果时,改用 `fetch_url` 直接访问 `https://cn.bing.com/search?q=...` 或 `https://www.so.com/s?q=...` 这类国内可达的搜索入口
-- 百度/知乎/大众点评等站点有反爬拦截,遇到跳转或空结果就换个搜索源,不必在同一个来源上反复硬抓
diff --git a/release-gate/release-script.test.mjs b/release-gate/release-script.test.mjs
index 8b7c287..8c7c36c 100644
--- a/release-gate/release-script.test.mjs
+++ b/release-gate/release-script.test.mjs
@@ -353,11 +353,10 @@ test('canary release and rollback remote shells remain syntactically valid', asy
assertShellParses(edgeRollback, '105 edge rollback shell');
});
-test('release readiness ignores only the generated canary artifact cover path', async () => {
+test('release readiness no longer needs a legacy tracked runtime exception', async () => {
const source = await fs.readFile(
path.join(ROOT, 'scripts', 'check-release-ready.sh'),
'utf8',
);
- assert.match(source, /:\(exclude\)\.runtime\/portal\/public\/plaza-covers\/\*\*/);
- assert.doesNotMatch(source, /:\(exclude\)\.runtime\/\*\*/);
+ assert.doesNotMatch(source, /:\(exclude\)\.runtime\//);
});
diff --git a/release-gate/runner.mjs b/release-gate/runner.mjs
index 882c6db..bd47965 100644
--- a/release-gate/runner.mjs
+++ b/release-gate/runner.mjs
@@ -113,8 +113,7 @@ export function filterGeneratedWorktreeStatus(status) {
.filter((line) => {
if (!line) return false;
const path = line.slice(3).replace(/^"|"$/g, '');
- return !path.startsWith('.release-gate/')
- && !path.startsWith('.runtime/portal/public/plaza-covers/');
+ return !path.startsWith('.release-gate/');
})
.join('\n');
}
diff --git a/release-gate/runner.test.mjs b/release-gate/runner.test.mjs
index 6bd5009..40579ae 100644
--- a/release-gate/runner.test.mjs
+++ b/release-gate/runner.test.mjs
@@ -58,11 +58,17 @@ test('exclusive suites finish before parallel suites start', async () => {
assert.ok(events.indexOf('end:mutating') < events.indexOf('start:parallel-b'));
});
-test('worktree cleanliness ignores only release-gate and generated plaza cover paths', () => {
+test('worktree cleanliness ignores release-gate output but keeps runtime changes', () => {
const status = [
'?? .release-gate/ea07f6f/report.json',
' D .runtime/portal/public/plaza-covers/cover.jpg',
' M release-gate/runner.mjs',
].join('\n');
- assert.equal(filterGeneratedWorktreeStatus(status), ' M release-gate/runner.mjs');
+ assert.equal(
+ filterGeneratedWorktreeStatus(status),
+ [
+ ' D .runtime/portal/public/plaza-covers/cover.jpg',
+ ' M release-gate/runner.mjs',
+ ].join('\n'),
+ );
});
diff --git a/scripts/check-release-ready.sh b/scripts/check-release-ready.sh
index dcfd4e5..647e566 100755
--- a/scripts/check-release-ready.sh
+++ b/scripts/check-release-ready.sh
@@ -53,12 +53,8 @@ case "${branch}" in
;;
esac
-# The runtime builder intentionally strips this legacy tracked content from the
-# source-free artifact. The complete Gate applies the same narrow exception.
relevant_status="$(
- git -C "${ROOT}" status --porcelain=v1 --untracked-files=all -- \
- . \
- ':(exclude).runtime/portal/public/plaza-covers/**'
+ git -C "${ROOT}" status --porcelain=v1 --untracked-files=all
)"
if [[ -n "${relevant_status}" ]]; then
echo "Release check failed: worktree has uncommitted or untracked changes." >&2