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 智趣」小图标应同时出现。

-
-
-

连云港三天两夜攻略 🌊

-

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

-
- - TKMind 智趣 -
-
- -
-
-
og:title 连云港三天两夜攻略 🌊
-
og:description 三天两夜·沙滩酒店·必吃美食
-
og:site_name TKMind 智趣
-
og:image https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=200&q=80
-
icon /brand/tkmind-icon.png
-
-
- - \ No newline at end of file diff --git a/.runtime/portal/public/dev/wechat-share-preview.html b/.runtime/portal/public/dev/wechat-share-preview.html 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 旅行攻略 - 马来西亚旅游攻略 - - 马来西亚深度游 - TKMIND - \ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 旅行攻略 - 马来西亚旅游攻略 - - 马来西亚深度游 - TKMIND - \ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 页面 - 马来西亚旅游攻略 - - 马来西亚深度游 - TKMIND - \ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 美食专题 - 麻婆豆腐 - - Mapo Tofu — - TKMIND - \ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 美食专题 - 麻婆豆腐 - - Mapo Tofu — - TKMIND - \ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 页面 - 麻婆豆腐 - - Mapo Tofu — - TKMIND - \ 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}: \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}: \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 ` - - - - - - - - - - - - - - - - - - - - - - - ${overlayStops} - - - - - - - - - - - - - - - - - - - - - - ${photoLayer} - ${scenicLayers} - - - - ${tag.toUpperCase()} - ${line1} - ${line2 ? `${line2}` : ""} - ${subtitle} - TKMIND -`; -} -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 = [ - / buffer.length) break; - if (compressionMethod !== 0 && compressedSize > 0 && uncompressedSize / compressedSize > MAX_ZIP_COMPRESSION_RATIO) { - return { - scanStatus: "blocked", - riskLevel: "critical", - findings: ["zip_bomb_ratio"] - }; - } - headers += 1; - offset += headerSize; - if (compressedSize > 0) { - offset += compressedSize; - } - } - return null; -} -function runBasicFileScan(buffer, { filename, mimeType }) { - if (!Buffer.isBuffer(buffer) || buffer.length === 0) { - return { - scanStatus: "blocked", - riskLevel: "high", - findings: ["empty_file"] - }; - } - if (mimeType.startsWith("text/") || mimeType === "application/pdf") { - const textFinding = scanTextContent(buffer); - if (textFinding) return textFinding; - } - if (mimeType === "application/pdf" && buffer.subarray(0, 5).toString("ascii") !== "%PDF-") { - return { - scanStatus: "blocked", - riskLevel: "medium", - findings: ["pdf_signature_mismatch"] - }; - } - if (isZipContainer(mimeType)) { - const zipFinding = scanZipStructure(buffer); - if (zipFinding) return zipFinding; - } - return { - scanStatus: "passed", - riskLevel: "none", - findings: [] - }; -} - -// mindspace-attachment-text.mjs -import zlib from "node:zlib"; -function escapeXml2(text) { - return String(text ?? "").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'"); -} -function extractZipEntry(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 zlib.inflateRawSync(compressed); - return null; - } - offset = dataStart + compressedSize; - } - return null; -} -function extractXmlText(xml) { - const paragraphs = []; - for (const block of String(xml ?? "").split("")) { - const texts = [...block.matchAll(/]*>([\s\S]*?)<\/w:t>/g)].map( - (match) => escapeXml2(match[1]) - ); - const line = texts.join("").replace(/\s+/g, " ").trim(); - if (line) paragraphs.push(line); - } - return paragraphs.join("\n\n"); -} -function extractDocxText(buffer) { - const xmlBuffer = extractZipEntry(buffer, "word/document.xml"); - return xmlBuffer ? extractXmlText(xmlBuffer.toString("utf8")) : ""; -} -function extractXlsxText(buffer) { - const sharedStringsXml = extractZipEntry(buffer, "xl/sharedStrings.xml"); - const sharedStrings = sharedStringsXml ? [...sharedStringsXml.toString("utf8").matchAll(/]*>([\s\S]*?)<\/t>/g)].map( - (match) => escapeXml2(match[1]) - ) : []; - const sheetEntries = []; - let index = 1; - while (true) { - const sheet = extractZipEntry(buffer, `xl/worksheets/sheet${index}.xml`); - if (!sheet) break; - const rows = []; - for (const rowBlock of sheet.toString("utf8").split("")) { - const cells = []; - for (const cell of rowBlock.matchAll(/]*?(?:t="([^"]+)")?[^>]*>(?:[\s\S]*?([\s\S]*?)<\/v>)?/g)) { - const type = cell[1] ?? ""; - const value = cell[2] ?? ""; - if (type === "s") { - const sharedIndex = Number(value); - cells.push(sharedStrings[sharedIndex] ?? value); - } else { - cells.push(escapeXml2(value)); - } - } - if (cells.length) rows.push(cells.join(" ")); - } - if (rows.length) { - sheetEntries.push(`Sheet ${index} -${rows.join("\n")}`); - } - index += 1; - } - return sheetEntries.join("\n\n"); -} -function extractPptxText(buffer) { - const slides = []; - let index = 1; - while (true) { - const slide = extractZipEntry(buffer, `ppt/slides/slide${index}.xml`); - if (!slide) break; - const texts = [...slide.toString("utf8").matchAll(/]*>([\s\S]*?)<\/a:t>/g)].map( - (match) => escapeXml2(match[1]) - ); - const text = texts.join(" ").replace(/\s+/g, " ").trim(); - if (text) slides.push(`Slide ${index} -${text}`); - index += 1; - } - return slides.join("\n\n"); -} -function extractPdfText(buffer) { - const text = buffer.toString("latin1"); - const segments = []; - for (const streamMatch of text.matchAll(/stream\r?\n([\s\S]*?)\r?\nendstream/g)) { - const stream = streamMatch[1]; - for (const textMatch of stream.matchAll(/\(([^()\\]*(?:\\.[^()\\]*)*)\)\s*Tj/g)) { - const raw = textMatch[1].replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\\(/g, "(").replace(/\\\)/g, ")"); - const cleaned = raw.replace(/\s+/g, " ").trim(); - if (cleaned) segments.push(cleaned); - } - } - return segments.join("\n"); -} -function extractAttachmentText(buffer, mimeType, filename = "") { - const normalizedMimeType = String(mimeType ?? ""); - const extension = String(filename ?? "").toLowerCase(); - if (normalizedMimeType === "application/pdf") { - const text = extractPdfText(buffer); - return { text, format: "pdf", warnings: text ? [] : ["pdf_text_not_detected"] }; - } - if (normalizedMimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || extension.endsWith(".docx")) { - const text = extractDocxText(buffer); - return { text, format: "docx", warnings: text ? [] : ["docx_text_not_detected"] }; - } - if (normalizedMimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || extension.endsWith(".xlsx")) { - const text = extractXlsxText(buffer); - return { text, format: "xlsx", warnings: text ? [] : ["xlsx_text_not_detected"] }; - } - if (normalizedMimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation" || extension.endsWith(".pptx")) { - const text = extractPptxText(buffer); - return { text, format: "pptx", warnings: text ? [] : ["pptx_text_not_detected"] }; - } - if (normalizedMimeType.startsWith("text/")) { - return { text: buffer.toString("utf8"), format: "text", warnings: [] }; - } - return { text: "", format: "unsupported", warnings: ["unsupported_attachment_type"] }; -} - -// user-image-url.mjs -import path14 from "node:path"; -var PUBLIC_IMAGES_DIR = "images"; -var cachedSigner = null; -var signerInitAttempted = false; -function resolveImgproxyBaseUrl(env = process.env) { - return (env.IMGPROXY_BASE_URL ?? "http://localhost:20081").replace(/\/$/, ""); -} -function getImgproxySigner(env = process.env) { - if (signerInitAttempted) return cachedSigner; - signerInitAttempted = true; - const key = env.IMGPROXY_SIGNING_KEY?.trim(); - const salt = env.IMGPROXY_SIGNING_SALT?.trim(); - if (!key || !salt) return null; - try { - cachedSigner = createImgproxySigner(key, salt); - } catch { - cachedSigner = null; - } - return cachedSigner; -} -function formatImageDateFolder(date = /* @__PURE__ */ new Date()) { - const value = date instanceof Date ? date : new Date(date); - const year = value.getFullYear(); - const month = String(value.getMonth() + 1).padStart(2, "0"); - const day = String(value.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; -} -function extensionForMime(mimeType, fallbackFilename = "") { - const ext = path14.extname(String(fallbackFilename)).toLowerCase(); - if (ext && /^\.[a-z0-9]{1,8}$/.test(ext)) return ext === ".jpeg" ? ".jpg" : ext; - if (mimeType === "image/jpeg") return ".jpg"; - if (mimeType === "image/png") return ".png"; - if (mimeType === "image/webp") return ".webp"; - if (mimeType === "image/gif") return ".gif"; - return ".img"; -} -function sanitizeImageBasename(filename, assetId) { - const raw = path14.basename(String(filename ?? ""), path14.extname(String(filename ?? ""))).trim(); - const normalized = raw.normalize("NFKC").replace(/[^\w\u4e00-\u9fff.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80); - return normalized || assetId.slice(0, 8); -} -function buildPublicImagePaths({ - userId, - assetId, - mimeType, - originalFilename, - date = /* @__PURE__ */ new Date(), - uniqueSuffix = null -}) { - const dateFolder = formatImageDateFolder(date); - const extension = extensionForMime(mimeType, originalFilename); - const basename = sanitizeImageBasename(originalFilename, assetId); - const suffix = uniqueSuffix ? `-${uniqueSuffix}` : ""; - const filename = `${basename}${suffix}${extension}`; - const storageKey = path14.posix.join("users", userId, PUBLIC_IMAGES_DIR, dateFolder, filename); - const workspaceRelativePath = path14.posix.join("public", PUBLIC_IMAGES_DIR, dateFolder, filename); - return { storageKey, workspaceRelativePath, filename, dateFolder }; -} -function buildImgproxyDisplayUrl(storageKey, options = {}) { - const baseUrl = (options.baseUrl ?? resolveImgproxyBaseUrl()).replace(/\/$/, ""); - const signer = options.signer ?? getImgproxySigner(); - if (signer) { - return signer.buildUrl(baseUrl, storageKey, options.preset ?? "display"); - } - const preset = options.preset ?? "display"; - if (preset === "vision") { - return `${baseUrl}/unsafe/rs:fit:768:768:0/q:60/plain/local:///${storageKey}@jpg`; - } - if (preset === "thumb") { - return `${baseUrl}/unsafe/rs:fit:256:256:0/q:70/plain/local:///${storageKey}@jpg`; - } - return `${baseUrl}/unsafe/rs:fit:1280:1280:0/q:85/plain/local:///${storageKey}@jpg`; -} -function buildUserImagePublicUrl({ userId, storageKey, mimeType, originalFilename }) { - void userId; - void mimeType; - void originalFilename; - return buildImgproxyDisplayUrl(storageKey); -} -function isPublicImageStorageKey(storageKey) { - const normalized = String(storageKey ?? "").replace(/\\/g, "/"); - return /\/images\/\d{4}-\d{2}-\d{2}\//.test(normalized); -} - -// mindspace-asset-preview.mjs -import zlib2 from "node:zlib"; -var PREVIEWABLE_MIME_TYPES = /* @__PURE__ */ new Set([ - "text/html", - "text/plain", - "text/markdown", - "text/csv", - "application/pdf", - "image/png", - "image/jpeg", - "image/webp", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" -]); -var PREVIEW_SHELL_STYLE = ` -html,body{margin:0;padding:0;background:#f5f0e5;color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} -body{padding:24px;box-sizing:border-box;line-height:1.6} -pre,code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace} -pre{white-space:pre-wrap;word-break:break-word;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px} -table{border-collapse:collapse;width:100%;background:#fff;border-radius:12px;overflow:hidden} -th,td{border:1px solid #e5e7eb;padding:8px 10px;text-align:left;font-size:14px} -th{background:#faf7ef} -.docx-preview h1{font-size:1.5rem;margin:0 0 16px} -.docx-preview p{margin:0 0 12px;text-indent:2em} -.docx-preview .meta{color:#6b7280;font-size:13px;margin-bottom:20px;text-indent:0} -.pdf-frame,.image-frame{display:block;width:100%;min-height:calc(100vh - 48px);border:0;border-radius:12px;background:#fff} -.image-frame{object-fit:contain;max-height:calc(100vh - 48px);width:auto;max-width:100%;margin:0 auto;cursor:zoom-in} -.image-viewer{padding:0} -.image-viewer h1,.image-viewer .meta{display:none} -.image-viewer .image-frame{min-height:100vh;max-height:100vh;margin:0;border-radius:0;background:#0b100e} -.image-lightbox[hidden]{display:none} -.image-lightbox{position:fixed;inset:0;z-index:9999;display:grid;place-items:center;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));background:rgba(7,12,10,.9);cursor:zoom-out} -.image-lightbox img{display:block;max-width:min(100%,1600px);max-height:calc(100vh - 32px);object-fit:contain;border-radius:8px;box-shadow:0 24px 80px rgba(0,0,0,.35);cursor:default} -.image-lightbox-close{position:fixed;top:max(16px,env(safe-area-inset-top));right:max(16px,env(safe-area-inset-right));z-index:10000;display:grid;place-items:center;width:40px;height:40px;padding:0;border:0;border-radius:999px;color:#fffaf0;background:rgba(24,33,29,.72);box-shadow:0 8px 24px rgba(0,0,0,.28);cursor:pointer} -.image-lightbox-close svg{display:block;width:18px;height:18px;stroke:currentColor;stroke-width:2;fill:none} -.image-lightbox-close:hover{background:rgba(24,33,29,.92)} -`; -var IMAGE_LIGHTBOX_CLOSE_ICON = ''; -var IMAGE_LIGHTBOX_SCRIPT = ``; -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 `${safeTitle}${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 `
${tail}${header}${content}
`; -} -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: //gi, - mask: () => "", - 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`, - replacement: () => "" - }, - { - type: "html_form_action", - label: "\u8868\u5355\u63D0\u4EA4", - riskLevel: "high", - blocking: true, - pattern: /[\s\S]*?<\/form>/gi, - mask: () => "
\u2026
", - 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}`; - }); -} -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(//gi, " ").replace(//gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 180) : content.replace(/\s+/g, " ").slice(0, 180); - return { - title, - summary: summary || plainSummary, - content, - contentBytes, - templateId, - contentFormat, - pageType: contentFormat === "html" ? "html" : input.pageType || "article", - categoryCode: SAVE_CATEGORY_CODES.has(input.categoryCode) ? input.categoryCode : "draft" - }; -} -function extensionForMime2(mimeType, filename = "") { - const ext = path21.extname(String(filename ?? "")).replace(/^\./, "").toLowerCase(); - if (ext && /^[a-z0-9]{1,8}$/.test(ext)) return ext === "jpeg" ? "jpg" : ext; - if (mimeType === "image/jpeg") return "jpg"; - if (mimeType === "image/png") return "png"; - if (mimeType === "image/webp") return "webp"; - if (mimeType === "image/gif") return "gif"; - return "bin"; -} -function pageResponse(row, { includeContent = true } = {}) { - const response = { - id: row.id, - categoryId: row.category_id, - categoryCode: row.category_code, - sourceSessionId: row.source_session_id, - sourceMessageId: row.source_message_id, - sourceAssetId: row.source_asset_id, - title: row.title, - summary: row.summary ?? "", - pageType: row.page_type, - contentFormat: row.page_type === "html" ? "html" : "markdown", - hasThumbnail: row.page_type === "html", - templateId: row.template_id, - status: row.status, - visibility: row.visibility, - publicationAccessMode: row.pub_access_mode ?? null, - publicationUrl: row.pub_public_url ?? null, - currentVersionId: row.current_version_id, - versionNo: asNumber4(row.version_no), - createdAt: asNumber4(row.created_at), - updatedAt: asNumber4(row.updated_at) - }; - if (includeContent) { - response.content = row.content; - } - return response; -} -var LIST_PAGES_MAX_LIMIT = 100; -function normalizeListPageFilters(filters = {}) { - const limit = Math.min(Math.max(Number(filters.limit) || LIST_PAGES_MAX_LIMIT, 1), LIST_PAGES_MAX_LIMIT); - const offset = Math.max(Number(filters.offset) || 0, 0); - return { limit, offset }; -} -function escapeHtml2(value) { - return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); -} -function renderContent(content) { - return content.split(/\n{2,}/).map((block) => { - const trimmed = block.trim(); - if (!trimmed) return ""; - if (trimmed.startsWith("### ")) return `

${escapeHtml2(trimmed.slice(4))}

`; - if (trimmed.startsWith("## ")) return `

${escapeHtml2(trimmed.slice(3))}

`; - if (trimmed.startsWith("# ")) return `

${escapeHtml2(trimmed.slice(2))}

`; - return `

${escapeHtml2(trimmed).replaceAll("\n", "
")}

`; - }).join("\n"); -} -function renderPreviewHtml(page) { - const templateClass = TEMPLATE_IDS.has(page.templateId) ? page.templateId : "editorial"; - return ` - - - - - - ${escapeHtml2(page.title)} - - - -
-
MINDSPACE DRAFT \xB7 V${page.versionNo}
-

${escapeHtml2(page.title)}

-
${escapeHtml2(page.summary)}
-
${renderContent(page.content)}
-
- -`; -} -function renderHtmlPreview(html) { - const csp = ``; - const previewShell = ''; - const source = String(html); - if (/]*>/i.test(source)) { - return source.replace(/]*)>/i, `${csp}${previewShell}`); - } - if (/]*>/i.test(source)) { - return source.replace(/]*)>/i, `${csp}${previewShell}`); - } - return `${csp}${previewShell}${source}`; -} -function previewContentSecurityPolicy(contentFormat) { - if (contentFormat === "html") { - return "default-src 'none'; style-src 'unsafe-inline' https:; img-src data: https:; font-src https: data:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'; script-src 'none'"; - } - return "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"; -} -function isHtmlPage(page) { - return page.pageType === "html" || page.page_type === "html" || page.contentFormat === "html"; -} -function renderPublicationHtml(page) { - if (isHtmlPage(page)) { - return renderHtmlPreview(page.content); - } - return renderPreviewHtml({ - title: page.title, - summary: page.summary ?? "", - templateId: page.template_id, - versionNo: page.version_no, - content: page.content - }).replace( - `MINDSPACE DRAFT \xB7 V${page.version_no}`, - `MINDSPACE \xB7 V${page.version_no}` - ); -} -function createPageService(pool2, options = {}) { - const storageRoot = path21.resolve(options.storageRoot ?? path21.join(process.cwd(), "data", "mindspace")); - const h5Root = options.h5Root ? path21.resolve(options.h5Root) : null; - const idFactory = options.idFactory ?? (() => crypto12.randomUUID()); - const conversationPackageRegistry = options.conversationPackageRegistry ?? null; - const resolveWorkspacePublishDir = async (userId) => { - if (!h5Root) return null; - const [rows] = await pool2.query( - `SELECT username FROM h5_users WHERE id = ? LIMIT 1`, - [userId] - ); - const username = rows[0]?.username; - if (!username) return null; - return resolvePublishDir(h5Root, { id: userId }); - }; - const absoluteStoragePath2 = (storageKey) => { - const resolved = path21.resolve(storageRoot, storageKey); - if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path21.sep}`)) { - throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C"); - } - return resolved; - }; - 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 fs17.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 writeVersionContent = async (userId, assetId, versionId, content) => { - const storageKey = path21.posix.join( - "users", - userId, - "pages", - assetId, - "versions", - `${versionId}.md` - ); - const target = absoluteStoragePath2(storageKey); - await fs17.mkdir(path21.dirname(target), { recursive: true }); - await fs17.writeFile(target, content, { flag: "wx" }); - return { storageKey, target }; - }; - const createVersion = async (userId, input, source = {}) => { - let pageInput = input; - if (input.pageId && input.contentFormat == null) { - const [existingPages] = await pool2.query( - `SELECT page_type FROM h5_page_records WHERE id = ? AND user_id = ? AND status <> 'deleted' LIMIT 1`, - [input.pageId, userId] - ); - if (existingPages[0]?.page_type === "html") { - pageInput = { ...input, contentFormat: "html" }; - } - } - const normalized = normalizePageInput(pageInput); - const conn = await pool2.getConnection(); - let writtenPath; - try { - await conn.beginTransaction(); - const [spaces] = await conn.query( - `SELECT id, quota_bytes, used_bytes, reserved_bytes, status - FROM h5_user_spaces WHERE user_id = ? LIMIT 1 FOR UPDATE`, - [userId] - ); - const space = spaces[0]; - if (!space || space.status !== "active") { - throw pageError("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u7528", "space_unavailable"); - } - const available = asNumber4(space.quota_bytes) - asNumber4(space.used_bytes) - asNumber4(space.reserved_bytes); - if (available < normalized.contentBytes) { - throw pageError("\u5269\u4F59\u7A7A\u95F4\u4E0D\u8DB3", "quota_exceeded", { - requiredBytes: normalized.contentBytes, - availableBytes: Math.max(0, available) - }); - } - const [categories] = await conn.query( - `SELECT id, category_code FROM h5_space_categories - WHERE space_id = ? AND user_id = ? AND category_code = ? - LIMIT 1 FOR UPDATE`, - [space.id, userId, normalized.categoryCode] - ); - const category = categories[0]; - if (!category) throw pageError("\u76EE\u6807\u5206\u7C7B\u4E0D\u5B58\u5728", "category_not_found"); - if (normalized.categoryCode !== "draft") { - throw pageError("\u9875\u9762\u8BB0\u5F55\u53EA\u80FD\u4FDD\u5B58\u5728\u9875\u9762\u8349\u7A3F\u533A", "category_not_pageable"); - } - let pageId = input.pageId; - let nextVersionNo = 1; - if (pageId) { - const [pages] = await conn.query( - `SELECT p.id, p.current_version_id, pv.version_no - FROM h5_page_records p - LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id - WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted' - LIMIT 1 FOR UPDATE`, - [pageId, userId] - ); - const page2 = pages[0]; - if (!page2) throw pageError("\u9875\u9762\u4E0D\u5B58\u5728", "page_not_found"); - if (asNumber4(input.expectedVersion) !== asNumber4(page2.version_no)) { - throw pageError("\u9875\u9762\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "version_conflict", { - currentVersion: asNumber4(page2.version_no) - }); - } - nextVersionNo = asNumber4(page2.version_no) + 1; - } else { - pageId = idFactory(); - } - const contentAssetId = idFactory(); - const assetVersionId = idFactory(); - const pageVersionId = idFactory(); - const checksum = crypto12.createHash("sha256").update(normalized.content).digest("hex"); - const stored = await writeVersionContent( - userId, - contentAssetId, - assetVersionId, - normalized.content - ); - writtenPath = stored.target; - const now = source.createdAt != null && Number.isFinite(Number(source.createdAt)) ? Math.floor(Number(source.createdAt)) : Date.now(); - const contentAssetType = normalized.contentFormat === "html" ? "html" : "markdown"; - const contentMimeType = normalized.contentFormat === "html" ? "text/html" : "text/markdown"; - const contentExtension = normalized.contentFormat === "html" ? "html" : "md"; - 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'none', - 'private', 'ready', ?, ?, ?)`, - [ - contentAssetId, - userId, - space.id, - category.id, - contentAssetType, - contentMimeType, - `${pageId}-v${nextVersionNo}.${contentExtension}`, - `${normalized.title} \xB7 v${nextVersionNo}`, - assetVersionId, - normalized.contentBytes, - checksum, - source.type ?? "generated", - 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, ?, ?, ?, ?, ?, ?, 'pending', ?)`, - [ - assetVersionId, - contentAssetId, - stored.storageKey, - normalized.contentBytes, - checksum, - contentMimeType, - userId, - input.changeNote ?? "\u4FDD\u5B58\u9875\u9762\u8349\u7A3F", - now - ] - ); - if (nextVersionNo === 1) { - await conn.query( - `INSERT INTO h5_page_records - (id, user_id, space_id, category_id, source_session_id, source_message_id, - source_asset_id, title, summary, page_type, template_id, draft_content_ref, - current_version_id, status, visibility, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft', 'private', ?, ?)`, - [ - pageId, - userId, - space.id, - category.id, - source.sessionId ?? null, - source.messageId ?? null, - source.assetId ?? null, - normalized.title, - normalized.summary, - normalized.pageType, - normalized.templateId, - stored.storageKey, - pageVersionId, - now, - now - ] - ); - } - await conn.query( - `INSERT INTO h5_page_versions - (id, page_id, version_no, content_asset_id, source_snapshot_json, - created_by, change_note, immutable, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`, - [ - pageVersionId, - pageId, - nextVersionNo, - contentAssetId, - JSON.stringify(source.snapshot ?? {}), - userId, - input.changeNote ?? "\u4FDD\u5B58\u9875\u9762\u8349\u7A3F", - now - ] - ); - if (nextVersionNo > 1) { - await conn.query( - `UPDATE h5_page_records - SET title = ?, summary = ?, page_type = ?, template_id = ?, - draft_content_ref = ?, current_version_id = ?, updated_at = ? - WHERE id = ? AND user_id = ?`, - [ - normalized.title, - normalized.summary, - normalized.pageType, - normalized.templateId, - stored.storageKey, - pageVersionId, - now, - pageId, - userId - ] - ); - } - await conn.query( - `UPDATE h5_user_spaces SET used_bytes = used_bytes + ?, updated_at = ? - WHERE id = ? AND user_id = ?`, - [normalized.contentBytes, now, space.id, userId] - ); - await conn.commit(); - if (normalized.contentFormat === "html") { - const workspaceHtmlRelativePath = source.snapshot?.relative_path ?? null; - const workspacePublishDir = workspaceHtmlRelativePath ? await resolveWorkspacePublishDir(userId) : null; - if (workspacePublishDir && workspaceHtmlRelativePath) { - await ensureWorkspaceHtmlThumbnail( - workspacePublishDir, - workspaceHtmlRelativePath, - normalized.content, - { - title: normalized.title, - subtitle: normalized.summary || `\u8349\u7A3F v${nextVersionNo}`, - contentStorageKey: stored.storageKey - } - ).catch(() => { - }); - } - const thumbMeta = { - title: normalized.title, - subtitle: normalized.summary || `\u8349\u7A3F v${nextVersionNo}`, - contentStorageKey: stored.storageKey - }; - queueMicrotask(() => { - void ensurePageThumbnail({ - storageRoot, - pageThumbnailStorageKey: pageThumbnailKey(userId, pageId), - html: normalized.content, - meta: thumbMeta, - workspacePublishDir, - workspaceHtmlRelativePath - }).catch(() => { - }); - }); - } - const page = await getPage(userId, pageId); - await registerPageArtifactForConversation({ - registry: conversationPackageRegistry, - userId, - source, - page, - contentAssetId, - contentMimeType, - contentBytes: normalized.contentBytes, - storageKey: stored.storageKey, - versionNo: nextVersionNo, - now - }); - return page; - } catch (error) { - await conn.rollback(); - if (writtenPath) await fs17.rm(writtenPath, { force: true }).catch(() => { - }); - throw error; - } finally { - conn.release(); - } - }; - const findPageBySourceAsset = async (userId, assetId) => { - if (!assetId) return null; - const [rows] = await pool2.query( - `SELECT p.*, c.category_code, pv.version_no, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url - FROM h5_page_records p - JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id - LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id - LEFT JOIN h5_publish_records pr ON pr.id = p.current_publish_id AND pr.status = 'online' - WHERE p.user_id = ? AND p.source_asset_id = ? AND p.status <> 'deleted' - ORDER BY p.updated_at DESC - LIMIT 1`, - [userId, assetId] - ); - return rows[0] ? pageResponse(rows[0]) : null; - }; - const findPageBySourceMessage = async (userId, sessionId, messageId2) => { - if (!sessionId || !messageId2) return null; - const [rows] = await pool2.query( - `SELECT p.*, c.category_code, pv.version_no, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url - FROM h5_page_records p - JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id - LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id - LEFT JOIN h5_publish_records pr ON pr.id = p.current_publish_id AND pr.status = 'online' - WHERE p.user_id = ? AND p.source_session_id = ? AND p.source_message_id = ? AND p.status <> 'deleted' - ORDER BY p.updated_at DESC - LIMIT 1`, - [userId, sessionId, messageId2] - ); - return rows[0] ? pageResponse(rows[0]) : null; - }; - const findPageByRelativePath = async (userId, relativePath) => { - const normalized = normalizeWorkspaceRelativePath2(relativePath); - if (!normalized) return null; - const [rows] = await pool2.query( - `SELECT p.*, c.category_code, pv.version_no, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url - FROM h5_page_records p - JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id - JOIN h5_page_versions pv ON pv.id = p.current_version_id - LEFT JOIN h5_publish_records pr ON pr.id = p.current_publish_id AND pr.status = 'online' - WHERE p.user_id = ? - AND p.status <> 'deleted' - AND JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) = ? - ORDER BY p.updated_at DESC, p.id DESC - LIMIT 1`, - [userId, normalized] - ); - return rows[0] ? pageResponse(rows[0]) : null; - }; - const listPages = async (userId, filters = {}) => { - const clauses = [`p.user_id = ?`, `p.status <> 'deleted'`]; - const params = [userId]; - if (filters.status) { - clauses.push(`p.status = ?`); - params.push(filters.status); - } - if (filters.categoryCode) { - clauses.push(`c.category_code = ?`); - params.push(filters.categoryCode); - } - const where = clauses.join(" AND "); - const { limit, offset } = normalizeListPageFilters(filters); - const baseFrom = `FROM h5_page_records p - JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id - LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id - LEFT JOIN h5_publish_records pr ON pr.id = p.current_publish_id AND pr.status = 'online'`; - const [[countRows], [rows]] = await Promise.all([ - pool2.query(`SELECT COUNT(*) AS total ${baseFrom} WHERE ${where}`, params), - pool2.query( - `SELECT p.*, c.category_code, pv.version_no, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url - ${baseFrom} - WHERE ${where} - ORDER BY p.created_at DESC, p.updated_at DESC - LIMIT ? OFFSET ?`, - [...params, limit, offset] - ) - ]); - const total = asNumber4(countRows[0]?.total); - return { - items: rows.map((row) => pageResponse(row, { includeContent: false })), - total, - limit, - offset, - hasMore: offset + rows.length < total - }; - }; - async function getPage(userId, pageId) { - const [rows] = await pool2.query( - `SELECT p.*, c.category_code, pv.version_no, av.storage_key - FROM h5_page_records p - JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id - JOIN h5_page_versions pv ON pv.id = p.current_version_id - JOIN h5_asset_versions av ON av.asset_id = pv.content_asset_id AND av.version_no = 1 - WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted' - LIMIT 1`, - [pageId, userId] - ); - const row = rows[0]; - if (!row) throw pageError("\u9875\u9762\u4E0D\u5B58\u5728", "page_not_found"); - const storagePath = await resolveReadableStoragePath(row.storage_key); - const content = await fs17.readFile(storagePath, "utf8"); - return pageResponse({ ...row, content }); - } - const listVersions = async (userId, pageId) => { - await getPage(userId, pageId); - const [rows] = await pool2.query( - `SELECT pv.id, pv.version_no, pv.change_note, pv.immutable, pv.created_at - FROM h5_page_versions pv - JOIN h5_page_records p ON p.id = pv.page_id - WHERE pv.page_id = ? AND p.user_id = ? - ORDER BY pv.version_no DESC`, - [pageId, userId] - ); - return rows.map((row) => ({ - id: row.id, - versionNo: asNumber4(row.version_no), - changeNote: row.change_note, - immutable: Boolean(row.immutable), - createdAt: asNumber4(row.created_at) - })); - }; - const redactPage = async (userId, pageId, input = {}) => { - const page = await getPage(userId, pageId); - if (input.pageVersionId && input.pageVersionId !== page.currentVersionId) { - throw pageError("\u53EA\u80FD\u57FA\u4E8E\u5F53\u524D\u7248\u672C\u4FEE\u590D\u9875\u9762", "version_conflict", { - currentVersion: page.currentVersionId - }); - } - if (input.expectedVersion != null && asNumber4(input.expectedVersion) !== asNumber4(page.versionNo)) { - throw pageError("\u9875\u9762\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "version_conflict", { - currentVersion: asNumber4(page.versionNo) - }); - } - const contentFormat = page.contentFormat === "html" ? "html" : "markdown"; - const scanFormat = contentFormat === "html" ? "html" : "text"; - const sourceTitle = input.title ?? page.title; - const sourceSummary = input.summary ?? page.summary ?? ""; - const sourceContent = input.content ?? page.content ?? ""; - const titleRedaction = redactContent(sourceTitle, { format: "text" }); - const summaryRedaction = redactContent(sourceSummary, { format: "text" }); - const contentRedaction = redactContent(sourceContent, { format: scanFormat }); - const fieldChanges = (field, fieldLabel, redaction) => redaction.findings.map((finding) => ({ - field, - fieldLabel, - type: finding.type, - label: finding.label, - count: finding.occurrenceCount - })); - const changes = [ - ...fieldChanges("title", "\u6807\u9898", titleRedaction), - ...fieldChanges("summary", "\u6458\u8981", summaryRedaction), - ...fieldChanges("content", "\u6B63\u6587", contentRedaction) - ]; - const redactionsApplied = changes.reduce((total, change) => total + change.count, 0); - if (redactionsApplied === 0) { - throw pageError("\u672A\u53D1\u73B0\u9700\u8981\u4FEE\u590D\u7684\u98CE\u9669\u5185\u5BB9", "redaction_not_needed"); - } - const updated = await createVersion( - userId, - { - pageId, - expectedVersion: page.versionNo, - title: titleRedaction.content, - summary: summaryRedaction.content.trim() || sourceSummary, - content: contentRedaction.content, - templateId: page.templateId, - pageType: page.pageType, - contentFormat, - changeNote: `\u4E00\u952E\u4FEE\u590D v${page.versionNo + 1}` - }, - { - type: "generated", - snapshot: { - desensitized: true - } - } - ); - return { - page: updated, - originalScan: scanContent(`${sourceTitle} -${sourceSummary} -${sourceContent}`, { - format: scanFormat - }), - redactedScan: scanContent( - `${updated.title} -${updated.summary ?? ""} -${updated.content ?? ""}`, - { format: scanFormat } - ), - redactionsApplied, - changes - }; - }; - const loadPageWorkspaceContext = async (userId, pageId, htmlContent = null) => { - const [rows] = await pool2.query( - `SELECT pv.source_snapshot_json, p.title - FROM h5_page_records p - JOIN h5_page_versions pv ON pv.id = p.current_version_id - WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted' - LIMIT 1`, - [pageId, userId] - ); - let snapshot = {}; - try { - snapshot = parseJsonColumn(rows[0]?.source_snapshot_json); - } catch { - snapshot = {}; - } - const workspacePublishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null; - const workspaceHtmlRelativePath = inferWorkspaceHtmlRelativePath({ - snapshotRelativePath: snapshot.relative_path, - pageTitle: rows[0]?.title, - htmlContent, - publishDir: workspacePublishDir - }); - return { - workspaceHtmlRelativePath, - workspacePublishDir, - publishKey: userId - }; - }; - const rewriteHtmlDownloadLinksForPage = async (userId, pageId, html) => { - const ctx = await loadPageWorkspaceContext(userId, pageId, html); - const { html: rewritten } = await prepareHtmlDownloadLinks(pool2, userId, html, { - htmlRelativePath: ctx.workspaceHtmlRelativePath, - publishDir: ctx.workspacePublishDir, - publishKey: ctx.publishKey, - publicBaseUrl: "", - preferAssetDownload: true - }); - return rewritten; - }; - const finalizeHtmlPreview = async (userId, pageId, html) => { - const shell = renderHtmlPreview(html); - try { - const ctx = await loadPageWorkspaceContext(userId, pageId, html); - const { html: rewritten } = await prepareHtmlDownloadLinks(pool2, userId, shell, { - htmlRelativePath: ctx.workspaceHtmlRelativePath, - publishDir: ctx.workspacePublishDir, - publishKey: ctx.publishKey, - publicBaseUrl: "", - preferAssetDownload: true - }); - return rewritten; - } catch { - return shell; - } - }; - const localizePrivateResources = async (userId, pageId, input = {}) => { - const page = await getPage(userId, pageId); - if (input.pageVersionId && input.pageVersionId !== page.currentVersionId) { - throw pageError("\u53EA\u80FD\u57FA\u4E8E\u5F53\u524D\u7248\u672C\u4FEE\u590D\u9875\u9762", "version_conflict", { - currentVersion: page.currentVersionId - }); - } - if (input.expectedVersion != null && asNumber4(input.expectedVersion) !== asNumber4(page.versionNo)) { - throw pageError("\u9875\u9762\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "version_conflict", { - currentVersion: asNumber4(page.versionNo) - }); - } - if (page.contentFormat !== "html") { - throw pageError("\u8BE5\u9875\u9762\u4E0D\u652F\u6301\u8D44\u6E90\u672C\u5730\u5316\u4FEE\u590D", "unsupported_publish_fix"); - } - const sourceTitle = input.title ?? page.title; - const sourceSummary = input.summary ?? page.summary ?? ""; - const sourceContent = input.content ?? page.content ?? ""; - const matches = [...String(sourceContent).matchAll(PRIVATE_ASSET_URL_PATTERN2)]; - if (matches.length === 0) { - throw pageError("\u672A\u53D1\u73B0\u53EF\u5B89\u5168\u4FEE\u590D\u7684\u79C1\u6709\u56FE\u7247\u5F15\u7528", "publish_fix_not_needed"); - } - 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 byId = new Map(assets.map((asset) => [asset.id, asset])); - const replacements = /* @__PURE__ */ new Map(); - for (const assetId of assetIds) { - const asset = byId.get(assetId); - if (!asset) continue; - const buffer = await fs17.readFile(absoluteStoragePath2(asset.storage_key)); - const mimeType = String(asset.mime_type || "application/octet-stream"); - const ext = extensionForMime2(mimeType, asset.original_filename); - const dataUri = `data:${mimeType};base64,${buffer.toString("base64")}`; - replacements.set(assetId, { dataUri, ext }); - } - if (replacements.size === 0) { - throw pageError("\u79C1\u6709\u8D44\u6E90\u4E0D\u662F\u53EF\u5185\u8054\u7684\u56FE\u7247\uFF0C\u65E0\u6CD5\u81EA\u52A8\u4FEE\u590D", "unsupported_publish_fix"); - } - let changedCount = 0; - const updatedContent = String(sourceContent).replace( - PRIVATE_ASSET_URL_PATTERN2, - (value, assetId) => { - const replacement = replacements.get(assetId); - if (!replacement) return value; - changedCount += 1; - return replacement.dataUri; - } - ); - if (updatedContent === sourceContent || changedCount === 0) { - throw pageError("\u672A\u53D1\u73B0\u53EF\u5B89\u5168\u4FEE\u590D\u7684\u79C1\u6709\u56FE\u7247\u5F15\u7528", "publish_fix_not_needed"); - } - const updated = await createVersion( - userId, - { - pageId, - expectedVersion: page.versionNo, - title: sourceTitle, - summary: sourceSummary, - content: updatedContent, - templateId: page.templateId, - pageType: page.pageType, - contentFormat: "html", - changeNote: `\u4E00\u952E\u53D1\u5E03\u4FEE\u590D v${page.versionNo + 1}` - }, - { - type: "generated", - snapshot: { - localized_private_resources: true - } - } - ); - return { - page: updated, - originalScan: scanContent(`${sourceTitle} -${sourceSummary} -${sourceContent}`, { - format: "html" - }), - redactedScan: scanContent( - `${updated.title} -${updated.summary ?? ""} -${updated.content ?? ""}`, - { format: "html" } - ), - redactionsApplied: changedCount, - changes: [ - { - field: "content", - fieldLabel: "\u6B63\u6587", - type: "private_resource_reference", - label: "\u79C1\u6709\u8D44\u6E90\u5F15\u7528", - count: changedCount - } - ] - }; - }; - const collectLinkedAssets = async (conn, userId, pageId, options2 = {}) => { - const [versions] = await conn.query( - `SELECT pv.content_asset_id, pv.bundle_asset_id - FROM h5_page_versions pv - WHERE pv.page_id = ?`, - [pageId] - ); - const assetKind = /* @__PURE__ */ new Map(); - for (const version of versions) { - if (version.content_asset_id) assetKind.set(version.content_asset_id, "content"); - if (version.bundle_asset_id) assetKind.set(version.bundle_asset_id, "bundle"); - } - if (options2.sourceAssetId) { - assetKind.set(options2.sourceAssetId, "source"); - } - for (const assetId of options2.embeddedAssetIds ?? []) { - if (assetId) assetKind.set(assetId, "embedded"); - } - const assetIds = [...assetKind.keys()]; - if (assetIds.length === 0) { - return { assets: [], totalBytes: 0, assetIds: [] }; - } - const [assets] = await conn.query( - `SELECT id, display_name, size_bytes, status - FROM h5_assets - WHERE user_id = ? AND id IN (?) AND status <> 'deleted'`, - [userId, assetIds] - ); - const linkedAssets = assets.map((asset) => ({ - id: asset.id, - displayName: asset.display_name, - sizeBytes: asNumber4(asset.size_bytes), - kind: assetKind.get(asset.id) ?? "content" - })); - return { - assets: linkedAssets, - totalBytes: linkedAssets.reduce((sum, asset) => sum + asset.sizeBytes, 0), - assetIds: linkedAssets.map((asset) => asset.id) - }; - }; - const listPlazaPostsForPage = async (conn, userId, pageId) => { - const [rows] = await conn.query( - `SELECT pp.id, pp.title, pp.status, pp.publication_id - FROM plaza_posts pp - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pr.page_id = ? AND pr.user_id = ? AND pp.status != 'hidden' - ORDER BY pp.published_at DESC, pp.updated_at DESC`, - [pageId, userId] - ); - return rows.map((row) => ({ - id: row.id, - title: row.title, - status: row.status, - publicationId: row.publication_id - })); - }; - const hidePlazaPostsForPage = async (conn, userId, pageId, now) => { - const [result] = await conn.query( - `UPDATE plaza_posts pp - JOIN h5_publish_records pr ON pr.id = pp.publication_id - SET pp.status = 'hidden', pp.updated_at = ? - WHERE pr.page_id = ? AND pr.user_id = ? AND pp.status != 'hidden'`, - [now, pageId, userId] - ); - return asNumber4(result.affectedRows); - }; - const offlineOnlinePublications = async (conn, userId, pageId, now) => { - const [rows] = await conn.query( - `SELECT id, page_version_id, access_mode - FROM h5_publish_records - WHERE page_id = ? AND user_id = ? AND status = 'online' - FOR UPDATE`, - [pageId, userId] - ); - for (const publication of rows) { - await conn.query( - `UPDATE h5_publish_records - SET status = 'offline', offline_at = ?, updated_at = ? - WHERE id = ?`, - [now, now, publication.id] - ); - await conn.query( - `INSERT INTO h5_publication_events - (id, publish_id, event_type, actor_id, old_page_version_id, new_page_version_id, - access_mode, detail_json, created_at) - VALUES (?, ?, 'offlined', ?, ?, NULL, ?, ?, ?)`, - [ - idFactory(), - publication.id, - userId, - publication.page_version_id, - publication.access_mode, - JSON.stringify({ reason: "page_deleted" }), - now - ] - ); - } - return rows.length; - }; - const softDeleteLinkedAssets = async (conn, userId, spaceId, assetIds, now) => { - let freedBytes = 0; - let deletedAssetCount = 0; - for (const assetId of assetIds) { - const [rows] = 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 = rows[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] - ); - freedBytes += asNumber4(asset.size_bytes); - deletedAssetCount += 1; - } - if (freedBytes > 0) { - await conn.query( - `UPDATE h5_user_spaces - SET used_bytes = GREATEST(0, used_bytes - ?), updated_at = ? - WHERE id = ? AND user_id = ?`, - [freedBytes, now, spaceId, userId] - ); - } - return { deletedAssetCount, freedBytes }; - }; - const getDeletePreview = async (userId, pageId) => { - const [rows] = await pool2.query( - `SELECT p.id, p.title, p.source_asset_id, p.status - FROM h5_page_records p - WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted' - LIMIT 1`, - [pageId, userId] - ); - const page = rows[0]; - if (!page) throw pageError("\u9875\u9762\u4E0D\u5B58\u5728", "page_not_found"); - const pageDetail = await getPage(userId, pageId).catch(() => null); - const conn = await pool2.getConnection(); - try { - const [versionRows] = await conn.query( - `SELECT COUNT(*) AS version_count FROM h5_page_versions WHERE page_id = ?`, - [pageId] - ); - const { assets, totalBytes } = await collectLinkedAssets(conn, userId, pageId, { - sourceAssetId: page.source_asset_id, - embeddedAssetIds: pageDetail?.contentFormat === "html" ? extractAssetIdsFromHtml(pageDetail.content ?? "") : [] - }); - const [publications] = await conn.query( - `SELECT id, status, public_url, access_mode, view_count - FROM h5_publish_records - WHERE page_id = ? AND user_id = ? - ORDER BY published_at DESC`, - [pageId, userId] - ); - const [agentJobs] = await conn.query( - `SELECT COUNT(*) AS job_count - FROM h5_agent_jobs - WHERE user_id = ? AND result_page_id = ?`, - [userId, pageId] - ); - const onlinePublication = publications.find((item) => item.status === "online") ?? null; - const offlinePublicationCount = publications.filter((item) => item.status !== "online").length; - const plazaPosts2 = await listPlazaPostsForPage(conn, userId, pageId); - const preview = { - page: { - id: page.id, - title: page.title, - status: page.status, - versionCount: asNumber4(versionRows[0]?.version_count) - }, - onlinePublication: onlinePublication ? { - id: onlinePublication.id, - publicUrl: onlinePublication.public_url, - accessMode: onlinePublication.access_mode, - viewCount: asNumber4(onlinePublication.view_count) - } : null, - offlinePublicationCount, - plazaPosts: plazaPosts2, - linkedAssets: assets, - linkedAssetBytes: totalBytes, - linkedAgentJobCount: asNumber4(agentJobs[0]?.job_count), - preservesSourceAsset: false - }; - return { ...preview, summaryLines: buildPageDeleteSummary(preview) }; - } finally { - conn.release(); - } - }; - const deletePage = async (userId, pageId, options2 = {}) => { - const removeFromPlaza = Boolean(options2.removeFromPlaza); - const page = await getPage(userId, pageId); - let workspaceHtmlRelativePath = null; - try { - const [rows] = await pool2.query( - `SELECT pv.source_snapshot_json - FROM h5_page_records p - JOIN h5_page_versions pv ON pv.id = p.current_version_id - WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted' - LIMIT 1`, - [pageId, userId] - ); - const snapshot = parseJsonColumn(rows[0]?.source_snapshot_json); - workspaceHtmlRelativePath = snapshot.relative_path ?? null; - } catch { - workspaceHtmlRelativePath = null; - } - const embeddedAssetIds = page.contentFormat === "html" ? extractAssetIdsFromHtml(page.content ?? "") : []; - const conn = await pool2.getConnection(); - let result; - try { - await conn.beginTransaction(); - const [pages] = await conn.query( - `SELECT id, space_id, title, source_asset_id - FROM h5_page_records - WHERE id = ? AND user_id = ? AND status <> 'deleted' - LIMIT 1 FOR UPDATE`, - [pageId, userId] - ); - const pageRow = pages[0]; - if (!pageRow) throw pageError("\u9875\u9762\u4E0D\u5B58\u5728", "page_not_found"); - const now = Date.now(); - const { assetIds } = await collectLinkedAssets(conn, userId, pageId, { - sourceAssetId: pageRow.source_asset_id, - embeddedAssetIds - }); - const offlinedPublicationCount = await offlineOnlinePublications(conn, userId, pageId, now); - const hiddenPlazaPostCount = removeFromPlaza ? await hidePlazaPostsForPage(conn, userId, pageId, now) : 0; - const { deletedAssetCount, freedBytes } = await softDeleteLinkedAssets( - conn, - userId, - pageRow.space_id, - assetIds, - now - ); - const [agentJobRows] = await conn.query( - `SELECT COUNT(*) AS job_count - FROM h5_agent_jobs - WHERE user_id = ? AND result_page_id = ?`, - [userId, pageId] - ); - const clearedAgentJobCount = asNumber4(agentJobRows[0]?.job_count); - await conn.query( - `UPDATE h5_agent_jobs - SET result_page_id = NULL, updated_at = ? - WHERE user_id = ? AND result_page_id = ?`, - [now, userId, pageId] - ); - await conn.query( - `UPDATE h5_page_records - SET status = 'deleted', deleted_at = ?, updated_at = ?, - current_publish_id = NULL, visibility = 'private', source_asset_id = NULL - WHERE id = ? AND user_id = ?`, - [now, now, pageId, userId] - ); - await conn.commit(); - result = { - deleted: true, - pageId, - title: pageRow.title, - offlinedPublicationCount, - hiddenPlazaPostCount, - deletedAssetCount, - freedBytes, - clearedAgentJobCount - }; - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - const workspacePublishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null; - const purgeResult = await purgeWorkspacePageArtifacts({ - publishDir: workspacePublishDir, - htmlRelativePath: workspaceHtmlRelativePath, - html: page.content ?? "" - }).catch((error) => { - console.warn("[MindSpace] workspace purge failed:", error?.message ?? error); - return { removed: [], skipped: [] }; - }); - return { ...result, workspaceFilesRemoved: purgeResult.removed ?? [] }; - }; - const loadPageThumbnailContext = async (userId, pageId) => { - const [rows] = await pool2.query( - `SELECT p.title, p.summary, p.page_type, pv.version_no, av.storage_key, pv.source_snapshot_json - FROM h5_page_records p - JOIN h5_page_versions pv ON pv.id = p.current_version_id - JOIN h5_asset_versions av ON av.asset_id = pv.content_asset_id AND av.version_no = 1 - WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted' - LIMIT 1`, - [pageId, userId] - ); - const row = rows[0]; - if (!row) throw pageError("\u9875\u9762\u4E0D\u5B58\u5728", "page_not_found"); - if (row.page_type !== "html") { - throw pageError("\u8BE5\u9875\u9762\u4E0D\u652F\u6301\u7F29\u7565\u56FE", "thumbnail_not_supported"); - } - const storagePath = await resolveReadableStoragePath(row.storage_key); - const content = await fs17.readFile(storagePath, "utf8"); - let snapshot = {}; - try { - snapshot = parseJsonColumn(row.source_snapshot_json); - } catch { - snapshot = {}; - } - const workspaceHtmlRelativePath = snapshot.relative_path ?? null; - const workspacePublishDir = workspaceHtmlRelativePath ? await resolveWorkspacePublishDir(userId) : null; - return { - row, - content, - workspaceHtmlRelativePath, - workspacePublishDir, - thumbnailStorageKey: pageThumbnailKey(userId, pageId) - }; - }; - const syncWorkspaceThumbnail = async ({ - workspacePublishDir, - workspaceHtmlRelativePath, - html, - meta - }) => { - if (!workspacePublishDir || !workspaceHtmlRelativePath) return; - await ensureWorkspaceHtmlThumbnail( - workspacePublishDir, - workspaceHtmlRelativePath, - html, - { ...meta, force: true } - ).catch(() => { - }); - }; - const uploadThumbnail = async (userId, pageId, input = {}) => { - const imageBase64 = String(input.imageBase64 ?? "").trim(); - if (!imageBase64) throw pageError("\u8BF7\u4E0A\u4F20\u56FE\u7247", "thumbnail_image_required"); - const buffer = Buffer.from(imageBase64, "base64"); - const coverDataUri = bufferToImageDataUri(buffer); - if (!coverDataUri) throw pageError("\u56FE\u7247\u65E0\u6548\u6216\u8FC7\u5927", "thumbnail_image_invalid"); - const ctx = await loadPageThumbnailContext(userId, pageId); - const html = String(input.html ?? ctx.content); - const title = String(input.title ?? ctx.row.title ?? "").trim() || ctx.row.title; - const summary = String(input.summary ?? ctx.row.summary ?? "").trim(); - const signals = extractCoverSignals(html, { - title, - subtitle: summary || `\u8349\u7A3F v${asNumber4(ctx.row.version_no)}` - }); - const svg = buildFeedThumbnailSvg(signals, { coverDataUri }); - await writeThumbnail(storageRoot, ctx.thumbnailStorageKey, svg); - if (ctx.workspacePublishDir && ctx.workspaceHtmlRelativePath) { - await writeThumbnail( - ctx.workspacePublishDir, - workspaceThumbnailRelativePath(ctx.workspaceHtmlRelativePath), - svg - ); - } - return { updatedAt: Date.now() }; - }; - const regenerateThumbnail = async (userId, pageId, input = {}, options2 = {}) => { - const ctx = await loadPageThumbnailContext(userId, pageId); - let html = String(input.html ?? ctx.content); - const title = String(input.title ?? ctx.row.title ?? "").trim() || ctx.row.title; - const summary = String(input.summary ?? ctx.row.summary ?? "").trim(); - let updatedContent = null; - let metaOverrides = {}; - if (input.useAi) { - if (!options2.suggestCoverMeta) { - throw pageError("AI \u5C01\u9762\u751F\u6210\u672A\u542F\u7528", "cover_ai_unavailable"); - } - metaOverrides = await options2.suggestCoverMeta({ - title, - summary, - html, - instruction: input.instruction - }); - updatedContent = upsertMindspaceCoverMeta(html, metaOverrides); - html = updatedContent; - } - const meta = { - title, - subtitle: summary || `\u8349\u7A3F v${asNumber4(ctx.row.version_no)}`, - contentStorageKey: ctx.row.storage_key, - force: true, - ...metaOverrides - }; - await generateHtmlThumbnail(storageRoot, ctx.thumbnailStorageKey, html, meta); - await syncWorkspaceThumbnail({ - workspacePublishDir: ctx.workspacePublishDir, - workspaceHtmlRelativePath: ctx.workspaceHtmlRelativePath, - html, - meta - }); - return { - updatedAt: Date.now(), - content: updatedContent - }; - }; - return { - createFromChat: (userId, input, source) => createVersion(userId, input, { - type: "chat", - sessionId: source.sessionId, - messageId: source.messageId, - assetId: source.assetId ?? null, - snapshot: source.snapshot, - createdAt: source.createdAt - }), - createFromAgent: (userId, input, source) => createVersion(userId, input, { - type: "agent", - assetId: source.assetId ?? null, - snapshot: { - job_id: source.jobId ?? null, - source_asset_ids: source.assetIds ?? [] - } - }), - createPage: (userId, input) => createVersion(userId, input, { type: "template" }), - updatePage: (userId, pageId, input) => createVersion(userId, { ...input, pageId }, { type: "generated" }), - localizePrivateResources, - redactPage, - createRedactedCopy: redactPage, - listPages, - findPageBySourceAsset, - findPageBySourceMessage, - findPageByRelativePath, - getPage, - getDeletePreview, - deletePage, - listVersions, - rewriteHtmlDownloadLinksForPage, - renderPreview: async (userId, pageId) => { - const page = await getPage(userId, pageId); - const html = page.contentFormat === "html" ? await finalizeHtmlPreview(userId, pageId, page.content) : renderPreviewHtml(page); - return { html, contentFormat: page.contentFormat }; - }, - renderDraftPreview: async (userId, pageId, input = {}) => { - const page = await getPage(userId, pageId); - const title = String(input.title ?? page.title ?? "").trim() || page.title; - const summary = String(input.summary ?? page.summary ?? "").trim(); - const content = String(input.content ?? page.content ?? ""); - const templateId = input.templateId ?? page.templateId; - const html = page.contentFormat === "html" ? await finalizeHtmlPreview(userId, pageId, content) : renderPreviewHtml({ - title, - summary, - templateId, - versionNo: page.versionNo, - content - }); - return { html, contentFormat: page.contentFormat }; - }, - renderThumbnail: async (userId, pageId) => { - const ctx = await loadPageThumbnailContext(userId, pageId); - return ensurePageThumbnail({ - storageRoot, - pageThumbnailStorageKey: ctx.thumbnailStorageKey, - html: ctx.content, - meta: { - title: ctx.row.title, - subtitle: ctx.row.summary || `\u8349\u7A3F v${asNumber4(ctx.row.version_no)}`, - contentStorageKey: ctx.row.storage_key - }, - workspacePublishDir: ctx.workspacePublishDir, - workspaceHtmlRelativePath: ctx.workspaceHtmlRelativePath - }); - }, - uploadThumbnail, - regenerateThumbnail - }; -} -async function registerPageArtifactForConversation({ - registry, - userId, - source, - page, - contentAssetId, - contentMimeType, - contentBytes, - storageKey, - versionNo, - now -}) { - const sessionId = String(source?.sessionId ?? "").trim(); - if (!registry || !sessionId || !page?.id) return null; - try { - const packageRecord = await registry.ensurePackage({ - userId, - sessionId, - title: page.title, - now - }); - const artifact = await registry.recordArtifact({ - id: `ca_${page.id}_${page.currentVersionId}`, - packageId: packageRecord.id, - artifactKind: "page", - role: "assistant", - assetId: contentAssetId, - pageId: page.id, - messageId: source.messageId ?? page.sourceMessageId ?? null, - displayName: page.title, - mimeType: contentMimeType, - sizeBytes: contentBytes, - storageKey, - canonicalUrl: `/api/mindspace/v1/pages/${encodeURIComponent(page.id)}/preview`, - sortOrder: Number.isFinite(Number(versionNo)) ? Number(versionNo) : 0, - now - }); - await registry.writeManifestForSession({ userId, sessionId }); - return artifact; - } catch (error) { - console.warn( - "[MindSpace] conversation package page artifact registration failed:", - error instanceof Error ? error.message : error - ); - return null; - } -} -var pageInternals = { - escapeHtml: escapeHtml2, - normalizePageInput, - normalizeListPageFilters, - parseJsonColumn, - renderContent, - renderPreviewHtml, - renderHtmlPreview, - previewContentSecurityPolicy, - isHtmlPage, - renderPublicationHtml, - registerPageArtifactForConversation, - redactContent, - buildPageDeleteSummary -}; -async function inlinePrivateAssetsInHtml(pool2, storageRoot, userId, html) { - const PATTERN = /(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi; - const resolvedRoot = path21.resolve(storageRoot); - const absoluteStoragePath2 = (key) => { - const resolved = path21.resolve(resolvedRoot, key); - if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path21.sep}`)) { - throw Object.assign(new Error("\u8DEF\u5F84\u8D8A\u754C"), { code: "invalid_storage_path" }); - } - return resolved; - }; - const matches = [...String(html).matchAll(PATTERN)]; - if (matches.length === 0) return { html, count: 0 }; - const assetIds = [...new Set(matches.map((m) => m[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 dataUriMap = /* @__PURE__ */ new Map(); - for (const asset of assets) { - try { - const buffer = await fs17.readFile(absoluteStoragePath2(asset.storage_key)); - dataUriMap.set(asset.id, `data:${asset.mime_type};base64,${buffer.toString("base64")}`); - } catch { - } - } - let count = 0; - const result = String(html).replace(PATTERN, (_match, assetId) => { - const uri = dataUriMap.get(assetId); - if (!uri) return ""; - count += 1; - return uri; - }); - return { html: result, count }; -} -function buildPageDeleteSummary(preview) { - const lines = [ - `\u9875\u9762\u300C${preview.page.title}\u300D\u53CA\u5176 ${preview.page.versionCount} \u4E2A\u7248\u672C` - ]; - if (preview.onlinePublication) { - lines.push(`\u5728\u7EBF\u516C\u5F00\u94FE\u63A5\u5C06\u7ACB\u5373\u4E0B\u7EBF\uFF1A${preview.onlinePublication.publicUrl}`); - } - if (preview.offlinePublicationCount > 0) { - lines.push(`${preview.offlinePublicationCount} \u6761\u5386\u53F2\u53D1\u5E03\u8BB0\u5F55\u5C06\u4FDD\u7559\u5BA1\u8BA1\u4FE1\u606F\uFF0C\u4F46\u4E0D\u518D\u53EF\u8BBF\u95EE`); - } - if (preview.linkedAssets.length > 0) { - lines.push( - `${preview.linkedAssets.length} \u4E2A\u9875\u9762\u5185\u5BB9\u8D44\u4EA7\u5C06\u88AB\u5220\u9664\uFF08\u7EA6 ${formatByteSize(preview.linkedAssetBytes)}\uFF09` - ); - } - if (preview.linkedAgentJobCount > 0) { - lines.push(`${preview.linkedAgentJobCount} \u4E2A Agent \u4EFB\u52A1\u7ED3\u679C\u5173\u8054\u5C06\u88AB\u6E05\u9664`); - } - if (preview.plazaPosts?.length > 0) { - lines.push( - `\u8BE5\u9875\u9762\u5728\u5E7F\u573A\u6709 ${preview.plazaPosts.length} \u4E2A\u5E16\u5B50\uFF1B\u53EF\u5728\u4E0B\u65B9\u9009\u62E9\u662F\u5426\u4E00\u5E76\u4ECE\u5E7F\u573A\u79FB\u9664` - ); - } - if (preview.preservesSourceAsset) { - lines.push("\u539F\u59CB\u4E0A\u4F20\u8D44\u6599\u4E0D\u4F1A\u88AB\u5220\u9664"); - } - return lines; -} -function formatByteSize(bytes) { - const value = asNumber4(bytes); - if (value < 1024) return `${value} B`; - if (value < 1024 * 1024) return `${(value / 1024).toFixed(value % 1024 === 0 ? 0 : 1)} KB`; - return `${(value / (1024 * 1024)).toFixed(value % (1024 * 1024) === 0 ? 0 : 1)} MB`; -} - -// mindspace-conversation-package-registry.mjs -function registryError(message, code) { - return Object.assign(new Error(message), { code }); -} -function requireMethod(target, methodName, code) { - if (!target || typeof target[methodName] !== "function") { - throw registryError(`MindSpace conversation package registry requires ${methodName}`, code); - } -} -function createConversationPackageRegistry({ store, service } = {}) { - requireMethod(store, "upsertPackage", "missing_conversation_package_store"); - requireMethod(store, "upsertArtifact", "missing_conversation_package_store"); - requireMethod(store, "getPackageBySession", "missing_conversation_package_store"); - requireMethod(store, "listArtifacts", "missing_conversation_package_store"); - requireMethod(service, "putConversationManifest", "missing_mindspace_service"); - requireMethod(service, "buildConversationManifest", "missing_mindspace_service"); - requireMethod(service, "putPackageObject", "missing_mindspace_service"); - requireMethod(service, "getPackageObject", "missing_mindspace_service"); - return { - async ensurePackage(input) { - return store.upsertPackage(input); - }, - async recordArtifact(input) { - return store.upsertArtifact(input); - }, - async writeManifestForSession({ userId, sessionId }) { - const packageRecord = await store.getPackageBySession(userId, sessionId); - if (!packageRecord) return null; - const artifacts = await store.listArtifacts(packageRecord.id); - return service.putConversationManifest({ packageRecord, artifacts }); - }, - async putObjectForSession({ userId, sessionId, title = null, relativePath, body }) { - const packageRecord = await store.upsertPackage({ userId, sessionId, title }); - const writeResult = await service.putPackageObject({ packageRecord, relativePath, body }); - return { packageRecord, writeResult }; - }, - async readManifestForSession({ userId, sessionId }) { - const packageRecord = await store.getPackageBySession(userId, sessionId); - if (!packageRecord) return null; - const artifacts = await store.listArtifacts(packageRecord.id); - return service.buildConversationManifest({ packageRecord, artifacts }); - }, - async readArtifactObject({ userId, sessionId, artifactId }) { - const packageRecord = await store.getPackageBySession(userId, sessionId); - if (!packageRecord) return null; - const artifacts = await store.listArtifacts(packageRecord.id); - const artifact = artifacts.find((item) => item.id === artifactId); - if (!artifact?.storageKey) return null; - const prefix = `${packageRecord.storagePrefix}/`; - if (!artifact.storageKey.startsWith(prefix)) { - throw registryError("conversation artifact storage key is outside package", "invalid_artifact_storage_key"); - } - const body = await service.getPackageObject({ - packageRecord, - relativePath: artifact.storageKey.slice(prefix.length) - }); - return { packageRecord, artifact, body }; - } - }; -} - -// mindspace-conversation-package-routes.mjs -function defaultSendData(res, _req, data) { - return res.json({ data }); -} -function defaultRouteError(res, _req, error) { - return res.status(500).json({ message: error?.message || "MindSpace \u670D\u52A1\u5F02\u5E38" }); -} -function resolveRegistry(input) { - return typeof input === "function" ? input() : input; -} -function encodeDownloadFilename(filename) { - const safe = String(filename ?? "").trim() || "mindspace-artifact"; - return `attachment; filename="${encodeURIComponent(safe)}"; filename*=UTF-8''${encodeURIComponent(safe)}`; -} -function createGetConversationPackageHandler({ - registry, - getRegistry, - userAuth: userAuth2, - getUserAuth, - ensureMindSpaceEnabled: ensureMindSpaceEnabled2 = () => true, - sendData: sendData2 = defaultSendData, - mindSpaceError: mindSpaceError2 = defaultRouteError, - beforeReadManifest -} = {}) { - return async function getConversationPackage(req, res) { - const activeRegistry = resolveRegistry(getRegistry ?? registry); - if (!activeRegistry || !ensureMindSpaceEnabled2(res, req)) { - if (res.headersSent) return void 0; - return res.status(503).json({ message: "MindSpace \u5BF9\u8BDD\u5305\u672A\u542F\u7528" }); - } - const sessionId = String(req.params?.sessionId ?? "").trim(); - if (!sessionId) return res.status(400).json({ message: "\u7F3A\u5C11 sessionId" }); - try { - const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth2); - if (!await activeUserAuth.ownsSession(req.currentUser.id, sessionId)) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - if (typeof beforeReadManifest === "function") { - await beforeReadManifest({ req, userId: req.currentUser.id, sessionId }); - } - const manifest = await activeRegistry.readManifestForSession({ - userId: req.currentUser.id, - sessionId - }); - if (!manifest) return res.status(404).json({ message: "\u5BF9\u8BDD\u5305\u4E0D\u5B58\u5728" }); - return sendData2(res, req, manifest); - } catch (error) { - return mindSpaceError2(res, req, error); - } - }; -} -function createDownloadConversationPackageManifestHandler({ - registry, - getRegistry, - userAuth: userAuth2, - getUserAuth, - ensureMindSpaceEnabled: ensureMindSpaceEnabled2 = () => true, - mindSpaceError: mindSpaceError2 = defaultRouteError, - beforeReadManifest -} = {}) { - return async function downloadConversationPackageManifest(req, res) { - const activeRegistry = resolveRegistry(getRegistry ?? registry); - if (!activeRegistry || !ensureMindSpaceEnabled2(res, req)) { - if (res.headersSent) return void 0; - return res.status(503).json({ message: "MindSpace \u5BF9\u8BDD\u5305\u672A\u542F\u7528" }); - } - const sessionId = String(req.params?.sessionId ?? "").trim(); - if (!sessionId) return res.status(400).json({ message: "\u7F3A\u5C11 sessionId" }); - try { - const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth2); - if (!await activeUserAuth.ownsSession(req.currentUser.id, sessionId)) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - if (typeof beforeReadManifest === "function") { - await beforeReadManifest({ req, userId: req.currentUser.id, sessionId }); - } - const manifest = await activeRegistry.readManifestForSession({ - userId: req.currentUser.id, - sessionId - }); - if (!manifest) return res.status(404).json({ message: "\u5BF9\u8BDD\u5305\u4E0D\u5B58\u5728" }); - const filename = `${manifest.packageId || sessionId}-manifest.json`; - res.set("Content-Type", "application/json; charset=utf-8"); - res.set("Content-Disposition", encodeDownloadFilename(filename)); - res.set("Cache-Control", "private, no-store"); - if (req.requestId) res.setHeader("X-Request-Id", req.requestId); - return res.send(`${JSON.stringify(manifest, null, 2)} -`); - } catch (error) { - return mindSpaceError2(res, req, error); - } - }; -} -function createDownloadConversationPackageArtifactHandler({ - registry, - getRegistry, - userAuth: userAuth2, - getUserAuth, - ensureMindSpaceEnabled: ensureMindSpaceEnabled2 = () => true, - mindSpaceError: mindSpaceError2 = defaultRouteError -} = {}) { - return async function downloadConversationPackageArtifact(req, res) { - const activeRegistry = resolveRegistry(getRegistry ?? registry); - if (!activeRegistry || !ensureMindSpaceEnabled2(res, req)) { - if (res.headersSent) return void 0; - return res.status(503).json({ message: "MindSpace \u5BF9\u8BDD\u5305\u672A\u542F\u7528" }); - } - const sessionId = String(req.params?.sessionId ?? "").trim(); - const artifactId = String(req.params?.artifactId ?? "").trim(); - if (!sessionId) return res.status(400).json({ message: "\u7F3A\u5C11 sessionId" }); - if (!artifactId) return res.status(400).json({ message: "\u7F3A\u5C11 artifactId" }); - try { - const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth2); - if (!await activeUserAuth.ownsSession(req.currentUser.id, sessionId)) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - const result = await activeRegistry.readArtifactObject({ - userId: req.currentUser.id, - sessionId, - artifactId - }); - if (!result) return res.status(404).json({ message: "\u6587\u4EF6\u4E0D\u5B58\u5728" }); - const filename = result.artifact.displayName || result.artifact.id; - res.set("Content-Type", result.artifact.mimeType || "application/octet-stream"); - res.set("Content-Disposition", encodeDownloadFilename(filename)); - res.set("Cache-Control", "private, no-store"); - if (req.requestId) res.setHeader("X-Request-Id", req.requestId); - return res.send(result.body); - } catch (error) { - return mindSpaceError2(res, req, error); - } - }; -} - -// mindspace-conversation-package-backfill.mjs -import fs19 from "node:fs/promises"; -import path23 from "node:path"; - -// mindspace-publications.mjs -import crypto13 from "node:crypto"; -import fs18 from "node:fs/promises"; -import path22 from "node:path"; - -// mindspace-html-localize.mjs -var GOOGLE_FONTS_CSS_IMPORT_RE = /@import\s+url\(\s*['"]?(https:\/\/fonts\.googleapis\.com\/[^'")\s]+)['"]?\s*\)\s*;?/gi; -var GOOGLE_FONTS_LINK_RE = /]*\bhref=['"](https:\/\/fonts\.googleapis\.com\/[^'"]+)['"][^>]*>/gi; -async function fetchStylesheet(url) { - const response = await fetch(url, { - headers: { "User-Agent": "TKMind-MindSpace/1.0" }, - signal: AbortSignal.timeout(15e3) - }); - if (!response.ok) { - throw new Error(`\u65E0\u6CD5\u4E0B\u8F7D\u6837\u5F0F\u8868: ${response.status}`); - } - return response.text(); -} -function uniqueUrls(matches) { - return [...new Set(matches.map((value) => value.replace(/&/g, "&")))]; -} -async function localizeGoogleFontsCss(html) { - const source = String(html); - const importUrls = uniqueUrls([...source.matchAll(GOOGLE_FONTS_CSS_IMPORT_RE)].map((m) => m[1])); - const linkUrls = uniqueUrls([...source.matchAll(GOOGLE_FONTS_LINK_RE)].map((m) => m[1])); - const stylesheetUrls = uniqueUrls([...importUrls, ...linkUrls]); - if (!stylesheetUrls.length) { - return { html: source, localizedCount: 0, inlinedBytes: 0 }; - } - const inlinedBlocks = []; - for (const url of stylesheetUrls) { - try { - inlinedBlocks.push(`/* localized from ${url} */ -${await fetchStylesheet(url)}`); - } catch { - inlinedBlocks.push(`/* failed to localize ${url} */`); - } - } - let next = source; - for (const url of importUrls) { - const pattern = new RegExp( - `@import\\s+url\\(\\s*['"]?${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]?\\s*\\)\\s*;?`, - "gi" - ); - next = next.replace(pattern, ""); - } - next = next.replace(GOOGLE_FONTS_LINK_RE, ""); - const styleBlock = ``; - if (/]*>/i.test(next)) { - next = next.replace(/]*)>/i, `${styleBlock}`); - } else if (/]*>/i.test(next)) { - next = next.replace(/]*)>/i, `${styleBlock}`); - } else { - next = `${styleBlock} -${next}`; - } - const inlinedBytes = Buffer.byteLength(inlinedBlocks.join("\n")); - return { html: next, localizedCount: stylesheetUrls.length, inlinedBytes }; -} - -// mindspace-publications.mjs -var SCANNER_VERSION = "mindspace-content-v1"; -var PRIVATE_ASSET_DOWNLOAD_URL_PATTERN = /(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi; -var URL_ATTR_PATTERN3 = /(\b(?:href|src)\s*=\s*["'])([^"']+)(["'])/gi; -var ACCESS_MODES = /* @__PURE__ */ new Set([ - "public", - "password", - "private_link", - "time_limited", - "login_required", - "owner_only" -]); -var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/; -function publicationError(message, code, details) { - return Object.assign(new Error(message), { code, details }); -} -function normalizeSlug(value) { - const slug = String(value ?? "").normalize("NFKC").trim().toLowerCase(); - if (!SLUG_PATTERN.test(slug)) { - throw publicationError("\u9875\u9762\u5730\u5740\u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26", "invalid_publish_input"); - } - return slug; -} -async function findAvailableSlug(pool2, userId, preferredSlug, excludePageId) { - const normalized = normalizeSlug(preferredSlug); - const candidates = [normalized]; - for (let index = 2; index <= 20; index += 1) { - candidates.push(`${normalized}-${index}`); - } - candidates.push(`${normalized}-${excludePageId.replace(/-/g, "").slice(0, 8)}`); - for (const candidate of candidates) { - if (!SLUG_PATTERN.test(candidate)) continue; - const [rows] = await pool2.query( - `SELECT id FROM h5_publish_records - WHERE user_id = ? AND url_slug = ? AND status = 'online' AND page_id <> ? - LIMIT 1`, - [userId, candidate, excludePageId] - ); - if (!rows[0]) return candidate; - } - return `${normalized}-${crypto13.randomBytes(3).toString("hex")}`; -} -function normalizeAccessMode(value) { - const mode = String(value ?? "public"); - if (!ACCESS_MODES.has(mode)) { - throw publicationError("\u4E0D\u652F\u6301\u7684\u8BBF\u95EE\u65B9\u5F0F", "invalid_publish_input"); - } - return mode; -} -function normalizePassword(value, required) { - const password = String(value ?? ""); - if (!required && !password) return null; - if (password.length < 8 || password.length > 128) { - throw publicationError("\u8BBF\u95EE\u5BC6\u7801\u957F\u5EA6\u5FC5\u987B\u4E3A 8 \u5230 128 \u4E2A\u5B57\u7B26", "invalid_publish_input"); - } - return password; -} -function normalizeExpiresAt(value, required) { - if (!required && (value == null || value === "")) return null; - const expiresAt = Number(value); - if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now()) { - throw publicationError("\u8FC7\u671F\u65F6\u95F4\u5FC5\u987B\u665A\u4E8E\u5F53\u524D\u65F6\u95F4", "invalid_publish_input"); - } - return expiresAt; -} -function hashPassword(password) { - const salt = crypto13.randomBytes(16); - const hash = crypto13.scryptSync(password, salt, 32); - return `scrypt$${salt.toString("hex")}$${hash.toString("hex")}`; -} -function verifyPassword2(password, encoded) { - const [algorithm, saltHex, hashHex] = String(encoded ?? "").split("$"); - if (algorithm !== "scrypt" || !saltHex || !hashHex) return false; - const expected = Buffer.from(hashHex, "hex"); - const actual = crypto13.scryptSync(String(password ?? ""), Buffer.from(saltHex, "hex"), expected.length); - return expected.length === actual.length && crypto13.timingSafeEqual(expected, actual); -} -function deviceType(userAgent) { - const value = String(userAgent ?? ""); - if (/bot|crawler|spider|slurp/i.test(value)) return "bot"; - if (/ipad|tablet|kindle|silk/i.test(value)) return "tablet"; - if (/mobile|iphone|android/i.test(value)) return "mobile"; - if (value) return "desktop"; - return "unknown"; -} -function referrerHost(referrer) { - if (!referrer) return null; - try { - return new URL(referrer).hostname.slice(0, 255) || null; - } catch { - return null; - } -} -function publicationResponse(row) { - if (!row) return null; - return { - id: row.id, - pageId: row.page_id, - pageVersionId: row.page_version_id, - urlSlug: row.url_slug, - publicUrl: row.public_url, - accessMode: row.access_mode, - expiresAt: row.expires_at == null ? null : Number(row.expires_at), - status: row.status, - viewCount: Number(row.view_count ?? 0), - publishedAt: Number(row.published_at), - offlineAt: row.offline_at == null ? null : Number(row.offline_at) - }; -} -function publicHomepageResponse(owner, pages) { - const totalViews = pages.reduce( - (sum, page) => sum + Number(page.viewCount ?? page.view_count ?? 0), - 0 - ); - return { - owner: { - id: owner.id, - slug: owner.slug, - username: owner.username, - displayName: owner.display_name || owner.username - }, - totalViews, - pageCount: pages.length, - pages: pages.map((page) => ({ - id: page.id, - pageId: page.page_id, - title: page.title, - summary: page.summary ?? "", - templateId: page.template_id, - publicUrl: page.public_url, - urlSlug: page.url_slug, - viewCount: Number(page.view_count ?? 0), - publishedAt: Number(page.published_at) - })) - }; -} -function buildPublicationThumbnailFallback(ownerSlug, urlSlug) { - return `/u/${encodeURIComponent(ownerSlug)}/pages/${encodeURIComponent(urlSlug)}.thumbnail.png`; -} -function companionKindForWorkspacePath(workspacePath) { - const normalized = String(workspacePath ?? "").toLowerCase(); - if (normalized.endsWith(".long.png")) return "long_image"; - if (normalized.endsWith(".docx") || normalized.endsWith(".doc")) return "docx"; - if (normalized.endsWith(".pdf")) return "pdf"; - return "generated_file"; -} -function companionMimeForWorkspacePath(workspacePath) { - const normalized = String(workspacePath ?? "").toLowerCase(); - if (normalized.endsWith(".long.png") || normalized.endsWith(".png")) return "image/png"; - if (normalized.endsWith(".docx")) { - return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - } - if (normalized.endsWith(".doc")) return "application/msword"; - if (normalized.endsWith(".pdf")) return "application/pdf"; - if (normalized.endsWith(".csv")) return "text/csv"; - if (normalized.endsWith(".md")) return "text/markdown"; - if (normalized.endsWith(".txt")) return "text/plain"; - return "application/octet-stream"; -} -async function statWorkspaceFile(publishDir, workspacePath) { - if (!publishDir || !workspacePath) return null; - const normalized = String(workspacePath).replace(/^\/+/, "").replace(/\\/g, "/"); - const root = path22.resolve(publishDir); - const absolutePath = path22.resolve(root, ...normalized.split("/")); - if (absolutePath !== root && !absolutePath.startsWith(`${root}${path22.sep}`)) return null; - try { - const stat = await fs18.stat(absolutePath); - if (!stat.isFile()) return null; - return { workspacePath: normalized, sizeBytes: stat.size }; - } catch (error) { - if (error?.code !== "ENOENT") throw error; - return null; - } -} -async function collectPublicationCompanionArtifacts({ - publishDir, - html = "", - htmlRelativePath = null -}) { - if (!publishDir || !htmlRelativePath) return []; - const candidates = /* @__PURE__ */ new Set(); - const normalizedHtmlPath = String(htmlRelativePath).replace(/^\/+/, "").replace(/\\/g, "/"); - if (normalizedHtmlPath.toLowerCase().endsWith(".html")) { - candidates.add(normalizedHtmlPath.replace(/\.html$/i, ".docx")); - candidates.add(normalizedHtmlPath.replace(/\.html$/i, ".long.png")); - } - let match; - while ((match = URL_ATTR_PATTERN3.exec(String(html ?? ""))) !== null) { - const ref = match[2]; - if (isRelativeDownloadReference(ref)) { - candidates.add(resolveWorkspaceRelativeFilePath(normalizedHtmlPath, ref)); - } - } - const artifacts = []; - for (const candidate of candidates) { - const stat = await statWorkspaceFile(publishDir, candidate); - if (stat) artifacts.push(stat); - } - return artifacts; -} -var PUBLIC_IMAGE_STORAGE_KEY_PATTERN2 = /^users\/([^/]+)\/images\/(\d{4}-\d{2}-\d{2}\/[^?#"'<>@\s]+)$/i; -function storageKeyToPublicStandardImageUrl(storageKey, publicBaseUrl) { - const match = String(storageKey ?? "").match(PUBLIC_IMAGE_STORAGE_KEY_PATTERN2); - if (!match) return null; - return buildPublicUrl(publicBaseUrl, match[1], `public/images/${match[2]}`); -} -function replaceImgproxyStandardImageReferences(html, publicBaseUrl) { - const source = String(html ?? ""); - if (!source) return source; - return source.replace( - /(?:https?:\/\/[^/"'<>?\s,)]+)?\/[^"'<>?\s,)]*plain\/local:\/\/\/(users\/[^"'<>?\s,)]*?\/images\/\d{4}-\d{2}-\d{2}\/[^"'<>?\s,@)]+)(?:@[a-z0-9]+)?(?:\?[^"'<>),\s]*)?/gi, - (value, storageKey) => storageKeyToPublicStandardImageUrl(storageKey, publicBaseUrl) ?? value - ); -} -function workspacePublicAssetRelativeUrl(htmlRelativePath, assetRelativePath) { - const htmlPath = String(htmlRelativePath ?? "").replace(/^\/+/, "") || "public/index.html"; - const assetPath = String(assetRelativePath ?? "").replace(/^\/+/, ""); - const htmlDir = path22.posix.dirname(htmlPath); - const relativePath = path22.posix.relative(htmlDir === "." ? "" : htmlDir, assetPath).replace(/\\/g, "/"); - return relativePath || path22.posix.basename(assetPath); -} -function rewriteWorkspacePublicAssetReferences(html, htmlRelativePath) { - const source = String(html ?? ""); - if (!source) return source; - return source.replace( - /public\/((?:images|\.tmp-images)\/[^"'<>@\s)]+(?:\?[^"'<>)\s]*)?)/gi, - (value, assetRelativePath) => workspacePublicAssetRelativeUrl(htmlRelativePath, `public/${assetRelativePath}`) - ); -} -async function localizePrivateImageReferences({ - pool: pool2, - userId, - html, - absoluteStoragePath: absoluteStoragePath2, - imgproxySigner = null -}) { - const source = String(html ?? ""); - const matches = [...source.matchAll(PRIVATE_ASSET_DOWNLOAD_URL_PATTERN)]; - if (matches.length === 0) return source; - const assetIds = [...new Set(matches.map((match) => match[1]).filter(Boolean))]; - if (assetIds.length === 0) return source; - const [assets] = await pool2.query( - `SELECT a.id, a.mime_type, v.storage_key - FROM h5_assets a - JOIN h5_asset_versions v ON v.id = a.current_version_id - WHERE a.user_id = ? AND a.id IN (?) AND a.mime_type LIKE 'image/%' - AND a.status <> 'deleted'`, - [userId, assetIds] - ); - const byId = new Map(assets.map((asset) => [asset.id, asset])); - const replacements = /* @__PURE__ */ new Map(); - for (const assetId of assetIds) { - const asset = byId.get(assetId); - if (!asset) continue; - if (imgproxySigner) { - replacements.set(assetId, imgproxySigner.buildUrl(imgproxySigner.baseUrl, asset.storage_key, "display")); - } else { - const mimeType = String(asset.mime_type || "application/octet-stream"); - const buffer = await fs18.readFile(absoluteStoragePath2(asset.storage_key)); - replacements.set(assetId, `data:${mimeType};base64,${buffer.toString("base64")}`); - } - } - if (replacements.size === 0) return source; - return source.replace(PRIVATE_ASSET_DOWNLOAD_URL_PATTERN, (value, assetId) => { - return replacements.get(assetId) ?? value; - }); -} -async function prepareHtmlPublishContent({ - pool: pool2, - userId, - html, - ownerSlug, - urlSlug, - htmlRelativePath = null, - pageTitle = null, - absoluteStoragePath: absoluteStoragePath2, - imgproxySigner = null, - h5Root = null -}) { - let publishContent = (await localizeGoogleFontsCss(html)).html; - publishContent = replaceImgproxyStandardImageReferences( - publishContent, - resolvePublicBaseUrl() - ); - publishContent = await localizePrivateImageReferences({ - pool: pool2, - userId, - html: publishContent, - absoluteStoragePath: absoluteStoragePath2, - imgproxySigner - }); - const publishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null; - const resolvedHtmlRelativePath = htmlRelativePath ?? inferWorkspaceHtmlRelativePath({ - pageTitle, - htmlContent: publishContent, - publishDir - }); - publishContent = rewriteWorkspacePublicAssetReferences(publishContent, resolvedHtmlRelativePath); - const linkIndex = await loadWorkspaceDownloadLinkIndex(pool2, userId); - publishContent = rewriteRelativeDownloadLinks(publishContent, { - htmlRelativePath: resolvedHtmlRelativePath, - publishDir, - linkIndex, - publishKey: userId, - publicBaseUrl: resolvePublicBaseUrl(), - preferAssetDownload: false - }).html; - return replacePrivateResourceReferences( - publishContent, - buildPublicationThumbnailFallback(ownerSlug, urlSlug) - ); -} -function createPublicationService(pool2, options = {}) { - const storageRoot = path22.resolve(options.storageRoot ?? path22.join(process.cwd(), "data", "mindspace")); - const h5Root = options.h5Root ?? null; - const idFactory = options.idFactory ?? (() => crypto13.randomUUID()); - const publicPageLimitFallback = Number(options.publicPageLimit ?? 5); - const conversationPackageRegistry = options.conversationPackageRegistry ?? null; - let imgproxySigner = null; - if (process.env.IMGPROXY_BASE_URL && process.env.IMGPROXY_SIGNING_KEY && process.env.IMGPROXY_SIGNING_SALT) { - try { - imgproxySigner = createImgproxySigner( - process.env.IMGPROXY_SIGNING_KEY, - process.env.IMGPROXY_SIGNING_SALT - ); - imgproxySigner.baseUrl = process.env.IMGPROXY_BASE_URL; - console.log("[Publication] imgproxy signer initialized"); - } catch (err) { - console.warn("[Publication] imgproxy signer init failed:", err instanceof Error ? err.message : err); - } - } - const resolvePublicPageLimit = async () => { - try { - const config = await loadMindSpaceConfig(pool2); - return Number(config.publicPageLimit ?? publicPageLimitFallback); - } catch { - return publicPageLimitFallback; - } - }; - const absoluteStoragePath2 = (storageKey) => { - const resolved = path22.resolve(storageRoot, storageKey); - if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path22.sep}`)) { - throw new Error("\u5B58\u50A8\u8DEF\u5F84\u8D8A\u754C"); - } - return resolved; - }; - const resolveStoragePathCandidates = (storageKey) => { - const normalized = String(storageKey ?? "").replace(/\\/g, "/"); - const withoutMd = normalized.endsWith(".md") ? normalized.slice(0, -3) : normalized; - const match = withoutMd.match(/^users\/([^/]+)\/(assets|pages)\/([^/]+)\/(v\d+)$/); - if (!match) return [normalized]; - const [, userId, scope, entityId, versionTag] = match; - return [ - normalized, - `users/${userId}/${scope}/${entityId}/versions/${versionTag}.md`, - `users/${userId}/${scope}/${entityId}/versions/${versionTag}` - ].filter((candidate, index, list) => list.indexOf(candidate) === index); - }; - const resolveReadableStoragePath = async (storageKey) => { - let lastError = null; - for (const candidate of resolveStoragePathCandidates(storageKey)) { - const absolutePath = absoluteStoragePath2(candidate); - try { - await fs18.stat(absolutePath); - return absolutePath; - } catch (error) { - if (error?.code === "ENOENT") { - lastError = error; - continue; - } - throw error; - } - } - throw lastError ?? Object.assign(new Error("\u5B58\u50A8\u6587\u4EF6\u4E0D\u5B58\u5728"), { code: "storage_not_found" }); - }; - const loadVersion = async (userId, pageId, pageVersionId) => { - const [rows] = await pool2.query( - `SELECT p.id AS page_id, p.title, p.summary, p.page_type, p.template_id, p.current_version_id, - p.source_session_id, p.source_message_id, - p.user_id, p.space_id, pv.id AS page_version_id, pv.version_no, - pv.bundle_asset_id, av.storage_key, - JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path - FROM h5_page_records p - JOIN h5_page_versions pv ON pv.page_id = p.id - JOIN h5_asset_versions av ON av.asset_id = pv.content_asset_id AND av.version_no = 1 - WHERE p.id = ? AND p.user_id = ? AND pv.id = ? AND p.status <> 'deleted' - LIMIT 1`, - [pageId, userId, pageVersionId] - ); - const row = rows[0]; - if (!row) throw publicationError("\u9875\u9762\u7248\u672C\u4E0D\u5B58\u5728", "page_not_found"); - return { - ...row, - version_no: Number(row.version_no), - content: await fs18.readFile(await resolveReadableStoragePath(row.storage_key), "utf8") - }; - }; - const loadOwnerSlug = async (userId) => { - const [rows] = await pool2.query( - `SELECT COALESCE(slug, username) AS public_slug - FROM h5_users - WHERE id = ? - LIMIT 1`, - [userId] - ); - const ownerSlug = String(rows[0]?.public_slug ?? "").trim(); - if (!ownerSlug) throw publicationError("\u7528\u6237\u516C\u5F00\u5730\u5740\u4E0D\u5B58\u5728", "publication_owner_not_found"); - return ownerSlug; - }; - const preparePublishContent = async (page, ownerSlug, urlSlug) => { - let publishContent = page.content; - if (page.page_type !== "html") return publishContent; - const publishDir = h5Root ? resolvePublishDir(h5Root, { id: page.user_id }) : null; - return prepareHtmlPublishContent({ - pool: pool2, - userId: page.user_id, - html: publishContent, - ownerSlug, - urlSlug, - pageTitle: page.title, - htmlRelativePath: inferWorkspaceHtmlRelativePath({ - snapshotRelativePath: page.source_relative_path, - pageTitle: page.title, - htmlContent: publishContent, - publishDir - }), - absoluteStoragePath: absoluteStoragePath2, - h5Root, - imgproxySigner: imgproxySigner ? { buildUrl: (path35, preset) => imgproxySigner.buildUrl(imgproxySigner.baseUrl, path35, preset) } : null - }); - }; - const persistScan = async (conn, userId, pageVersionId, scan, now) => { - const scanId = idFactory(); - await conn.query( - `INSERT INTO h5_security_scans - (id, user_id, target_type, target_id, scanner_version, status, risk_level, - findings_count, summary_json, started_at, completed_at) - VALUES (?, ?, 'page_version', ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - scanId, - userId, - pageVersionId, - SCANNER_VERSION, - scan.status, - scan.riskLevel, - scan.findings.length, - JSON.stringify({ types: scan.findings.map((finding) => finding.type) }), - now, - now - ] - ); - for (const finding of scan.findings) { - await conn.query( - `INSERT INTO h5_security_findings - (id, scan_id, finding_type, risk_level, occurrence_count, sample_masked, blocking, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [ - idFactory(), - scanId, - finding.type, - finding.riskLevel, - finding.occurrenceCount, - finding.sampleMasked, - finding.blocking ? 1 : 0, - now - ] - ); - } - return scanId; - }; - const check = async (userId, pageId, input) => { - const slug = normalizeSlug(input.urlSlug); - const accessMode = normalizeAccessMode(input.accessMode); - normalizePassword(input.password, accessMode === "password"); - const expiresAt = normalizeExpiresAt( - input.expiresAt, - accessMode === "time_limited" - ); - const page = await loadVersion(userId, pageId, input.pageVersionId); - if (page.page_version_id !== page.current_version_id) { - throw publicationError("\u53EA\u80FD\u53D1\u5E03\u9875\u9762\u7684\u5F53\u524D\u7248\u672C", "invalid_state_transition"); - } - const [conflicts] = await pool2.query( - `SELECT pr.id, pr.page_id, p.title - FROM h5_publish_records pr - JOIN h5_page_records p ON p.id = pr.page_id - WHERE pr.user_id = ? AND pr.url_slug = ? AND pr.status = 'online' - AND pr.page_id <> ? LIMIT 1`, - [userId, slug, pageId] - ); - const conflict = conflicts[0] ?? null; - const ownerSlug = await loadOwnerSlug(userId); - const publishContent = await preparePublishContent(page, ownerSlug, slug); - const scan = scanContent(`${page.title} -${page.summary ?? ""} -${publishContent}`, { - format: page.page_type === "html" ? "html" : "text", - allowHtmlActiveContent: page.page_type === "html" - }); - const suggestedUrlSlug = conflict && accessMode !== "private_link" ? await findAvailableSlug(pool2, userId, slug, pageId) : null; - return { - pageVersionId: page.page_version_id, - urlSlug: slug, - accessMode, - expiresAt, - slugAvailable: accessMode === "private_link" || !conflict, - slugConflict: conflict ? { pageId: conflict.page_id, title: conflict.title } : null, - suggestedUrlSlug, - ...scan, - allowed: (accessMode === "private_link" || !conflict) && scan.allowed - }; - }; - const publish = async (userId, pageId, input) => { - const result = await check(userId, pageId, input); - if (!result.slugAvailable) throw publicationError("\u9875\u9762\u5730\u5740\u5DF2\u88AB\u4F7F\u7528", "slug_conflict"); - if (!result.allowed) { - throw publicationError("\u9875\u9762\u5305\u542B\u4E0D\u53EF\u516C\u5F00\u7684\u9AD8\u98CE\u9669\u4FE1\u606F", "security_risk_blocked", { - findings: result.findings - }); - } - const acknowledged = new Set(input.acknowledgedFindingIds ?? []); - const missingAcknowledgements = result.findings.filter( - (finding) => !finding.blocking && !acknowledged.has(finding.id) - ); - if (missingAcknowledgements.length) { - throw publicationError("\u8BF7\u5148\u786E\u8BA4\u53D1\u5E03\u68C0\u67E5\u4E2D\u7684\u98CE\u9669\u63D0\u793A", "security_ack_required", { - findings: missingAcknowledgements - }); - } - const page = await loadVersion(userId, pageId, result.pageVersionId); - const ownerSlug = await loadOwnerSlug(userId); - const publishContent = await preparePublishContent(page, ownerSlug, result.urlSlug); - const html = pageInternals.renderPublicationHtml({ ...page, content: publishContent }); - const htmlBytes = Buffer.byteLength(html); - const conn = await pool2.getConnection(); - let writtenPath; - try { - await conn.beginTransaction(); - const [spaces] = await conn.query( - `SELECT quota_bytes, used_bytes, reserved_bytes FROM h5_user_spaces - WHERE id = ? AND user_id = ? FOR UPDATE`, - [page.space_id, userId] - ); - const available = Number(spaces[0]?.quota_bytes ?? 0) - Number(spaces[0]?.used_bytes ?? 0) - Number(spaces[0]?.reserved_bytes ?? 0); - if (available < htmlBytes) { - throw publicationError("\u5269\u4F59\u7A7A\u95F4\u4E0D\u8DB3", "quota_exceeded", { - requiredBytes: htmlBytes, - availableBytes: Math.max(0, available) - }); - } - const [publicationUsage] = await conn.query( - `SELECT COUNT(DISTINCT page_id) AS public_page_used, - MAX(CASE WHEN page_id = ? THEN 1 ELSE 0 END) AS page_already_online - FROM h5_publish_records - WHERE user_id = ? AND status = 'online'`, - [pageId, userId] - ); - const publicPageLimit = await resolvePublicPageLimit(); - if (!Number(publicationUsage[0]?.page_already_online) && Number(publicationUsage[0]?.public_page_used) >= publicPageLimit) { - throw publicationError("\u516C\u5F00\u9875\u9762\u6570\u91CF\u5DF2\u8FBE\u5230\u5957\u9910\u4E0A\u9650", "public_page_limit_exceeded", { - limit: publicPageLimit - }); - } - const [categories] = await conn.query( - `SELECT id FROM h5_space_categories - WHERE user_id = ? AND space_id = ? AND category_code = 'public' LIMIT 1`, - [userId, page.space_id] - ); - if (!categories[0]) throw publicationError("\u516C\u5F00\u5206\u7C7B\u4E0D\u5B58\u5728", "category_not_found"); - const now = Date.now(); - const scanId = await persistScan(conn, userId, page.page_version_id, result, now); - const bundleAssetId = idFactory(); - const assetVersionId = idFactory(); - const publishId = idFactory(); - const privateToken = result.accessMode === "private_link" ? crypto13.randomBytes(24).toString("base64url") : null; - const tokenHash = privateToken ? crypto13.createHash("sha256").update(privateToken).digest("hex") : null; - const passwordHash = result.accessMode === "password" ? hashPassword(input.password) : null; - const storageKey = path22.posix.join( - "users", - userId, - "publications", - publishId, - "index.html" - ); - writtenPath = absoluteStoragePath2(storageKey); - await fs18.mkdir(path22.dirname(writtenPath), { recursive: true }); - await fs18.writeFile(writtenPath, html, { flag: "wx" }); - const checksum = crypto13.createHash("sha256").update(html).digest("hex"); - const publicBaseUrl = resolvePublicBaseUrl(); - const publicUrl2 = privateToken ? `/s/${privateToken}` : `${publicBaseUrl}/u/${encodeURIComponent(ownerSlug)}/pages/${result.urlSlug}`; - await conn.query( - `INSERT INTO h5_assets - (id, user_id, space_id, category_id, asset_type, mime_type, original_filename, - display_name, current_version_id, size_bytes, checksum, risk_level, visibility, - status, source_type, created_at, updated_at) - VALUES (?, ?, ?, ?, 'html', 'text/html', 'index.html', ?, ?, ?, ?, ?, - 'public_candidate', 'ready', 'generated', ?, ?)`, - [ - bundleAssetId, - userId, - page.space_id, - categories[0].id, - `${page.title} \xB7 \u53D1\u5E03\u5FEB\u7167`, - assetVersionId, - htmlBytes, - checksum, - result.riskLevel, - now, - now - ] - ); - await conn.query( - `INSERT INTO h5_asset_versions - (id, asset_id, version_no, storage_key, size_bytes, checksum, mime_type, - created_by, change_note, scan_status, created_at) - VALUES (?, ?, 1, ?, ?, ?, 'text/html', ?, '\u53D1\u5E03\u9875\u9762\u5FEB\u7167', ?, ?)`, - [ - assetVersionId, - bundleAssetId, - storageKey, - htmlBytes, - checksum, - userId, - result.status === "passed" ? "passed" : "warned", - now - ] - ); - const [previousRows] = await conn.query( - `SELECT id, page_version_id FROM h5_publish_records - WHERE user_id = ? AND page_id = ? - ORDER BY published_at DESC LIMIT 1 FOR UPDATE`, - [userId, pageId] - ); - await conn.query( - `UPDATE h5_publish_records SET status = 'offline', offline_at = ?, updated_at = ? - WHERE user_id = ? AND page_id = ? AND status = 'online'`, - [now, now, userId, pageId] - ); - await conn.query( - `UPDATE h5_page_versions - SET immutable = 1, bundle_asset_id = ?, security_scan_id = ? - WHERE id = ? AND page_id = ?`, - [bundleAssetId, scanId, page.page_version_id, pageId] - ); - await conn.query( - `INSERT INTO h5_publish_records - (id, user_id, page_id, page_version_id, publish_type, url_slug, public_url, - access_mode, password_hash, token_hash, token_prefix, expires_at, user_confirmed_at, published_at, - status, view_count, security_scan_id, created_at, updated_at) - VALUES (?, ?, ?, ?, 'page', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'online', 0, ?, ?, ?)`, - [ - publishId, - userId, - pageId, - page.page_version_id, - result.urlSlug, - publicUrl2, - result.accessMode, - passwordHash, - tokenHash, - privateToken?.slice(0, 8) ?? null, - result.expiresAt, - null, - now, - scanId, - now, - now - ] - ); - await conn.query( - `INSERT INTO h5_publication_events - (id, publish_id, event_type, actor_id, old_page_version_id, new_page_version_id, - access_mode, detail_json, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - idFactory(), - publishId, - previousRows.length ? "republished" : "published", - userId, - previousRows[0]?.page_version_id ?? null, - page.page_version_id, - result.accessMode, - JSON.stringify({ findings: result.findings.map((finding) => finding.id) }), - now - ] - ); - await conn.query( - `UPDATE h5_page_records - SET current_publish_id = ?, status = ?, visibility = ?, updated_at = ? - WHERE id = ? AND user_id = ?`, - [ - publishId, - result.accessMode === "public" ? "published" : "protected", - result.accessMode === "public" ? "public" : "private", - now, - pageId, - userId - ] - ); - await conn.query( - `UPDATE h5_user_spaces SET used_bytes = used_bytes + ?, updated_at = ? - WHERE id = ? AND user_id = ?`, - [htmlBytes, now, page.space_id, userId] - ); - const assetUrlPattern = /\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)/gi; - const assetIds = /* @__PURE__ */ new Set(); - let match; - while ((match = assetUrlPattern.exec(html)) !== null) { - assetIds.add(match[1]); - } - if (assetIds.size > 0) { - const refValues = Array.from(assetIds).map((assetId) => [publishId, assetId, now]); - await conn.query( - `INSERT IGNORE INTO h5_publication_asset_refs - (publication_id, asset_id, created_at) VALUES ` + refValues.map(() => "(?, ?, ?)").join(","), - refValues.flat() - ); - } - await conn.commit(); - const publication = { - ...publicationResponse({ - id: publishId, - page_id: pageId, - page_version_id: page.page_version_id, - url_slug: result.urlSlug, - public_url: publicUrl2, - access_mode: result.accessMode, - status: "online", - view_count: 0, - published_at: now, - offline_at: null, - expires_at: result.expiresAt - }), - findings: result.findings - }; - await registerPublicationArtifactForConversation({ - registry: conversationPackageRegistry, - userId, - page, - publication, - bundleAssetId, - storageKey, - htmlBytes, - html: page.content, - publishDir: h5Root ? resolvePublishDir(h5Root, { id: userId }) : null, - now - }); - return publication; - } catch (error) { - await conn.rollback(); - if (writtenPath) await fs18.rm(writtenPath, { force: true }).catch(() => { - }); - throw error; - } finally { - conn.release(); - } - }; - const getCurrent = async (userId, pageId) => { - const [rows] = await pool2.query( - `SELECT pr.* FROM h5_publish_records pr - JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id - WHERE pr.page_id = ? AND pr.user_id = ? AND pr.status = 'online' - ORDER BY pr.published_at DESC LIMIT 1`, - [pageId, userId] - ); - return publicationResponse(rows[0]); - }; - const updatePublicationStatus = async (userId, publicationId, { accessMode, expiresAt }) => { - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [rows] = await conn.query( - `SELECT * FROM h5_publish_records - WHERE id = ? AND user_id = ? AND status = 'online' LIMIT 1 FOR UPDATE`, - [publicationId, userId] - ); - const publication = rows[0]; - if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); - const normalizedMode = normalizeAccessMode(accessMode); - const normalizedExpiresAt = normalizeExpiresAt(expiresAt, normalizedMode === "time_limited"); - const now = Date.now(); - await conn.query( - `UPDATE h5_publish_records - SET access_mode = ?, expires_at = ?, user_confirmed_at = ?, updated_at = ? - WHERE id = ? AND user_id = ?`, - [normalizedMode, normalizedExpiresAt, now, now, publicationId, userId] - ); - await conn.commit(); - const updated = { - ...publication, - access_mode: normalizedMode, - expires_at: normalizedExpiresAt, - user_confirmed_at: now, - updated_at: now - }; - return publicationResponse(updated); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - }; - const offline = async (userId, publicationId) => { - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [rows] = await conn.query( - `SELECT * FROM h5_publish_records - WHERE id = ? AND user_id = ? AND status = 'online' LIMIT 1 FOR UPDATE`, - [publicationId, userId] - ); - const publication = rows[0]; - if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); - const now = Date.now(); - await conn.query( - `UPDATE h5_publish_records SET status = 'offline', offline_at = ?, updated_at = ? - WHERE id = ?`, - [now, now, publicationId] - ); - await conn.query( - `UPDATE h5_page_records - SET current_publish_id = NULL, status = 'offline', visibility = 'private', updated_at = ? - WHERE id = ? AND user_id = ? AND current_publish_id = ?`, - [now, publication.page_id, userId, publicationId] - ); - await conn.query( - `INSERT INTO h5_publication_events - (id, publish_id, event_type, actor_id, old_page_version_id, new_page_version_id, - access_mode, detail_json, created_at) - VALUES (?, ?, 'offlined', ?, ?, NULL, ?, '{}', ?)`, - [ - idFactory(), - publicationId, - userId, - publication.page_version_id, - publication.access_mode, - now - ] - ); - await conn.query( - `UPDATE plaza_posts SET status = 'hidden', updated_at = ? - WHERE publication_id = ? AND status != 'hidden'`, - [now, publicationId] - ); - await conn.commit(); - return { id: publicationId, status: "offline", offlineAt: now }; - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - }; - const resolveRow = async (row, viewerId, password, requestMeta = {}) => { - if (!row) throw publicationError("\u516C\u5F00\u9875\u9762\u4E0D\u5B58\u5728", "publication_not_found"); - if (row.expires_at && Number(row.expires_at) <= Date.now()) { - await pool2.query( - `UPDATE h5_publish_records SET status = 'expired', updated_at = ? WHERE id = ?`, - [Date.now(), row.id] - ); - throw publicationError("\u516C\u5F00\u9875\u9762\u5DF2\u8FC7\u671F", "publication_not_found"); - } - if (row.access_mode === "owner_only" && row.owner_id !== viewerId) { - throw publicationError("\u516C\u5F00\u9875\u9762\u4E0D\u5B58\u5728", "publication_not_found"); - } - if (row.access_mode === "login_required" && !viewerId) { - throw publicationError("\u767B\u5F55\u540E\u624D\u80FD\u8BBF\u95EE\u6B64\u9875\u9762", "publication_login_required"); - } - if (row.access_mode === "password" && !verifyPassword2(password, row.password_hash)) { - throw publicationError("\u9700\u8981\u8BBF\u95EE\u5BC6\u7801", "publication_password_required"); - } - const now = Date.now(); - await pool2.query( - `UPDATE h5_publish_records SET view_count = view_count + 1, updated_at = ? WHERE id = ?`, - [now, row.id] - ); - await pool2.query( - `INSERT INTO h5_publication_views - (id, publish_id, viewer_user_id, referrer_host, device_type, viewed_at) - VALUES (?, ?, ?, ?, ?, ?)`, - [ - idFactory(), - row.id, - viewerId ?? null, - referrerHost(requestMeta.referrer), - deviceType(requestMeta.userAgent), - now - ] - ); - const html = await fs18.readFile(await resolveReadableStoragePath(row.storage_key), "utf8"); - return { - html: await refreshPublishedHtmlDownloadLinks(row, html), - publication: publicationResponse({ ...row, view_count: Number(row.view_count) + 1 }), - ownerId: row.owner_id, - pageSource: { - pageId: row.page_id, - title: row.title, - sourceSessionId: row.source_session_id ?? null, - sourceMessageId: row.source_message_id ?? null - } - }; - }; - const refreshPublishedHtmlDownloadLinks = async (row, html) => { - if (!h5Root || !row?.owner_id) return html; - const source = String(html ?? ""); - if (!source.trim()) return html; - const publishDir = resolvePublishDir(h5Root, { id: row.owner_id }); - const linkIndex = await loadWorkspaceDownloadLinkIndex(pool2, row.owner_id); - return rewriteRelativeDownloadLinks(html, { - htmlRelativePath: inferWorkspaceHtmlRelativePath({ - pageTitle: row.title, - htmlContent: html, - publishDir - }), - publishDir, - linkIndex, - publishKey: row.owner_id, - publicBaseUrl: resolvePublicBaseUrl(), - preferAssetDownload: false - }).html; - }; - const resolvePublic = async (ownerSlug, urlSlug, viewerId, password, requestMeta) => { - const [rows] = await pool2.query( - `SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id, av.storage_key - FROM h5_publish_records pr - JOIN h5_users u ON u.id = pr.user_id - JOIN h5_page_records p ON p.id = pr.page_id - JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.immutable = 1 - JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1 - WHERE COALESCE(u.slug, u.username) = ? AND pr.url_slug = ? AND pr.status = 'online' - AND pr.access_mode <> 'private_link' - ORDER BY pr.published_at DESC LIMIT 1`, - [ownerSlug, urlSlug] - ); - return resolveRow(rows[0], viewerId, password, requestMeta); - }; - const resolvePrivateLink = async (token, viewerId, requestMeta) => { - const tokenHash = crypto13.createHash("sha256").update(String(token)).digest("hex"); - const [rows] = await pool2.query( - `SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id, av.storage_key - FROM h5_publish_records pr - JOIN h5_users u ON u.id = pr.user_id - JOIN h5_page_records p ON p.id = pr.page_id - JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.immutable = 1 - JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1 - WHERE pr.token_hash = ? AND pr.status = 'online' AND pr.access_mode = 'private_link' - LIMIT 1`, - [tokenHash] - ); - return resolveRow(rows[0], viewerId, null, requestMeta); - }; - const getStats = async (userId, publicationId) => { - const [publications] = await pool2.query( - `SELECT id, view_count, published_at, status - FROM h5_publish_records WHERE id = ? AND user_id = ? LIMIT 1`, - [publicationId, userId] - ); - const publication = publications[0]; - if (!publication) throw publicationError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); - const since = Date.now() - 30 * 24 * 60 * 60 * 1e3; - const [daily] = await pool2.query( - `SELECT DATE_FORMAT(FROM_UNIXTIME(viewed_at / 1000), '%Y-%m-%d') AS day, - COUNT(*) AS views - FROM h5_publication_views - WHERE publish_id = ? AND viewed_at >= ? - GROUP BY day ORDER BY day`, - [publicationId, since] - ); - const [devices] = await pool2.query( - `SELECT device_type, COUNT(*) AS views - FROM h5_publication_views WHERE publish_id = ? - GROUP BY device_type ORDER BY views DESC`, - [publicationId] - ); - const [sources] = await pool2.query( - `SELECT COALESCE(referrer_host, 'direct') AS source, COUNT(*) AS views - FROM h5_publication_views WHERE publish_id = ? - GROUP BY source ORDER BY views DESC LIMIT 10`, - [publicationId] - ); - return { - publicationId, - status: publication.status, - totalViews: Number(publication.view_count), - publishedAt: Number(publication.published_at), - daily: daily.map((row) => ({ day: row.day, views: Number(row.views) })), - devices: devices.map((row) => ({ - deviceType: row.device_type, - views: Number(row.views) - })), - sources: sources.map((row) => ({ source: row.source, views: Number(row.views) })) - }; - }; - const getPublicHomepage = async (ownerSlug) => { - const [owners] = await pool2.query( - `SELECT id, username, COALESCE(slug, username) AS slug, display_name - FROM h5_users - WHERE COALESCE(slug, username) = ? - LIMIT 1`, - [ownerSlug] - ); - const owner = owners[0]; - if (!owner) throw publicationError("\u516C\u5F00\u4E3B\u9875\u4E0D\u5B58\u5728", "publication_not_found"); - const [rows] = await pool2.query( - `SELECT pr.id, pr.page_id, pr.url_slug, pr.public_url, pr.view_count, pr.published_at, - p.title, p.summary, p.template_id - FROM h5_publish_records pr - JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id - WHERE pr.user_id = ? AND pr.status = 'online' AND pr.access_mode = 'public' - ORDER BY pr.published_at DESC - LIMIT 48`, - [owner.id] - ); - return publicHomepageResponse(owner, rows); - }; - const cleanupExpiredUnconfirmedPublications = async (now = Date.now()) => { - const [result] = await pool2.query( - `UPDATE h5_publish_records - SET access_mode = 'private', expires_at = NULL, updated_at = ? - WHERE access_mode = 'public' - AND expires_at IS NOT NULL - AND expires_at <= ? - AND user_confirmed_at IS NULL - AND status = 'online'`, - [now, now] - ); - return { cleaned: result.affectedRows }; - }; - return { - check, - publish, - getCurrent, - getPublicHomepage, - getStats, - offline, - updatePublicationStatus, - resolvePublic, - resolvePrivateLink, - cleanupExpiredUnconfirmedPublications - }; -} -async function registerPublicationArtifactForConversation({ - registry, - userId, - page, - publication, - bundleAssetId, - storageKey, - htmlBytes, - html, - publishDir, - now -}) { - const sessionId = String(page?.source_session_id ?? "").trim(); - if (!registry || !sessionId || !publication?.id) return null; - try { - const packageRecord = await registry.ensurePackage({ - userId, - sessionId, - title: page.title, - now - }); - const artifact = await registry.recordArtifact({ - id: `ca_${publication.id}`, - packageId: packageRecord.id, - artifactKind: "public_html", - role: "assistant", - assetId: bundleAssetId, - pageId: page.page_id, - publicationId: publication.id, - messageId: page.source_message_id ?? null, - displayName: `${page.title}.html`, - mimeType: "text/html", - sizeBytes: htmlBytes, - storageKey, - canonicalUrl: publication.publicUrl, - sortOrder: Number.isFinite(Number(publication.publishedAt)) ? Number(publication.publishedAt) : 0, - now - }); - const htmlRelativePath = page.source_relative_path ?? null; - const companionArtifacts = await collectPublicationCompanionArtifacts({ - publishDir, - html, - htmlRelativePath - }); - await Promise.all( - companionArtifacts.map( - (companion, index) => registry.recordArtifact({ - id: `ca_${publication.id}_${crypto13.createHash("sha256").update(companion.workspacePath).digest("hex").slice(0, 12)}`, - packageId: packageRecord.id, - artifactKind: companionKindForWorkspacePath(companion.workspacePath), - role: "assistant", - pageId: page.page_id, - publicationId: publication.id, - messageId: page.source_message_id ?? null, - displayName: path22.posix.basename(companion.workspacePath), - mimeType: companionMimeForWorkspacePath(companion.workspacePath), - sizeBytes: companion.sizeBytes, - canonicalUrl: buildPublicUrl(resolvePublicBaseUrl(), userId, companion.workspacePath), - sortOrder: (Number.isFinite(Number(publication.publishedAt)) ? Number(publication.publishedAt) : 0) + index + 1, - now - }) - ) - ); - await registry.writeManifestForSession({ userId, sessionId }); - return artifact; - } catch (error) { - console.warn( - "[MindSpace] conversation package publication artifact registration failed:", - error instanceof Error ? error.message : error - ); - return null; - } -} -var publicationInternals = { - normalizeSlug, - normalizeAccessMode, - normalizePassword, - normalizeExpiresAt, - hashPassword, - verifyPassword: verifyPassword2, - deviceType, - referrerHost, - publicHomepageResponse, - buildPublicationThumbnailFallback, - replaceImgproxyStandardImageReferences, - workspacePublicAssetRelativeUrl, - rewriteWorkspacePublicAssetReferences, - localizePrivateImageReferences, - collectPublicationCompanionArtifacts, - prepareHtmlPublishContent, - registerPublicationArtifactForConversation, - scanContent - // re-exported from mindspace-content-scan.mjs -}; - -// mindspace-conversation-package-backfill.mjs -var BACKFILL_LIMIT = 200; -function asNumber5(value) { - const number = Number(value); - return Number.isFinite(number) ? number : null; -} -async function readUtf8IfExists(storageRoot, storageKey) { - if (!storageRoot || !storageKey) return ""; - try { - return await fs19.readFile(path23.resolve(storageRoot, storageKey), "utf8"); - } catch { - return ""; - } -} -async function backfillConversationPackageArtifacts({ - pool: pool2, - registry, - storageRoot, - h5Root, - user, - sessionId, - now = Date.now() -} = {}) { - const userId = String(user?.id ?? "").trim(); - const normalizedSessionId = String(sessionId ?? "").trim(); - if (!pool2 || !registry || !userId || !normalizedSessionId) { - return { pages: 0, publications: 0, uploads: 0 }; - } - let pages = 0; - const [pageRows] = await pool2.query( - `SELECT p.id, p.title, p.source_message_id, p.current_version_id, - pv.version_no, pv.content_asset_id, - av.mime_type, av.size_bytes, av.storage_key - FROM h5_page_records p - LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id AND pv.page_id = p.id - LEFT JOIN h5_asset_versions av ON av.asset_id = pv.content_asset_id AND av.version_no = 1 - WHERE p.user_id = ? AND p.source_session_id = ? AND p.status <> 'deleted' - ORDER BY p.updated_at DESC - LIMIT ?`, - [userId, normalizedSessionId, BACKFILL_LIMIT] - ); - for (const row of pageRows) { - const artifact = await pageInternals.registerPageArtifactForConversation({ - registry, - userId, - source: { sessionId: normalizedSessionId, messageId: row.source_message_id ?? null }, - page: { - id: row.id, - title: row.title, - currentVersionId: row.current_version_id, - sourceMessageId: row.source_message_id ?? null - }, - contentAssetId: row.content_asset_id ?? null, - contentMimeType: row.mime_type ?? null, - contentBytes: asNumber5(row.size_bytes), - storageKey: row.storage_key ?? null, - versionNo: row.version_no, - now - }); - if (artifact) pages += 1; - } - let publications = 0; - const [publicationRows] = await pool2.query( - `SELECT pr.id, pr.page_id, pr.public_url, pr.published_at, - p.title, p.source_message_id, - pv.bundle_asset_id, - JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path, - av.storage_key, av.size_bytes - FROM h5_publish_records pr - JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id - JOIN h5_page_versions pv ON pv.id = pr.page_version_id AND pv.page_id = p.id - LEFT JOIN h5_asset_versions av ON av.asset_id = pv.bundle_asset_id AND av.version_no = 1 - WHERE pr.user_id = ? AND p.source_session_id = ? AND pr.status IN ('online', 'offline') - ORDER BY pr.published_at DESC - LIMIT ?`, - [userId, normalizedSessionId, BACKFILL_LIMIT] - ); - const publishDir = h5Root ? resolvePublishDir(h5Root, { id: userId }) : null; - for (const row of publicationRows) { - const artifact = await publicationInternals.registerPublicationArtifactForConversation({ - registry, - userId, - page: { - page_id: row.page_id, - title: row.title, - source_session_id: normalizedSessionId, - source_message_id: row.source_message_id ?? null, - source_relative_path: row.source_relative_path ?? null - }, - publication: { - id: row.id, - publicUrl: row.public_url, - publishedAt: row.published_at - }, - bundleAssetId: row.bundle_asset_id ?? null, - storageKey: row.storage_key ?? null, - htmlBytes: asNumber5(row.size_bytes), - html: await readUtf8IfExists(storageRoot, row.storage_key), - publishDir, - now - }); - if (artifact) publications += 1; - } - let uploads = 0; - const [uploadRows] = await pool2.query( - `SELECT u.id, u.filename, u.source_message_id, u.completed_asset_id, - u.detected_mime_type, u.actual_size, - a.mime_type, a.original_filename, a.display_name, a.size_bytes, - v.storage_key - FROM h5_upload_sessions u - JOIN h5_assets a ON a.id = u.completed_asset_id AND a.user_id = u.user_id - LEFT JOIN h5_asset_versions v ON v.id = a.current_version_id AND v.asset_id = a.id - WHERE u.user_id = ? AND u.source_session_id = ? AND u.status = 'completed' - ORDER BY COALESCE(u.completed_at, u.created_at) DESC - LIMIT ?`, - [userId, normalizedSessionId, BACKFILL_LIMIT] - ); - for (const row of uploadRows) { - const artifact = await assetInternals.registerUploadArtifactForConversation({ - registry, - userId, - upload: { - filename: row.filename, - source_session_id: normalizedSessionId, - source_message_id: row.source_message_id ?? null, - detected_mime_type: row.detected_mime_type ?? row.mime_type ?? null, - actual_size: row.actual_size ?? row.size_bytes ?? null - }, - asset: { - id: row.completed_asset_id, - mimeType: row.mime_type ?? row.detected_mime_type ?? null, - filename: row.original_filename ?? row.filename, - displayName: row.display_name ?? row.filename, - sizeBytes: asNumber5(row.size_bytes ?? row.actual_size) - }, - assetId: row.completed_asset_id, - storageKey: row.storage_key ?? null, - canonicalUrl: `/api/mindspace/v1/assets/${encodeURIComponent(row.completed_asset_id)}/download`, - now - }); - if (artifact) uploads += 1; - } - return { pages, publications, uploads }; -} - -// mindspace-conversation-package.mjs -var PACKAGE_ID_PREFIX = "cp_"; -var ARTIFACT_ID_PREFIX = "ca_"; -var CONVERSATION_PACKAGE_SCHEMA_VERSION = 1; -var CONVERSATION_ARTIFACT_KINDS = Object.freeze([ - "input_image", - "input_file", - "generated_image", - "generated_file", - "page", - "public_html", - "long_image", - "thumbnail", - "docx", - "pdf" -]); -var MYSQL_SIGNED_INT_MAX = 2147483647; -var MYSQL_SIGNED_INT_MIN = -2147483648; -function requiredString(value, field) { - const normalized = String(value ?? "").trim(); - if (!normalized) { - throw Object.assign(new Error(`${field} is required`), { - code: "invalid_conversation_package", - field - }); - } - return normalized; -} -function optionalString(value) { - const normalized = String(value ?? "").trim(); - return normalized || null; -} -function optionalNumber(value) { - if (value == null || value === "") return null; - const number = Number(value); - return Number.isFinite(number) ? number : null; -} -function safeSortOrder(value) { - const number = Number(value); - if (!Number.isFinite(number)) return 0; - let normalized = Math.trunc(number); - if (normalized > MYSQL_SIGNED_INT_MAX && Math.abs(normalized) >= 1e12) { - normalized = Math.trunc(normalized / 1e3); - } - if (normalized > MYSQL_SIGNED_INT_MAX) return MYSQL_SIGNED_INT_MAX; - if (normalized < MYSQL_SIGNED_INT_MIN) return MYSQL_SIGNED_INT_MIN; - return normalized; -} -function safeStorageSegment(value, field) { - const normalized = requiredString(value, field); - const encoded = encodeURIComponent(normalized); - if (encoded.includes("%2F") || encoded.includes("%5C")) { - throw Object.assign(new Error(`${field} must not contain path separators`), { - code: "invalid_storage_segment", - field - }); - } - return encoded; -} -function buildConversationStoragePrefix({ userId, sessionId }) { - return `users/${safeStorageSegment(userId, "userId")}/conversations/${safeStorageSegment( - sessionId, - "sessionId" - )}`; -} -function buildConversationPackageUri({ userId, sessionId }) { - return `mindspace://users/${safeStorageSegment(userId, "userId")}/conversations/${safeStorageSegment( - sessionId, - "sessionId" - )}`; -} -function createConversationPackageRecord(input) { - const userId = requiredString(input?.userId, "userId"); - const sessionId = requiredString(input?.sessionId, "sessionId"); - const now = Number.isFinite(Number(input?.now)) ? Number(input.now) : Date.now(); - const id = optionalString(input?.id) ?? `${PACKAGE_ID_PREFIX}${sessionId}`; - const storagePrefix = optionalString(input?.storagePrefix) ?? buildConversationStoragePrefix({ userId, sessionId }); - return { - id, - userId, - sessionId, - title: optionalString(input?.title), - status: optionalString(input?.status) ?? "active", - storagePrefix, - manifestAssetId: optionalString(input?.manifestAssetId), - createdAt: Number.isFinite(Number(input?.createdAt)) ? Number(input.createdAt) : now, - updatedAt: Number.isFinite(Number(input?.updatedAt)) ? Number(input.updatedAt) : now - }; -} -function assertConversationArtifactKind(kind) { - const normalized = requiredString(kind, "artifactKind"); - if (!CONVERSATION_ARTIFACT_KINDS.includes(normalized)) { - throw Object.assign(new Error(`unsupported conversation artifact kind: ${normalized}`), { - code: "unsupported_conversation_artifact_kind", - field: "artifactKind" - }); - } - return normalized; -} -function createConversationArtifact(input) { - const packageId = requiredString(input?.packageId, "packageId"); - const artifactKind = assertConversationArtifactKind(input?.artifactKind ?? input?.kind); - const now = Number.isFinite(Number(input?.now)) ? Number(input.now) : Date.now(); - const id = optionalString(input?.id) ?? `${ARTIFACT_ID_PREFIX}${packageId}_${artifactKind}_${now}`; - return { - id, - packageId, - artifactKind, - role: optionalString(input?.role), - assetId: optionalString(input?.assetId), - pageId: optionalString(input?.pageId), - publicationId: optionalString(input?.publicationId), - agentRunId: optionalString(input?.agentRunId), - messageId: optionalString(input?.messageId), - displayName: optionalString(input?.displayName), - mimeType: optionalString(input?.mimeType), - sizeBytes: optionalNumber(input?.sizeBytes), - storageKey: optionalString(input?.storageKey), - canonicalUrl: optionalString(input?.canonicalUrl), - sortOrder: safeSortOrder(input?.sortOrder), - createdAt: Number.isFinite(Number(input?.createdAt)) ? Number(input.createdAt) : now - }; -} -function artifactManifestEntry(artifact) { - return { - artifactId: artifact.id, - kind: artifact.artifactKind, - ...artifact.role ? { role: artifact.role } : {}, - ...artifact.assetId ? { assetId: artifact.assetId } : {}, - ...artifact.pageId ? { pageId: artifact.pageId } : {}, - ...artifact.publicationId ? { publicationId: artifact.publicationId } : {}, - ...artifact.agentRunId ? { agentRunId: artifact.agentRunId } : {}, - ...artifact.messageId ? { messageId: artifact.messageId } : {}, - ...artifact.displayName ? { displayName: artifact.displayName } : {}, - ...artifact.mimeType ? { mimeType: artifact.mimeType } : {}, - ...artifact.sizeBytes !== null ? { sizeBytes: artifact.sizeBytes } : {}, - ...artifact.storageKey ? { storageKey: artifact.storageKey } : {}, - ...artifact.canonicalUrl ? { canonicalUrl: artifact.canonicalUrl } : {}, - sortOrder: artifact.sortOrder, - createdAt: artifact.createdAt - }; -} -function groupedManifestEntries(artifacts, keyField, idField) { - const groups = /* @__PURE__ */ new Map(); - for (const artifact of artifacts) { - const key = artifact[keyField]; - if (!key) continue; - const existing = groups.get(key) ?? { - [idField]: key, - artifactIds: [], - createdAt: artifact.createdAt, - updatedAt: artifact.createdAt - }; - existing.artifactIds.push(artifact.id); - if (!existing.displayName && artifact.displayName) existing.displayName = artifact.displayName; - if (!existing.canonicalUrl && artifact.canonicalUrl) existing.canonicalUrl = artifact.canonicalUrl; - existing.createdAt = Math.min(existing.createdAt, artifact.createdAt); - existing.updatedAt = Math.max(existing.updatedAt, artifact.createdAt); - groups.set(key, existing); - } - return [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a[idField].localeCompare(b[idField])); -} -function provenanceManifestEntries(artifacts, keyField, idField) { - const groups = /* @__PURE__ */ new Map(); - for (const artifact of artifacts) { - const key = artifact[keyField]; - if (!key) continue; - const existing = groups.get(key) ?? { - [idField]: key, - artifactIds: [], - kinds: [], - createdAt: artifact.createdAt, - updatedAt: artifact.createdAt - }; - existing.artifactIds.push(artifact.id); - if (!existing.kinds.includes(artifact.artifactKind)) existing.kinds.push(artifact.artifactKind); - existing.createdAt = Math.min(existing.createdAt, artifact.createdAt); - existing.updatedAt = Math.max(existing.updatedAt, artifact.createdAt); - groups.set(key, existing); - } - return [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a[idField].localeCompare(b[idField])); -} -function buildConversationPackageManifest({ packageRecord, artifacts = [] }) { - const record = createConversationPackageRecord(packageRecord); - const normalizedArtifacts = artifacts.map((artifact) => createConversationArtifact({ ...artifact, packageId: record.id })).sort((a, b) => a.sortOrder - b.sortOrder || a.createdAt - b.createdAt || a.id.localeCompare(b.id)); - return { - schemaVersion: CONVERSATION_PACKAGE_SCHEMA_VERSION, - packageId: record.id, - userId: record.userId, - sessionId: record.sessionId, - uri: buildConversationPackageUri({ userId: record.userId, sessionId: record.sessionId }), - title: record.title, - status: record.status, - storagePrefix: record.storagePrefix, - manifestAssetId: record.manifestAssetId, - createdAt: record.createdAt, - updatedAt: record.updatedAt, - artifacts: normalizedArtifacts.map(artifactManifestEntry), - pages: groupedManifestEntries(normalizedArtifacts, "pageId", "pageId"), - publications: groupedManifestEntries(normalizedArtifacts, "publicationId", "publicationId"), - messages: provenanceManifestEntries(normalizedArtifacts, "messageId", "messageId"), - agentRuns: provenanceManifestEntries(normalizedArtifacts, "agentRunId", "agentRunId") - }; -} - -// mindspace-conversation-package-store.mjs -function storeError(message, code) { - return Object.assign(new Error(message), { code }); -} -function requirePool(pool2) { - if (!pool2 || typeof pool2.query !== "function") { - throw storeError("MindSpace conversation package store requires a queryable pool", "missing_query_pool"); - } - return pool2; -} -function packageFromRow(row) { - if (!row) return null; - return createConversationPackageRecord({ - id: row.id, - userId: row.user_id, - sessionId: row.session_id, - title: row.title, - status: row.status, - storagePrefix: row.storage_prefix, - manifestAssetId: row.manifest_asset_id, - createdAt: Number(row.created_at), - updatedAt: Number(row.updated_at) - }); -} -function artifactFromRow(row) { - if (!row) return null; - return createConversationArtifact({ - id: row.id, - packageId: row.package_id, - artifactKind: row.artifact_kind, - role: row.role, - assetId: row.asset_id, - pageId: row.page_id, - publicationId: row.publication_id, - agentRunId: row.agent_run_id, - messageId: row.message_id, - displayName: row.display_name, - mimeType: row.mime_type, - sizeBytes: row.size_bytes == null ? null : Number(row.size_bytes), - storageKey: row.storage_key, - canonicalUrl: row.canonical_url, - sortOrder: Number(row.sort_order ?? 0), - createdAt: Number(row.created_at) - }); -} -function createConversationPackageStore(pool2) { - const db = requirePool(pool2); - return { - async upsertPackage(input) { - const record = createConversationPackageRecord(input); - await db.query( - `INSERT INTO h5_conversation_packages - (id, user_id, session_id, title, status, storage_prefix, manifest_asset_id, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - title = VALUES(title), - status = VALUES(status), - storage_prefix = VALUES(storage_prefix), - manifest_asset_id = VALUES(manifest_asset_id), - updated_at = VALUES(updated_at)`, - [ - record.id, - record.userId, - record.sessionId, - record.title, - record.status, - record.storagePrefix, - record.manifestAssetId, - record.createdAt, - record.updatedAt - ] - ); - return record; - }, - async getPackageBySession(userId, sessionId) { - const [rows] = await db.query( - `SELECT id, user_id, session_id, title, status, storage_prefix, manifest_asset_id, created_at, updated_at - FROM h5_conversation_packages - WHERE user_id = ? AND session_id = ? - LIMIT 1`, - [userId, sessionId] - ); - return packageFromRow(rows?.[0]); - }, - async upsertArtifact(input) { - const artifact = createConversationArtifact(input); - await db.query( - `INSERT INTO h5_conversation_artifacts - (id, package_id, artifact_kind, role, asset_id, page_id, publication_id, agent_run_id, - message_id, display_name, mime_type, size_bytes, storage_key, canonical_url, sort_order, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - artifact_kind = VALUES(artifact_kind), - role = VALUES(role), - asset_id = VALUES(asset_id), - page_id = VALUES(page_id), - publication_id = VALUES(publication_id), - agent_run_id = VALUES(agent_run_id), - message_id = VALUES(message_id), - display_name = VALUES(display_name), - mime_type = VALUES(mime_type), - size_bytes = VALUES(size_bytes), - storage_key = VALUES(storage_key), - canonical_url = VALUES(canonical_url), - sort_order = VALUES(sort_order)`, - [ - artifact.id, - artifact.packageId, - artifact.artifactKind, - artifact.role, - artifact.assetId, - artifact.pageId, - artifact.publicationId, - artifact.agentRunId, - artifact.messageId, - artifact.displayName, - artifact.mimeType, - artifact.sizeBytes, - artifact.storageKey, - artifact.canonicalUrl, - artifact.sortOrder, - artifact.createdAt - ] - ); - return artifact; - }, - async listArtifacts(packageId) { - const [rows] = await db.query( - `SELECT id, package_id, artifact_kind, role, asset_id, page_id, publication_id, agent_run_id, - message_id, display_name, mime_type, size_bytes, storage_key, canonical_url, sort_order, created_at - FROM h5_conversation_artifacts - WHERE package_id = ? - ORDER BY sort_order ASC, created_at ASC, id ASC`, - [packageId] - ); - return (rows ?? []).map(artifactFromRow); - }, - async buildManifestForSession({ userId, sessionId }) { - const packageRecord = await this.getPackageBySession(userId, sessionId); - if (!packageRecord) return null; - const artifacts = await this.listArtifacts(packageRecord.id); - return buildConversationPackageManifest({ packageRecord, artifacts }); - } - }; -} - -// mindspace-storage-adapter.mjs -import fs20 from "node:fs"; -import fsp from "node:fs/promises"; -import path24 from "node:path"; -function storageError(message, code, details = {}) { - return Object.assign(new Error(message), { code, ...details }); -} -function normalizeMindSpaceStorageKey(key) { - const raw = String(key ?? "").trim(); - if (!raw) { - throw storageError("storage key is required", "invalid_storage_key"); - } - if (raw.includes("\0")) { - throw storageError("storage key must not contain null bytes", "invalid_storage_key"); - } - if (raw.includes("\\")) { - throw storageError("storage key must use forward slashes", "invalid_storage_key"); - } - if (path24.isAbsolute(raw) || /^[A-Za-z]:\//.test(raw)) { - throw storageError("storage key must be relative", "invalid_storage_key"); - } - if (raw.split("/").includes("..")) { - throw storageError("storage key must not traverse parent directories", "invalid_storage_key"); - } - const normalized = path24.posix.normalize(raw.replace(/^\/+/, "")); - if (normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) { - throw storageError("storage key must not traverse parent directories", "invalid_storage_key"); - } - return normalized; -} -function resolveStoragePath(storageRoot, key) { - const root = path24.resolve(String(storageRoot ?? "")); - if (!root) { - throw storageError("storage root is required", "invalid_storage_root"); - } - const normalizedKey = normalizeMindSpaceStorageKey(key); - const target = path24.resolve(root, ...normalizedKey.split("/")); - if (target !== root && !target.startsWith(`${root}${path24.sep}`)) { - throw storageError("storage key resolves outside storage root", "invalid_storage_key", { - key: normalizedKey - }); - } - return target; -} -async function walkFiles(root, dir, prefix, results) { - const entries = await fsp.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - const absolutePath = path24.join(dir, entry.name); - const relativePath = path24.relative(root, absolutePath).split(path24.sep).join("/"); - if (entry.isDirectory()) { - await walkFiles(root, absolutePath, prefix, results); - } else if (!prefix || relativePath === prefix || relativePath.startsWith(`${prefix}/`)) { - const stat = await fsp.stat(absolutePath); - results.push({ - key: relativePath, - sizeBytes: stat.size, - updatedAt: stat.mtimeMs - }); - } - } -} -function createLocalMindSpaceStorageAdapter(storageRoot) { - const root = path24.resolve(storageRoot); - return { - kind: "local-fs", - root, - async putObject(key, body) { - const target = resolveStoragePath(root, key); - await fsp.mkdir(path24.dirname(target), { recursive: true }); - await fsp.writeFile(target, body); - const stat = await fsp.stat(target); - return { - key: normalizeMindSpaceStorageKey(key), - sizeBytes: stat.size, - updatedAt: stat.mtimeMs - }; - }, - async getObject(key) { - return fsp.readFile(resolveStoragePath(root, key)); - }, - async statObject(key) { - try { - const stat = await fsp.stat(resolveStoragePath(root, key)); - return { - key: normalizeMindSpaceStorageKey(key), - exists: true, - sizeBytes: stat.size, - updatedAt: stat.mtimeMs, - isDirectory: stat.isDirectory() - }; - } catch (error) { - if (error?.code !== "ENOENT") throw error; - return { - key: normalizeMindSpaceStorageKey(key), - exists: false, - sizeBytes: 0, - updatedAt: null, - isDirectory: false - }; - } - }, - async deleteObject(key) { - await fsp.rm(resolveStoragePath(root, key), { force: true }); - return { key: normalizeMindSpaceStorageKey(key), deleted: true }; - }, - async listObjects(prefix = "") { - const normalizedPrefix = prefix ? normalizeMindSpaceStorageKey(prefix).replace(/\/$/, "") : ""; - const results = []; - try { - await walkFiles(root, root, normalizedPrefix, results); - } catch (error) { - if (error?.code !== "ENOENT") throw error; - } - return results.sort((a, b) => a.key.localeCompare(b.key)); - }, - async copyObject(sourceKey, targetKey) { - const source = resolveStoragePath(root, sourceKey); - const target = resolveStoragePath(root, targetKey); - await fsp.mkdir(path24.dirname(target), { recursive: true }); - await fsp.copyFile(source, target); - const stat = await fsp.stat(target); - return { - key: normalizeMindSpaceStorageKey(targetKey), - sourceKey: normalizeMindSpaceStorageKey(sourceKey), - sizeBytes: stat.size, - updatedAt: stat.mtimeMs - }; - }, - createReadStream(key) { - return fs20.createReadStream(resolveStoragePath(root, key)); - }, - createWriteStream(key) { - const target = resolveStoragePath(root, key); - fs20.mkdirSync(path24.dirname(target), { recursive: true }); - return fs20.createWriteStream(target); - }, - async getSignedUrl() { - throw storageError("local fs adapter does not support signed URLs", "signed_url_not_supported"); - } - }; -} - -// mindspace-service.mjs -function serviceError(message, code, details = {}) { - return Object.assign(new Error(message), { code, ...details }); -} -function requireStorageAdapter(storageAdapter) { - if (!storageAdapter || typeof storageAdapter.putObject !== "function") { - throw serviceError("MindSpace storage adapter is required", "missing_mindspace_storage_adapter"); - } - return storageAdapter; -} -function packageStorageKey(packageRecord, relativePath) { - const record = createConversationPackageRecord(packageRecord); - const relative = normalizeMindSpaceStorageKey(relativePath); - return `${record.storagePrefix}/${relative}`; -} -function createMindSpaceServiceFacade({ - storageAdapter, - publicBaseUrl, - conversationPackageBackfill, - conversationPackagePublicHtmlHydrator, - logger = console -} = {}) { - const storage = requireStorageAdapter(storageAdapter); - return { - createConversationPackage(input) { - return createConversationPackageRecord(input); - }, - createConversationArtifact(input) { - return createConversationArtifact(input); - }, - buildConversationManifest(input) { - return buildConversationPackageManifest(input); - }, - async putConversationManifest({ packageRecord, artifacts = [] }) { - const manifest = buildConversationPackageManifest({ packageRecord, artifacts }); - const body = `${JSON.stringify(manifest, null, 2)} -`; - const writeResult = await this.putPackageObject({ - packageRecord: manifest, - relativePath: "manifest.json", - body - }); - return { - manifest, - storageKey: writeResult.key, - writeResult - }; - }, - buildConversationStoragePrefix(input) { - return buildConversationStoragePrefix(input); - }, - createPublicPageUrl({ ownerKey, filename }) { - return createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey, filename }); - }, - createPublicObjectUrl({ ownerKey, relativePath }) { - return createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath }); - }, - resolvePackageStorageKey({ packageRecord, relativePath }) { - return packageStorageKey(packageRecord, relativePath); - }, - async putPackageObject({ packageRecord, relativePath, body }) { - return storage.putObject(packageStorageKey(packageRecord, relativePath), body); - }, - async getPackageObject({ packageRecord, relativePath }) { - return storage.getObject(packageStorageKey(packageRecord, relativePath)); - }, - async statPackageObject({ packageRecord, relativePath }) { - return storage.statObject(packageStorageKey(packageRecord, relativePath)); - }, - async listPackageObjects({ packageRecord, prefix = "" }) { - const record = createConversationPackageRecord(packageRecord); - const cleanPrefix = prefix ? normalizeMindSpacePublicRelativePath(prefix) : ""; - const storagePrefix = cleanPrefix ? `${record.storagePrefix}/${cleanPrefix}` : record.storagePrefix; - return storage.listObjects(storagePrefix); - }, - async prepareConversationPackageRead({ user, sessionId } = {}) { - if (typeof conversationPackageBackfill === "function") { - await conversationPackageBackfill({ user, sessionId }).catch((error) => { - logger?.warn?.("[MindSpace] conversation package DB backfill failed:", error?.message ?? error); - }); - } - if (typeof conversationPackagePublicHtmlHydrator === "function") { - return conversationPackagePublicHtmlHydrator({ user, sessionId }); - } - return []; - } - }; -} - -// mindspace-page-patch.mjs -function mergeMindSpacePagePatch(base, patch) { - return { - title: patch.title ?? base.title, - summary: patch.summary ?? base.summary, - content: patch.content ?? base.content - }; -} -function normalizeMindSpacePagePatchInput(input = {}) { - const patch = {}; - if (typeof input.title === "string") patch.title = input.title; - if (typeof input.summary === "string") patch.summary = input.summary; - if (typeof input.content === "string") patch.content = input.content; - return Object.keys(patch).length > 0 ? patch : null; -} -function buildMindSpacePageSaveInstructions(page, options = {}) { - if (!page?.id) return []; - const h5ApiBase = String(options.h5ApiBase ?? "").replace(/\/$/, ""); - const sessionId = String(options.sessionId ?? "").trim(); - const lines = [ - "\u3010\u9875\u9762\u5B9E\u65F6\u4FEE\u6539\uFF08\u786C\u6027\uFF09\u3011", - `\u7528\u6237\u8981\u6C42\u4FEE\u6539\u5F53\u524D\u9875\u9762\uFF08id: ${page.id}\uFF09\u65F6\uFF0C\u5FC5\u987B\u8BA9\u754C\u9762\u9884\u89C8\u7ACB\u523B\u66F4\u65B0\u3002\u4F18\u5148\u4F7F\u7528 API\uFF0C\u5176\u6B21\u4F7F\u7528\u8865\u4E01\u4EE3\u7801\u5757\u3002` - ]; - if (h5ApiBase && sessionId) { - lines.push( - "1. **\u4F18\u5148** \u6BCF\u6B21\u5B8C\u6210\u4E00\u5904\u4FEE\u6539\u540E\uFF0C\u7ACB\u5373\u7528 shell \u8C03\u7528\uFF08\u53EF\u591A\u6B21\u8C03\u7528\uFF0C\u6539\u4E00\u70B9\u8C03\u4E00\u6B21\uFF09\uFF1A", - "```bash", - `curl -sS -X POST '${h5ApiBase}/api/agent/mindspace_page_patch' \\`, - " -H 'Content-Type: application/json' \\", - ` -d '{"session_id":"${sessionId}","page_id":"${page.id}","title":"\u4FEE\u6539\u540E\u7684\u6807\u9898"}'`, - "```", - "- \u53EA\u4F20\u5B9E\u9645\u53D8\u66F4\u5B57\u6BB5\uFF1A`title` / `summary` / `content`", - "- HTML \u9875\u9762\u6539\u6807\u9898\u6216\u6B63\u6587\u65F6\uFF0C`content` \u4F20\u5B8C\u6574 HTML", - "- \u7981\u6B62\u53EA\u6539\u5DE5\u4F5C\u533A\u91CC\u7684\u5176\u5B83 html \u6587\u4EF6\u800C\u4E0D\u8C03\u6B64 API" - ); - } - lines.push( - "2. **\u5907\u7528** \u82E5 curl \u5931\u8D25\uFF0C\u5728\u56DE\u590D\u672B\u5C3E\u8F93\u51FA\uFF1A", - "```mindspace-page-update", - JSON.stringify( - { - title: "\u4FEE\u6539\u540E\u7684\u6807\u9898\uFF08\u5982\u6709\u53D8\u66F4\uFF09", - summary: "\u4FEE\u6539\u540E\u7684\u6458\u8981\uFF08\u5982\u6709\u53D8\u66F4\uFF09", - content: page.contentFormat === "html" ? "" : "\u4FEE\u6539\u540E\u7684\u6B63\u6587\uFF08\u5982\u6709\u53D8\u66F4\uFF09" - }, - null, - 2 - ), - "```", - "- \u53EA\u586B\u5199\u5B9E\u9645\u53D8\u66F4\u7684\u5B57\u6BB5", - "- \u7981\u6B62\u53EA\u53E3\u5934\u8BF4\u300C\u5DF2\u4FEE\u6539\u300D\u800C\u4E0D\u8C03\u7528 API \u6216\u8F93\u51FA\u8865\u4E01\u5757", - "" - ); - return lines; -} - -// mindspace-page-live-edit.mjs -function createPageLiveEditService({ pageService, resolveUserIdForAgentSession }) { - const sessionBindings = /* @__PURE__ */ new Map(); - const liveRevisions = /* @__PURE__ */ new Map(); - const bumpRevision = (pageId) => { - liveRevisions.set(pageId, (liveRevisions.get(pageId) ?? 0) + 1); - }; - return { - bindSession({ userId, sessionId, pageId, parentSessionId = null }) { - if (!userId || !sessionId || !pageId) { - throw Object.assign(new Error("\u7F3A\u5C11\u7ED1\u5B9A\u53C2\u6570"), { code: "invalid_request" }); - } - sessionBindings.set(String(sessionId), { - userId: String(userId), - pageId: String(pageId), - parentSessionId: parentSessionId ? String(parentSessionId) : null, - boundAt: Date.now() - }); - return { - sessionId: String(sessionId), - pageId: String(pageId), - ...parentSessionId ? { parentSessionId: String(parentSessionId) } : {} - }; - }, - unbindSession(sessionId) { - sessionBindings.delete(String(sessionId)); - }, - getBinding(sessionId) { - return sessionBindings.get(String(sessionId)) ?? null; - }, - getLiveRevision(pageId) { - return liveRevisions.get(String(pageId)) ?? 0; - }, - async applyAgentPatch(input) { - const sessionId = String(input?.sessionId ?? input?.session_id ?? "").trim(); - const pageId = String(input?.pageId ?? input?.page_id ?? "").trim(); - if (!sessionId || !pageId) { - throw Object.assign(new Error("\u7F3A\u5C11 session_id \u6216 page_id"), { code: "invalid_request" }); - } - const userId = await resolveUserIdForAgentSession(sessionId); - if (!userId) { - throw Object.assign(new Error("\u65E0\u6548\u7684 Agent \u4F1A\u8BDD"), { code: "forbidden" }); - } - const binding = sessionBindings.get(sessionId); - if (binding && binding.pageId !== pageId) { - throw Object.assign(new Error("\u5F53\u524D\u4F1A\u8BDD\u672A\u7ED1\u5B9A\u8BE5\u9875\u9762"), { code: "page_binding_mismatch" }); - } - if (binding && binding.userId !== userId) { - throw Object.assign(new Error("\u4F1A\u8BDD\u4E0E\u7528\u6237\u4E0D\u5339\u914D"), { code: "forbidden" }); - } - const patch = normalizeMindSpacePagePatchInput(input); - if (!patch) { - throw Object.assign(new Error("\u7F3A\u5C11 title / summary / content \u53D8\u66F4"), { code: "invalid_request" }); - } - const page = await pageService.getPage(userId, pageId); - const merged = mergeMindSpacePagePatch( - { - title: page.title, - summary: page.summary ?? "", - content: page.content ?? "" - }, - patch - ); - const updated = await pageService.updatePage(userId, pageId, { - expectedVersion: input?.expectedVersion ?? input?.expected_version ?? page.versionNo, - title: merged.title, - summary: merged.summary, - content: merged.content, - templateId: page.templateId, - changeNote: String(input?.changeNote ?? input?.change_note ?? "Agent \u5BF9\u8BDD\u4FEE\u6539").slice(0, 255) - }); - bumpRevision(pageId); - return { - page: updated, - liveRevision: liveRevisions.get(pageId) ?? 0 - }; - }, - async getRevisionSnapshot(userId, pageId) { - const page = await pageService.getPage(userId, pageId); - return { - pageId: page.id, - versionNo: page.versionNo, - updatedAt: page.updatedAt, - liveRevision: liveRevisions.get(pageId) ?? 0 - }; - } - }; -} - -// mindspace-asset-agent.mjs -function createAssetAgentService({ assetService, resolveUserIdForAgentSession }) { - return { - async applyAgentDelete(input = {}) { - const sessionId = String(input?.sessionId ?? input?.session_id ?? "").trim(); - const assetId = String(input?.assetId ?? input?.asset_id ?? "").trim(); - const confirmed = input?.confirmed === true || String(input?.confirmed ?? "").toLowerCase() === "true"; - if (!sessionId || !assetId) { - throw Object.assign(new Error("\u7F3A\u5C11 session_id \u6216 asset_id"), { code: "invalid_request" }); - } - if (!confirmed) { - throw Object.assign(new Error("\u5220\u9664\u524D\u987B\u5DF2\u5728\u5BF9\u8BDD\u4E2D\u83B7\u5F97\u7528\u6237\u786E\u8BA4"), { code: "confirmation_required" }); - } - const userId = await resolveUserIdForAgentSession(sessionId); - if (!userId) { - throw Object.assign(new Error("\u65E0\u6548\u7684 Agent \u4F1A\u8BDD"), { code: "forbidden" }); - } - const result = await assetService.deleteAsset(userId, assetId); - return { - assetId, - userId, - ...result - }; - } - }; -} - -// mindspace-page-edit-session.mjs -import { Agent as Agent2, fetch as undiciFetch2 } from "undici"; - -// mindspace-chat-context.mjs -function buildPageEditSubAgentInstructions(context) { - const pageTitle = context.page?.title ?? "\u5F53\u524D\u9875\u9762"; - const lines = [ - "\u3010\u9875\u9762\u7F16\u8F91\u5B50 Agent\uFF08\u786C\u6027\uFF09\u3011", - `- \u4F60\u662F\u4E13\u6CE8\u4FEE\u6539\u9875\u9762\u300C${pageTitle}\u300D\u7684\u5B50 Agent\uFF0C\u7528\u6237\u6B63\u5728\u5168\u5C4F\u9884\u89C8\u4E2D\u7F16\u8F91\u3002`, - "- \u53EA\u505A\u9875\u9762\u5185\u5BB9/\u6807\u9898/\u6837\u5F0F\u4FEE\u6539\uFF0C\u4E0D\u8981\u8DD1\u9898\uFF0C\u4E0D\u8981 delegate / load_skill / \u6539\u5176\u5B83\u6587\u4EF6\u3002", - "- \u6BCF\u6B21\u5B8C\u6210\u4E00\u5904\u4FEE\u6539\u540E\u5FC5\u987B\u8BA9\u9884\u89C8\u7ACB\u523B\u66F4\u65B0\uFF08\u89C1\u4E0B\u65B9\u9875\u9762\u5B9E\u65F6\u4FEE\u6539\u8BF4\u660E\uFF09\u3002" - ]; - if (context.parentAgentSessionId) { - lines.push(`- \u7236\u5BF9\u8BDD session\uFF1A${context.parentAgentSessionId}\uFF08\u9000\u51FA\u9884\u89C8\u540E\u6458\u8981\u4F1A\u5408\u5E76\u56DE\u7236\u5BF9\u8BDD\u8BB0\u5FC6\uFF09\u3002`); - } - lines.push(""); - return lines.join("\n"); -} - -// mindspace-page-edit-session.mjs -var insecureDispatcher2 = new Agent2({ - connect: { rejectUnauthorized: false } -}); -function isHttpsTarget2(target) { - return target.startsWith("https://"); -} -function createApiFetch(apiTarget, apiSecret) { - return 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 undiciFetch2(url, { - ...init, - headers, - dispatcher: isHttpsTarget2(apiTarget) ? insecureDispatcher2 : void 0 - }); - }; -} -async function readJson2(upstream) { - const text = await upstream.text(); - if (!upstream.ok) { - throw Object.assign(new Error(text || `upstream ${upstream.status}`), { - code: "worker_unavailable" - }); - } - return text ? JSON.parse(text) : null; -} -function buildPageEditSeedContent({ page, parentSessionId, h5ApiBase, sessionId }) { - const context = { - spaceId: "mindspace", - spaceName: "MindSpace", - view: "page", - route: `/mindspace/page/${page.id}/preview`, - pageEditMode: true, - parentAgentSessionId: parentSessionId, - agentSessionId: sessionId, - h5ApiBase, - page: { - id: page.id, - title: page.title, - summary: page.summary ?? "", - status: page.status, - versionNo: page.versionNo, - templateId: page.templateId, - contentFormat: page.contentFormat ?? "html" - } - }; - const lines = [ - ...buildPageEditSubAgentInstructions(context).split("\n"), - "[MindSpace \u9875\u9762\u7F16\u8F91\u5B50\u4F1A\u8BDD]", - `- \u9875\u9762\uFF1A${page.title}\uFF08id: ${page.id}\uFF09`, - `- \u5185\u5BB9\u683C\u5F0F\uFF1A${page.contentFormat ?? "html"}`, - `- \u5F53\u524D\u7248\u672C\uFF1Av${page.versionNo ?? 1}`, - ...buildMindSpacePageSaveInstructions( - { - id: page.id, - contentFormat: page.contentFormat ?? "html" - }, - { sessionId, h5ApiBase } - ), - "\u8BF7\u7528\u4E00\u53E5\u8BDD\u786E\u8BA4\u4F60\u5DF2\u51C6\u5907\u597D\u4FEE\u6539\u6B64\u9875\u9762\uFF0C\u5E76\u7B49\u5F85\u7528\u6237\u6307\u4EE4\u3002" - ]; - return lines.join("\n"); -} -function buildPageEditSessionConstraints(pageTitle) { - return [ - "\u3010\u9875\u9762\u7F16\u8F91\u5B50 Agent \u6C99\u7BB1\u3011", - `- \u5F53\u524D\u4EFB\u52A1\uFF1A\u4EC5\u4FEE\u6539 MindSpace \u9875\u9762\u300C${pageTitle}\u300D`, - "- \u7981\u6B62\u8BFB\u5199\u5DE5\u4F5C\u533A\u5176\u5B83\u6587\u4EF6\uFF1B\u7981\u6B62 delegate / load_skill / \u53D1\u5E03\u65B0\u9875\u9762", - "- \u6539\u9875\u9762\u4F18\u5148 curl \u8C03\u7528 mindspace_page_patch\uFF1B\u82E5\u65E0 shell \u6743\u9650\u5219\u5728\u56DE\u590D\u672B\u5C3E\u8F93\u51FA ```mindspace-page-update``` \u5757", - "- \u6BCF\u6B21\u6539\u52A8\u540E\u7ACB\u5373\u89E6\u53D1\u9884\u89C8\u66F4\u65B0\uFF0C\u4E0D\u8981\u53EA\u53E3\u5934\u8BF4\u300C\u5DF2\u4FEE\u6539\u300D" - ].join("\n"); -} -function createPageEditSessionService({ - apiTarget, - apiSecret, - userAuth: userAuth2, - pageService, - pageLiveEdit, - llmProviderService: llmProviderService2 -}) { - const apiFetch2 = createApiFetch(apiTarget, apiSecret); - const applySessionLlmProvider = async (sessionId) => { - if (!llmProviderService2 || !sessionId) return null; - try { - return await llmProviderService2.applyBestProviderForSession(sessionId); - } catch { - return null; - } - }; - return { - async forkSession({ userId, pageId, parentSessionId, h5ApiBase = null }) { - if (!userId || !pageId || !parentSessionId) { - throw Object.assign(new Error("\u7F3A\u5C11 fork \u53C2\u6570"), { code: "invalid_request" }); - } - const ownsParent = await userAuth2.ownsSession(userId, parentSessionId); - if (!ownsParent) { - throw Object.assign(new Error("\u65E0\u6743\u4F7F\u7528\u7236\u4F1A\u8BDD"), { code: "forbidden" }); - } - const page = await pageService.getPage(userId, pageId); - if (page.contentFormat !== "html") { - throw Object.assign(new Error("\u4EC5 HTML \u9875\u9762\u652F\u6301\u7F16\u8F91\u5B50\u4F1A\u8BDD"), { code: "invalid_request" }); - } - const gate = await userAuth2.canUseChat(userId); - if (!gate.ok) { - throw Object.assign(new Error(gate.message || "\u5F53\u524D\u7528\u6237\u65E0\u6CD5\u4F7F\u7528 Agent"), { code: "forbidden" }); - } - const workingDir = await userAuth2.resolveWorkingDir(userId); - const basePolicy = await userAuth2.getAgentSessionPolicy(userId); - const sessionPolicy = buildPageEditAgentPolicy(basePolicy); - const publishLayout = await userAuth2.getUserPublishLayout(userId); - const startSession = await readJson2( - await apiFetch2("/agent/start", { - method: "POST", - body: JSON.stringify({ - working_dir: workingDir, - enable_context_memory: sessionPolicy.enableContextMemory, - ...sessionPolicy.extensionOverrides ? { extension_overrides: sessionPolicy.extensionOverrides } : {} - }) - }) - ); - const sessionId = startSession?.id; - if (!sessionId) { - throw Object.assign(new Error("Agent \u4F1A\u8BDD\u542F\u52A8\u5931\u8D25"), { code: "worker_unavailable" }); - } - await userAuth2.registerAgentSession(userId, sessionId); - if (sessionPolicy.gooseMode) { - await readJson2( - await apiFetch2("/agent/update_session", { - method: "POST", - body: JSON.stringify({ - session_id: sessionId, - goose_mode: sessionPolicy.gooseMode - }) - }) - ); - } - const sandboxConstraints = [ - publishLayout?.constraints ?? "", - buildPageEditSessionConstraints(page.title) - ].filter(Boolean).join("\n\n"); - await reconcileAgentSession(apiFetch2, sessionId, { - workingDir, - sessionPolicy, - sandboxConstraints, - userContext: publishLayout ? { - userId, - displayName: publishLayout.displayName, - username: publishLayout.username, - slug: publishLayout.slug - } : null - }); - await applySessionLlmProvider(sessionId); - await pageLiveEdit.bindSession({ - userId, - sessionId, - pageId, - parentSessionId - }); - const seed = buildPageEditSeedContent({ - page, - parentSessionId, - h5ApiBase, - sessionId - }); - await apiFetch2("/agent/harness_remember", { - method: "POST", - body: JSON.stringify({ - sessionId, - title: `\u9875\u9762\u7F16\u8F91 \xB7 ${page.title}`, - content: seed - }) - }); - await apiFetch2("/agent/harness_bootstrap", { - method: "POST", - body: JSON.stringify({ sessionId, force: true }) - }); - return { - sessionId, - pageId, - parentSessionId - }; - }, - async closeSession({ userId, sessionId, pageId, parentSessionId, summary = "" }) { - if (!userId || !sessionId || !pageId) { - throw Object.assign(new Error("\u7F3A\u5C11 close \u53C2\u6570"), { code: "invalid_request" }); - } - const owns = await userAuth2.ownsSession(userId, sessionId); - if (!owns) { - throw Object.assign(new Error("\u65E0\u6743\u5173\u95ED\u8BE5\u5B50\u4F1A\u8BDD"), { code: "forbidden" }); - } - const binding = pageLiveEdit.getBinding(sessionId); - if (binding && binding.pageId !== String(pageId)) { - throw Object.assign(new Error("\u5B50\u4F1A\u8BDD\u672A\u7ED1\u5B9A\u8BE5\u9875\u9762"), { code: "page_binding_mismatch" }); - } - pageLiveEdit.unbindSession(sessionId); - const trimmedSummary = String(summary ?? "").trim(); - const parentId = String(parentSessionId ?? binding?.parentSessionId ?? "").trim(); - if (trimmedSummary && parentId) { - const ownsParent = await userAuth2.ownsSession(userId, parentId); - if (ownsParent) { - const page = await pageService.getPage(userId, pageId).catch(() => null); - await apiFetch2("/agent/harness_remember", { - method: "POST", - body: JSON.stringify({ - sessionId: parentId, - title: `\u9875\u9762\u7F16\u8F91\u6458\u8981 \xB7 ${page?.title ?? pageId}`, - content: trimmedSummary - }) - }); - } - } - return { sessionId, pageId, merged: Boolean(trimmedSummary && parentId) }; - } - }; -} - -// mindspace-cover-ai.mjs -import { Agent as Agent4, fetch as undiciFetch4 } from "undici"; -import { jsonrepair } from "jsonrepair"; - -// llm-providers.mjs -import crypto14 from "node:crypto"; -import fs21 from "node:fs"; -import os from "node:os"; -import path25 from "node:path"; -import { spawn, spawnSync } from "node:child_process"; -import { Agent as Agent3, fetch as undiciFetch3 } from "undici"; -var CUSTOM_PROVIDER_ID = "__custom__"; -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 insecureDispatcher3 = new Agent3({ - 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 crypto14.createHash("sha256").update(raw).digest(); -} -function encryptSecret(plaintext, encryptionKey) { - const key = resolveEncryptionKey(encryptionKey); - const iv = crypto14.randomBytes(12); - const cipher = crypto14.createCipheriv("aes-256-gcm", key, iv); - const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); - return { - ciphertext: encrypted.toString("base64"), - iv: iv.toString("base64"), - tag: cipher.getAuthTag().toString("base64") - }; -} -function decryptSecret({ ciphertext, iv, tag }, encryptionKey) { - const key = resolveEncryptionKey(encryptionKey); - const decipher = crypto14.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64")); - decipher.setAuthTag(Buffer.from(tag, "base64")); - const plain = Buffer.concat([ - decipher.update(Buffer.from(ciphertext, "base64")), - 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 = undiciFetch3) { - 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://") ? insecureDispatcher3 : 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 = undiciFetch3) { - 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 normalizeExecutor2(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 = normalizeExecutor2(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 path25.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 (path25.isAbsolute(command)) { - try { - fs21.accessSync(command, fs21.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` - }; - } - fs21.mkdirSync(logDir, { recursive: true }); - const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); - const logFile = path25.join( - logDir, - `${plan.executor ?? "executor"}-${stamp}-${crypto14.randomUUID().slice(0, 8)}.log` - ); - const fd = fs21.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 { - fs21.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 `${normalizeExecutor2(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 args = mode === "serve" ? ["serve", "--mount-cwd"] : ["--headless", "--json", "--override-with-envs", "--task", instruction]; - return { - ok: true, - executor: "openhands", - cwd, - command, - args, - 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 = undiciFetch3) { - 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://") ? insecureDispatcher3 : 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 = undiciFetch3) { - 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 = undiciFetch3) { - 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 = [], apiSecret, encryptionKey, apiFetchImpl = undiciFetch3 } = {}) { - const launchStates = /* @__PURE__ */ new Map(); - const syncTargets = [ - ...new Set([...Array.isArray(apiTargets) ? apiTargets : [], 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 = normalizeExecutor2(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 = normalizeExecutor2(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: normalizeExecutor2(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 = normalizeExecutor2(executor); - if (!normalizedExecutor) return { ok: false, message: "\u4E0D\u652F\u6301\u7684\u6267\u884C\u5668" }; - const purpose = normalizePurpose(payload.purpose); - const enabled = payload.enabled !== false; - 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 (?, ?, ?, ?, ?, ?, ?, ?)`, - [crypto14.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 = normalizeExecutor2(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 = normalizeExecutor2(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 = normalizeExecutor2(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 = crypto14.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 undiciFetch3(`${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 }; - } - }; -} - -// mindspace-cover-ai.mjs -var insecureDispatcher4 = new Agent4({ - connect: { rejectUnauthorized: false } -}); -function resolveEncryptionKey2(explicitKey) { - const raw = explicitKey ?? process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY ?? "local-dev-secret"; - return raw; -} -function parseJsonObject(text) { - const source = String(text ?? "").trim(); - if (!source) return null; - try { - return JSON.parse(source); - } catch { - try { - return JSON.parse(jsonrepair(source)); - } catch { - return null; - } - } -} -function extractJsonObject(text) { - const direct = parseJsonObject(text); - if (direct && typeof direct === "object") return direct; - const source = String(text ?? "").trim(); - const start = source.indexOf("{"); - const end = source.lastIndexOf("}"); - if (start < 0 || end <= start) { - throw Object.assign(new Error("AI \u672A\u8FD4\u56DE\u6709\u6548 JSON"), { code: "cover_ai_invalid_output" }); - } - const raw = source.slice(start, end + 1); - try { - return JSON.parse(jsonrepair(raw)); - } catch (error) { - throw Object.assign(new Error("AI \u5C01\u9762 JSON \u89E3\u6790\u5931\u8D25"), { - code: "cover_ai_invalid_output", - cause: error instanceof Error ? error.message : String(error) - }); - } -} -function buildCoverPrompt({ title, summary, html, instruction, currentCover }) { - const excerpt = String(html ?? "").replace(//gi, " ").replace(//gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 1200); - return [ - "\u4F60\u662F MindSpace \u4FE1\u606F\u6D41\u5C01\u9762\u8BBE\u8BA1\u52A9\u624B\u3002\u8BF7\u6839\u636E\u9875\u9762\u5185\u5BB9\u751F\u6210 mindspace-cover \u5143\u6570\u636E\u3002", - "\u53EA\u8FD4\u56DE\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981 Markdown\uFF0C\u4E0D\u8981\u89E3\u91CA\u3002", - "\u5B57\u6BB5\u8981\u6C42\uFF1A", - "- tag: 2-4 \u4E2A\u6C49\u5B57\u5206\u7C7B\uFF0C\u5982 \u65C5\u884C/\u7F8E\u98DF/\u6D3B\u52A8/\u62A5\u544A", - "- emoji: 1 \u4E2A\u4E0E\u4E3B\u9898\u9AD8\u5EA6\u76F8\u5173\u7684 emoji", - "- accent: \u4E3B\u8272 hex\uFF0C\u5982 #eeb04e", - "- accent2: \u8F85\u8272 hex", - "- subtitle: 12-28 \u5B57\u526F\u6807\u9898\uFF0C\u7528\u4E8E\u5C01\u9762\u5E95\u90E8", - "- cover: \u53EF\u9009\uFF0C\u82E5\u9875\u9762\u5DF2\u6709 hero \u56FE\u8DEF\u5F84\u53EF\u4FDD\u7559\uFF0C\u5426\u5219\u7559\u7A7A\u5B57\u7B26\u4E32", - "", - `\u9875\u9762\u6807\u9898\uFF1A${title || "\u672A\u547D\u540D\u9875\u9762"}`, - `\u9875\u9762\u6458\u8981\uFF1A${summary || "\u65E0"}`, - `\u5F53\u524D cover \u5143\u6570\u636E\uFF1A${JSON.stringify(currentCover ?? {})}`, - `\u9875\u9762\u6B63\u6587\u6458\u5F55\uFF1A${excerpt || "\u65E0"}`, - instruction ? `\u7528\u6237\u8865\u5145\u8981\u6C42\uFF1A${instruction}` : "" - ].filter(Boolean).join("\n"); -} -async function suggestCoverMetaWithAi(pool2, { title, summary, html, instruction, encryptionKey }) { - const [rows] = await pool2.query( - `SELECT * FROM h5_llm_provider_keys WHERE is_selected = 1 AND status = 'active' LIMIT 1` - ); - const row = rows[0]; - if (!row) { - throw Object.assign(new Error("\u8BF7\u5148\u5728\u7BA1\u7406\u540E\u53F0\u914D\u7F6E\u5E76\u542F\u7528 LLM"), { code: "llm_not_configured" }); - } - let apiKey; - try { - apiKey = decryptSecret( - { - ciphertext: row.api_key_ciphertext, - iv: row.api_key_iv, - tag: row.api_key_tag - }, - resolveEncryptionKey2(encryptionKey) - ); - } catch { - throw Object.assign(new Error("LLM \u5BC6\u94A5\u89E3\u5BC6\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u914D\u7F6E"), { code: "llm_not_configured" }); - } - const apiUrl = normalizeApiUrl(row.api_url); - const url = resolveChatCompletionsUrl(apiUrl); - if (!url) { - throw Object.assign(new Error("LLM API \u5730\u5740\u65E0\u6548"), { code: "llm_not_configured" }); - } - const prompt = buildCoverPrompt({ - title, - summary, - html, - instruction, - currentCover: parseMindspaceCoverMeta(html) - }); - let upstream; - try { - upstream = await undiciFetch4(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}` - }, - body: JSON.stringify({ - model: row.default_model, - messages: [{ role: "user", content: prompt }], - stream: false, - ...row.relay_provider ? { provider: row.relay_provider } : {} - }), - dispatcher: url.startsWith("https://") ? insecureDispatcher4 : void 0 - }); - } catch (error) { - throw Object.assign( - new Error(`AI \u5C01\u9762\u751F\u6210\u5931\u8D25\uFF1A${error instanceof Error ? error.message : "\u7F51\u7EDC\u9519\u8BEF"}`), - { code: "cover_ai_failed" } - ); - } - const text = await upstream.text().catch(() => ""); - if (!upstream.ok) { - throw Object.assign(new Error(`AI \u5C01\u9762\u751F\u6210\u5931\u8D25\uFF1A${text.slice(0, 240) || upstream.status}`), { - code: "cover_ai_failed" - }); - } - let data; - try { - data = JSON.parse(text); - } catch { - throw Object.assign(new Error("AI \u54CD\u5E94\u4E0D\u662F JSON"), { code: "cover_ai_failed" }); - } - const reply = data?.choices?.[0]?.message?.content ?? data?.message?.content ?? data?.output ?? text; - const parsed = normalizeCoverMetaSuggestion(extractJsonObject(reply)); - if (!parsed.tag && !parsed.emoji && !parsed.accent) { - throw Object.assign(new Error("AI \u672A\u751F\u6210\u6709\u6548\u5C01\u9762\u53C2\u6570"), { code: "cover_ai_invalid_output" }); - } - return parsed; -} - -// plaza-posts.mjs -import crypto15 from "node:crypto"; - -// plaza-algorithm.mjs -var DEFAULT_CONFIG = { - w_view: 0.1, - w_like: 3, - w_comment: 5, - w_collect: 4, - decay_base: 2, - decay_exp: 1.5 -}; -function computeHotScore(stats, config = DEFAULT_CONFIG, nowMs3 = Date.now()) { - const publishedAt = Number(stats.published_at ?? stats.publishedAt ?? nowMs3); - const ageHours = Math.max(0, (nowMs3 - publishedAt) / 36e5); - const numerator = Number(stats.view_count ?? 0) * config.w_view + Number(stats.like_count ?? 0) * config.w_like + Number(stats.comment_count ?? 0) * config.w_comment + Number(stats.collect_count ?? 0) * config.w_collect; - const denominator = Math.pow(ageHours + config.decay_base, config.decay_exp); - if (denominator <= 0) return 0; - return Math.max(0, numerator / denominator); -} -async function loadAlgorithmConfig(pool2) { - const [rows] = await pool2.query(`SELECT \`key\`, value FROM plaza_algorithm_config`); - const config = { ...DEFAULT_CONFIG }; - for (const row of rows) { - if (row.key in config) config[row.key] = Number(row.value); - } - return config; -} -async function ensureAlgorithmConfig(pool2) { - const [rows] = await pool2.query(`SELECT COUNT(*) AS count FROM plaza_algorithm_config`); - if (Number(rows[0]?.count ?? 0) > 0) return; - const now = Date.now(); - const seeds = [ - ["w_view", 0.1, "\u6D4F\u89C8\u91CF\u6743\u91CD"], - ["w_like", 3, "\u70B9\u8D5E\u6743\u91CD"], - ["w_comment", 5, "\u8BC4\u8BBA\u6743\u91CD"], - ["w_collect", 4, "\u6536\u85CF\u6743\u91CD"], - ["decay_base", 2, "\u65F6\u95F4\u8870\u51CF\u57FA\u6570"], - ["decay_exp", 1.5, "\u65F6\u95F4\u8870\u51CF\u6307\u6570"] - ]; - for (const [key, value, description] of seeds) { - await pool2.query( - `INSERT INTO plaza_algorithm_config (\`key\`, value, description, updated_at) - VALUES (?, ?, ?, ?)`, - [key, value, description, now] - ); - } -} -async function recalculateHotScores(pool2, { windowHours = 48 } = {}) { - await ensureAlgorithmConfig(pool2); - const config = await loadAlgorithmConfig(pool2); - const now = Date.now(); - const windowStart = now - windowHours * 36e5; - const [rows] = await pool2.query( - `SELECT id, view_count, like_count, comment_count, collect_count, published_at - FROM plaza_posts - WHERE status = 'published' AND published_at >= ?`, - [windowStart] - ); - if (rows.length === 0) return { updated: 0 }; - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - for (const row of rows) { - const hotScore = computeHotScore(row, config, now); - await conn.query( - `UPDATE plaza_posts SET hot_score = ?, hot_updated_at = ?, updated_at = ? WHERE id = ?`, - [hotScore, now, now, row.id] - ); - } - await conn.commit(); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - return { updated: rows.length }; -} - -// plaza-posts.mjs -var DEFAULT_CATEGORIES = [ - { name: "\u804C\u573A\u62A5\u544A", slug: "work-report", icon: "\u{1F4CA}", sort_order: 1 }, - { name: "\u5B66\u4E60\u7B14\u8BB0", slug: "study-notes", icon: "\u{1F4DA}", sort_order: 2 }, - { name: "\u521B\u610F\u4F5C\u54C1", slug: "creative", icon: "\u{1F3A8}", sort_order: 3 }, - { name: "\u95E8\u5E97\u5C55\u793A", slug: "business", icon: "\u{1F3EA}", sort_order: 4 }, - { name: "\u65C5\u884C\u653B\u7565", slug: "travel", icon: "\u2708\uFE0F", sort_order: 5 }, - { name: "\u6570\u636E\u5206\u6790", slug: "data-analysis", icon: "\u{1F4C8}", sort_order: 6 }, - { name: "\u751F\u6D3B\u8BB0\u5F55", slug: "lifestyle", icon: "\u{1F33F}", sort_order: 7 }, - { name: "\u5176\u4ED6", slug: "other", icon: "\u{1F4A1}", sort_order: 99 } -]; -function plazaError(message, code, details) { - return Object.assign(new Error(message), { code, details }); -} -function normalizeTags(value) { - if (!Array.isArray(value)) return []; - const tags = value.map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, 5); - return tags; -} -function clampLimit(value, fallback = 20, max = 50) { - const limit = Number(value ?? fallback); - if (!Number.isFinite(limit) || limit < 1) return fallback; - return Math.min(Math.floor(limit), max); -} -function normalizeSort(value) { - const raw = String(value ?? "recommend").trim().toLowerCase(); - if (raw === "new") return "new"; - if (raw === "hot") return "hot"; - return "recommend"; -} -function defaultPlazaCoverUrl(publicUrl2) { - const value = String(publicUrl2 ?? "").trim(); - if (!value) return ""; - const [pathname, suffix = ""] = value.split(/([?#].*)/, 2); - if (!pathname) return ""; - const nextPath = pathname.endsWith("/") ? `${pathname}index.thumbnail.png` : `${pathname}.thumbnail.png`; - return `${nextPath}${suffix}`; -} -function resolvePlazaCoverUrl(inputCoverUrl, publicUrl2) { - const value = String(inputCoverUrl ?? "").trim(); - if (!value) return defaultPlazaCoverUrl(publicUrl2); - if (/^https?:\/\//i.test(value)) return value; - const base = String(publicUrl2 ?? "").trim(); - if (base && /^https?:\/\//i.test(base)) { - try { - return new URL(value, base).toString(); - } catch { - return value; - } - } - return value; -} -function formatStats(row) { - return { - view_count: Number(row.view_count ?? 0), - like_count: Number(row.like_count ?? 0), - collect_count: Number(row.collect_count ?? 0), - comment_count: Number(row.comment_count ?? 0), - share_count: Number(row.share_count ?? 0) - }; -} -var FRESH_POST_WINDOW_MS = 24 * 60 * 60 * 1e3; -function promoteFreshPosts(items, now = Date.now()) { - const fresh = []; - const stable = []; - for (const post of items) { - const publishedAt = Date.parse(post.published_at); - if (Number.isFinite(publishedAt) && now - publishedAt <= FRESH_POST_WINDOW_MS) { - fresh.push(post); - } else { - stable.push(post); - } - } - fresh.sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at)); - return [...fresh, ...stable]; -} -function formatPostRow(row, { category, viewerReacted = null, publicationUrl = null } = {}) { - const resolvedPublicationUrl = publicationUrl ?? row.public_url ?? null; - return { - id: row.id, - title: row.title, - summary: row.summary ?? "", - cover_url: resolvePlazaCoverUrl(row.cover_url, resolvedPublicationUrl), - category: category ?? { - id: row.category_id, - name: row.category_name ?? "", - slug: row.category_slug ?? "", - icon: row.category_icon ?? "" - }, - tags: Array.isArray(row.tags) ? row.tags : JSON.parse(row.tags ?? "[]"), - author: { - user_id: row.user_id, - slug: row.user_slug, - display_name: row.user_display_name, - avatar_url: row.user_avatar_url ?? "" - }, - stats: formatStats(row), - viewer_reacted: viewerReacted, - published_at: new Date(Number(row.published_at)).toISOString(), - ...resolvedPublicationUrl ? { publication_url: resolvedPublicationUrl } : {}, - ...row.allow_comment != null ? { allow_comment: Boolean(row.allow_comment) } : {}, - ...row.status ? { status: row.status } : {} - }; -} -function createPlazaPostService(pool2, { - idFactory = () => crypto15.randomUUID(), - loadViewerReactions = null, - plazaRedis: plazaRedis2 = null, - algorithmConfig = null, - onPostPublished = null, - loadFeaturedPosts = null, - recommendService = null -} = {}) { - const autoApprove = String(process.env.PLAZA_AUTO_APPROVE ?? "").toLowerCase() === "true"; - const resolveHotScore = async (publishedAt) => { - const config = algorithmConfig ?? { - w_view: 0.1, - w_like: 3, - w_comment: 5, - w_collect: 4, - decay_base: 2, - decay_exp: 1.5 - }; - return computeHotScore( - { - view_count: 0, - like_count: 0, - comment_count: 0, - collect_count: 0, - published_at: publishedAt - }, - config - ); - }; - const ensureCategories = async () => { - const [rows] = await pool2.query(`SELECT COUNT(*) AS count FROM plaza_categories`); - if (Number(rows[0]?.count ?? 0) > 0) return; - const now = Date.now(); - for (const category of DEFAULT_CATEGORIES) { - await pool2.query( - `INSERT INTO plaza_categories - (id, name, slug, icon, description, sort_order, is_active, created_at) - VALUES (?, ?, ?, ?, '', ?, 1, ?)`, - [idFactory(), category.name, category.slug, category.icon, category.sort_order, now] - ); - } - }; - const listCategories = async () => { - await ensureCategories(); - const [rows] = await pool2.query( - `SELECT c.id, c.name, c.slug, c.icon, - COUNT(p.id) AS post_count - FROM plaza_categories c - LEFT JOIN plaza_posts p - ON p.category_id = c.id AND p.status = 'published' - WHERE c.is_active = 1 - GROUP BY c.id, c.name, c.slug, c.icon, c.sort_order - ORDER BY c.sort_order ASC, c.name ASC` - ); - return rows.map((row) => ({ - id: row.id, - name: row.name, - slug: row.slug, - icon: row.icon ?? "", - post_count: Number(row.post_count ?? 0) - })); - }; - const resolveCategoryId = async (categoryId, categorySlug) => { - await ensureCategories(); - if (categoryId) { - const [rows] = await pool2.query( - `SELECT id FROM plaza_categories WHERE id = ? AND is_active = 1 LIMIT 1`, - [categoryId] - ); - if (!rows[0]) throw plazaError("\u5206\u7C7B\u4E0D\u5B58\u5728", "category_not_found"); - return rows[0].id; - } - if (categorySlug) { - const [rows] = await pool2.query( - `SELECT id FROM plaza_categories WHERE slug = ? AND is_active = 1 LIMIT 1`, - [categorySlug] - ); - if (!rows[0]) throw plazaError("\u5206\u7C7B\u4E0D\u5B58\u5728", "category_not_found"); - return rows[0].id; - } - throw plazaError("\u5FC5\u987B\u9009\u62E9\u5206\u7C7B", "category_required"); - }; - const loadPublicationContext = async (userId, publicationId) => { - const [rows] = await pool2.query( - `SELECT pr.id, pr.user_id, pr.status, pr.public_url, - p.title, p.summary, p.cover_image_asset_id - FROM h5_publish_records pr - JOIN h5_page_records p ON p.id = pr.page_id - WHERE pr.id = ? AND pr.user_id = ? - LIMIT 1`, - [publicationId, userId] - ); - const row = rows[0]; - if (!row) throw plazaError("\u53D1\u5E03\u8BB0\u5F55\u4E0D\u5B58\u5728", "publication_not_found"); - if (row.status !== "online") { - throw plazaError("\u53D1\u5E03\u6E90\u4E0D\u662F\u5728\u7EBF\u72B6\u6001", "PUBLICATION_NOT_ONLINE"); - } - return row; - }; - const loadUserSnapshot = async (userId) => { - const [rows] = await pool2.query( - `SELECT id, slug, display_name, username, plaza_post_banned - FROM h5_users WHERE id = ? LIMIT 1`, - [userId] - ); - const row = rows[0]; - if (!row) throw plazaError("\u7528\u6237\u4E0D\u5B58\u5728", "user_not_found"); - if (row.plaza_post_banned) throw plazaError("\u65E0\u53D1\u5E16\u6743\u9650", "POST_PERMISSION_DENIED"); - return { - user_slug: row.slug || row.username, - user_display_name: row.display_name || row.username, - user_avatar_url: "" - }; - }; - const createPost = async (userId, input) => { - const publicationId = String(input?.publication_id ?? "").trim(); - if (!publicationId) throw plazaError("publication_id \u4E0D\u80FD\u4E3A\u7A7A", "invalid_input"); - const [existing] = await pool2.query( - `SELECT id FROM plaza_posts WHERE publication_id = ? LIMIT 1`, - [publicationId] - ); - if (existing[0]) throw plazaError("\u8BE5\u5185\u5BB9\u5DF2\u53D1\u5E03\u5230\u5E7F\u573A", "ALREADY_PUBLISHED"); - const publication = await loadPublicationContext(userId, publicationId); - const categoryId = await resolveCategoryId(input?.category_id, input?.category_slug); - const userSnapshot = await loadUserSnapshot(userId); - const tags = normalizeTags(input?.tags); - const coverUrl = resolvePlazaCoverUrl(input?.cover_url, publication.public_url); - const allowComment = input?.allow_comment == null ? true : Boolean(input.allow_comment); - const now = Date.now(); - const postId = idFactory(); - const status = autoApprove ? "published" : "pending_review"; - const hotScore = status === "published" ? await resolveHotScore(now) : 0; - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - await conn.query( - `INSERT INTO plaza_posts - (id, publication_id, user_id, title, summary, cover_url, - user_slug, user_display_name, user_avatar_url, - category_id, tags, status, allow_comment, hot_score, hot_updated_at, - published_at, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - postId, - publicationId, - userId, - publication.title, - publication.summary ?? "", - coverUrl, - userSnapshot.user_slug, - userSnapshot.user_display_name, - userSnapshot.user_avatar_url, - categoryId, - JSON.stringify(tags), - status, - allowComment ? 1 : 0, - hotScore, - status === "published" ? now : null, - now, - now, - now - ] - ); - await conn.query( - `UPDATE h5_users SET plaza_post_count = plaza_post_count + 1, updated_at = ? WHERE id = ?`, - [now, userId] - ); - await conn.commit(); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - if (status === "published") { - await plazaRedis2?.invalidateFeedCaches?.(); - await onPostPublished?.(postId); - } - return { id: postId, status }; - }; - const updatePost = async (userId, postId, input) => { - const [rows] = await pool2.query( - `SELECT pp.*, pr.public_url - FROM plaza_posts pp - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.id = ? AND pp.user_id = ? LIMIT 1`, - [postId, userId] - ); - const post = rows[0]; - if (!post) throw plazaError("\u5E16\u5B50\u4E0D\u5B58\u5728", "POST_NOT_FOUND"); - const updates = []; - const params = []; - if (input?.category_id != null || input?.category_slug != null) { - const categoryId = await resolveCategoryId(input?.category_id, input?.category_slug); - updates.push("category_id = ?"); - params.push(categoryId); - } - if (input?.tags != null) { - updates.push("tags = ?"); - params.push(JSON.stringify(normalizeTags(input.tags))); - } - if (input?.cover_url != null) { - updates.push("cover_url = ?"); - params.push(resolvePlazaCoverUrl(input.cover_url, post.public_url)); - } - if (input?.allow_comment != null) { - updates.push("allow_comment = ?"); - params.push(input.allow_comment ? 1 : 0); - } - if (updates.length === 0) throw plazaError("\u6CA1\u6709\u53EF\u66F4\u65B0\u7684\u5B57\u6BB5", "invalid_input"); - let status = post.status; - if (status === "rejected") status = "pending_review"; - updates.push("status = ?"); - updates.push("updated_at = ?"); - params.push(status, Date.now(), postId, userId); - await pool2.query( - `UPDATE plaza_posts SET ${updates.join(", ")} WHERE id = ? AND user_id = ?`, - params - ); - return { id: postId, status }; - }; - const hidePost = async (userId, postId) => { - const now = Date.now(); - const [result] = await pool2.query( - `UPDATE plaza_posts SET status = 'hidden', updated_at = ? - WHERE id = ? AND user_id = ? AND status != 'hidden'`, - [now, postId, userId] - ); - if (result.affectedRows === 0) throw plazaError("\u5E16\u5B50\u4E0D\u5B58\u5728", "POST_NOT_FOUND"); - return { id: postId, status: "hidden" }; - }; - const hidePostsByPublicationId = async (conn, publicationId, now = Date.now()) => { - await conn.query( - `UPDATE plaza_posts SET status = 'hidden', updated_at = ? - WHERE publication_id = ? AND status != 'hidden'`, - [now, publicationId] - ); - }; - const getPostById = async (postId, { viewerId = null, includeHidden = false } = {}) => { - const statusClause = includeHidden ? "" : `AND pp.status = 'published'`; - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.id = ? ${statusClause} - LIMIT 1`, - [postId] - ); - const row = rows[0]; - if (!row) throw plazaError("\u5E16\u5B50\u4E0D\u5B58\u5728\u6216\u5DF2\u9690\u85CF", "POST_NOT_FOUND"); - let viewerReacted = null; - if (viewerId && loadViewerReactions) { - const map = await loadViewerReactions(viewerId, [postId]); - viewerReacted = map.get(postId) ?? { liked: false, collected: false }; - } - return formatPostRow(row, { viewerReacted, publicationUrl: row.public_url }); - }; - const listFeed = async ({ - sort = "recommend", - categorySlug = null, - cursor = null, - limit = 20, - viewerId = null, - sessionId = null - } = {}) => { - await ensureCategories(); - const normalizedSort = normalizeSort(sort); - const pageLimit = clampLimit(limit); - if (normalizedSort === "recommend" && recommendService) { - return recommendService.listRecommendedFeed({ - viewerId, - sessionId, - categorySlug, - cursor, - limit: pageLimit - }); - } - if (!cursor && normalizedSort !== "recommend" && plazaRedis2?.getFeedCache) { - const cached = await plazaRedis2.getFeedCache(normalizedSort, categorySlug, null); - if (cached?.posts) { - if (viewerId && loadViewerReactions) { - const reactionMap2 = await loadViewerReactions( - viewerId, - cached.posts.map((post) => post.id) - ); - cached.posts = cached.posts.map((post) => ({ - ...post, - viewer_reacted: reactionMap2.get(post.id) ?? null - })); - } - return cached; - } - } - const params = ["published"]; - let categoryFilter = ""; - if (categorySlug) { - categoryFilter = "AND c.slug = ?"; - params.push(categorySlug); - } - let cursorClause = ""; - if (cursor) { - const [cursorRows] = await pool2.query( - `SELECT id, hot_score, published_at FROM plaza_posts WHERE id = ? LIMIT 1`, - [cursor] - ); - const cursorRow = cursorRows[0]; - if (cursorRow) { - if (normalizedSort === "new") { - cursorClause = "AND (pp.published_at < ? OR (pp.published_at = ? AND pp.id < ?))"; - params.push(cursorRow.published_at, cursorRow.published_at, cursor); - } else { - cursorClause = "AND (pp.hot_score < ? OR (pp.hot_score = ? AND pp.id < ?))"; - params.push(cursorRow.hot_score, cursorRow.hot_score, cursor); - } - } - } - const orderBy = normalizedSort === "new" ? "pp.published_at DESC, pp.id DESC" : "pp.hot_score DESC, pp.id DESC"; - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.status = ? - ${categoryFilter} - ${cursorClause} - ORDER BY ${orderBy} - LIMIT ?`, - [...params, pageLimit + 1] - ); - const hasMore = rows.length > pageLimit; - const pageRows = hasMore ? rows.slice(0, pageLimit) : rows; - let reactionMap = /* @__PURE__ */ new Map(); - if (viewerId && loadViewerReactions) { - reactionMap = await loadViewerReactions( - viewerId, - pageRows.map((row) => row.id) - ); - } - const posts = pageRows.map( - (row) => formatPostRow(row, { - viewerReacted: viewerId ? reactionMap.get(row.id) ?? null : null - }) - ); - let featured = { homepage_banner: [], trending: [], category_top: {} }; - if (!cursor && loadFeaturedPosts) { - featured = await loadFeaturedPosts(viewerId); - } - let mergedPosts = posts; - if (!cursor && normalizedSort === "hot" && featured.trending.length > 0) { - const trending = featured.trending.slice(0, 10); - const trendingIds = new Set(trending.map((post) => post.id)); - mergedPosts = [...trending, ...posts.filter((post) => !trendingIds.has(post.id))].slice( - 0, - pageLimit - ); - } - if (!cursor && normalizedSort === "hot" && categorySlug && featured.category_top?.[categorySlug]?.length) { - const tops = featured.category_top[categorySlug].slice(0, 3); - const topIds = new Set(tops.map((post) => post.id)); - mergedPosts = [...tops, ...mergedPosts.filter((post) => !topIds.has(post.id))].slice( - 0, - pageLimit - ); - } - if (!cursor && normalizedSort === "hot") { - mergedPosts = promoteFreshPosts(mergedPosts).slice(0, pageLimit); - } - const result = { - posts: mergedPosts, - featured: { homepage_banner: featured.homepage_banner, trending: [] }, - next_cursor: hasMore ? pageRows[pageRows.length - 1].id : null, - has_more: hasMore - }; - if (!cursor && plazaRedis2?.setFeedCache) { - const ttl = normalizedSort === "new" ? 60 : 300; - await plazaRedis2.setFeedCache( - normalizedSort, - categorySlug, - null, - { - posts: mergedPosts, - featured: result.featured, - next_cursor: result.next_cursor, - has_more: result.has_more - }, - ttl - ); - } - return result; - }; - const reviewPost = async (postId, action, { reason = null } = {}) => { - const now = Date.now(); - if (action === "approve") { - const hotScore = await resolveHotScore(now); - await pool2.query( - `UPDATE plaza_posts - SET status = 'published', hot_score = ?, hot_updated_at = ?, updated_at = ? - WHERE id = ?`, - [hotScore, now, now, postId] - ); - await plazaRedis2?.invalidateFeedCaches?.(); - await onPostPublished?.(postId); - return { id: postId, status: "published" }; - } - if (action === "reject") { - if (!reason) throw plazaError("\u62D2\u7EDD\u65F6\u5FC5\u987B\u586B\u5199\u539F\u56E0", "invalid_input"); - await pool2.query( - `UPDATE plaza_posts SET status = 'rejected', updated_at = ? WHERE id = ?`, - [now, postId] - ); - return { id: postId, status: "rejected", reason }; - } - if (action === "hide") { - await pool2.query( - `UPDATE plaza_posts SET status = 'hidden', updated_at = ? WHERE id = ?`, - [now, postId] - ); - return { id: postId, status: "hidden" }; - } - throw plazaError("\u4E0D\u652F\u6301\u7684\u5BA1\u6838\u64CD\u4F5C", "invalid_input"); - }; - const listPendingPosts = async (limit = 50) => { - const [rows] = await pool2.query( - `SELECT id, title, status, user_display_name, user_slug, published_at - FROM plaza_posts - WHERE status = 'pending_review' - ORDER BY published_at DESC - LIMIT ?`, - [clampLimit(limit, 50, 100)] - ); - return rows.map((row) => ({ - id: row.id, - title: row.title, - status: row.status, - author: row.user_display_name, - author_slug: row.user_slug, - published_at: new Date(Number(row.published_at)).toISOString() - })); - }; - return { - ensureCategories, - listCategories, - createPost, - updatePost, - hidePost, - hidePostsByPublicationId, - getPostById, - listFeed, - reviewPost, - listPendingPosts - }; -} -function mapPlazaError(error) { - const code = error?.code; - const map = { - PUBLICATION_NOT_ONLINE: 422, - ALREADY_PUBLISHED: 409, - POST_NOT_FOUND: 404, - POST_PERMISSION_DENIED: 403, - publication_not_found: 403, - category_not_found: 422, - category_required: 422, - invalid_input: 422, - COMMENT_DISABLED: 422, - COMMENT_TOO_LONG: 422, - COMMENT_RATE_LIMITED: 429, - REPLY_DEPTH_EXCEEDED: 422, - SELF_FOLLOW: 422, - COMMENT_NOT_FOUND: 404, - OPS_PERMISSION_DENIED: 403, - report_not_found: 404, - featured_not_found: 404, - user_not_found: 404 - }; - return map[code] ?? 500; -} - -// plaza-events.mjs -import crypto16 from "node:crypto"; -var PROFILE_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3; -var PROFILE_CACHE_TTL_MS = 45e3; -var PLAZA_EVENT_TYPES = /* @__PURE__ */ new Set([ - "impression", - "click", - "view", - "dwell", - "like", - "collect", - "comment", - "share", - "dislike", - "hide" -]); -var PLAZA_EVENT_WEIGHTS = { - impression: 0.06, - click: 0.85, - view: 1.25, - dwell: 1, - like: 5.5, - collect: 6.5, - comment: 4.5, - share: 3.5, - dislike: -9, - hide: -6 -}; -var DWELL_THRESHOLDS = [ - { ms: 3e4, boost: 4 }, - { ms: 1e4, boost: 2.5 }, - { ms: 3e3, boost: 1.5 } -]; -function plazaError2(message, code) { - return Object.assign(new Error(message), { code }); -} -var DEEP_SEEN_EVENTS = /* @__PURE__ */ new Set([ - "click", - "view", - "dwell", - "like", - "collect", - "comment", - "share" -]); -function emptyProfile() { - return { - profileVersion: 1, - categoryWeights: {}, - tagWeights: {}, - authorWeights: {}, - feedCategoryWeights: {}, - seenPostIds: /* @__PURE__ */ new Set(), - deepSeenPostIds: /* @__PURE__ */ new Set(), - dislikedPostIds: /* @__PURE__ */ new Set(), - dislikedCategorySlugs: /* @__PURE__ */ new Set(), - likedPostIds: /* @__PURE__ */ new Set(), - followedAuthorIds: /* @__PURE__ */ new Set(), - eventCount: 0 - }; -} -function decayFactor(ageMs, halfLifeMs) { - if (ageMs <= 0) return 1; - return Math.pow(0.5, ageMs / halfLifeMs); -} -function bumpWeight(map, key, delta) { - if (!key) return; - map[key] = (map[key] ?? 0) + delta; -} -function normalizeWeights(map) { - const entries = Object.entries(map); - if (entries.length === 0) return map; - const max = Math.max(...entries.map(([, value]) => value), 1e-6); - return Object.fromEntries(entries.map(([key, value]) => [key, value / max])); -} -function dwellBoost(dwellMs) { - const ms = Number(dwellMs ?? 0); - if (!Number.isFinite(ms) || ms <= 0) return 0; - for (const tier of DWELL_THRESHOLDS) { - if (ms >= tier.ms) return tier.boost; - } - return 0; -} -function eventSignal(event) { - let weight = PLAZA_EVENT_WEIGHTS[event.event_type] ?? 0; - if (event.event_type === "dwell") { - weight *= 1 + dwellBoost(event.dwell_ms); - } - return weight; -} -function createPlazaEventService(pool2, { idFactory = () => crypto16.randomUUID() } = {}) { - const profileCache = /* @__PURE__ */ new Map(); - const cacheKey = ({ userId, sessionId }) => userId ? `u:${userId}` : sessionId ? `s:${sessionId}` : "anon"; - const invalidateProfileCache = ({ userId, sessionId }) => { - profileCache.delete(cacheKey({ userId, sessionId })); - if (userId) profileCache.delete(`u:${userId}`); - if (sessionId) profileCache.delete(`s:${sessionId}`); - }; - const recordEvents = async ({ - userId = null, - sessionId, - events = [] - }) => { - if (!sessionId) throw plazaError2("session_id \u4E0D\u80FD\u4E3A\u7A7A", "invalid_input"); - const now = Date.now(); - const normalized = []; - for (const raw of events) { - const eventType = String(raw?.event_type ?? raw?.type ?? "").trim(); - if (!PLAZA_EVENT_TYPES.has(eventType)) continue; - const postId = String(raw?.post_id ?? "").trim(); - if (!postId) continue; - normalized.push({ - id: idFactory(), - userId, - sessionId, - postId, - eventType, - dwellMs: raw?.dwell_ms == null || raw?.dwell_ms === "" ? null : Math.max(0, Math.min(36e5, Number(raw.dwell_ms))), - feedSort: String(raw?.feed_sort ?? "").slice(0, 32), - feedCategory: String(raw?.feed_category ?? "").slice(0, 64), - position: raw?.position == null || raw?.position === "" ? null : Math.max(0, Math.floor(Number(raw.position))), - createdAt: Number(raw?.created_at) > 0 ? Number(raw.created_at) : now - }); - } - if (normalized.length === 0) { - return { accepted: 0 }; - } - const values = normalized.map((event) => [ - event.id, - event.userId, - event.sessionId, - event.postId, - event.eventType, - event.dwellMs, - event.feedSort, - event.feedCategory, - event.position, - event.createdAt - ]); - await pool2.query( - `INSERT INTO plaza_user_events - (id, user_id, session_id, post_id, event_type, dwell_ms, feed_sort, feed_category, position, created_at) - VALUES ?`, - [values] - ); - invalidateProfileCache({ userId, sessionId }); - return { accepted: normalized.length }; - }; - const buildProfileFromSignals = async ({ userId, sessionId, halfLifeMs }) => { - const profile = emptyProfile(); - const since = Date.now() - PROFILE_WINDOW_MS; - const params = [since]; - let identityClause = ""; - if (userId) { - identityClause = "AND (e.user_id = ? OR e.session_id = ?)"; - params.push(userId, sessionId); - } else { - identityClause = "AND e.session_id = ?"; - params.push(sessionId); - } - const [eventRows] = await pool2.query( - `SELECT e.event_type, e.dwell_ms, e.post_id, e.created_at, - e.feed_category, c.slug AS category_slug, pp.tags, pp.user_id AS author_id - FROM plaza_user_events e - JOIN plaza_posts pp ON pp.id = e.post_id - JOIN plaza_categories c ON c.id = pp.category_id - WHERE e.created_at >= ? ${identityClause}`, - params - ); - const now = Date.now(); - for (const row of eventRows) { - profile.eventCount += 1; - profile.seenPostIds.add(row.post_id); - if (DEEP_SEEN_EVENTS.has(row.event_type)) { - profile.deepSeenPostIds.add(row.post_id); - } - const ageMs = now - Number(row.created_at); - const decay = decayFactor(ageMs, halfLifeMs); - const signal = eventSignal({ event_type: row.event_type, dwell_ms: row.dwell_ms }) * decay; - const feedCategory = String(row.feed_category ?? "").trim(); - if (feedCategory && (row.event_type === "impression" || row.event_type === "click")) { - bumpWeight(profile.feedCategoryWeights, feedCategory, Math.max(signal, 0.12) * decay); - } - if (row.event_type === "dislike" || row.event_type === "hide") { - profile.dislikedPostIds.add(row.post_id); - profile.dislikedCategorySlugs.add(row.category_slug); - bumpWeight(profile.categoryWeights, row.category_slug, signal * 0.35); - continue; - } - bumpWeight(profile.categoryWeights, row.category_slug, signal); - const tags = Array.isArray(row.tags) ? row.tags : JSON.parse(row.tags ?? "[]"); - for (const tag of tags) bumpWeight(profile.tagWeights, String(tag).toLowerCase(), signal * 0.65); - bumpWeight(profile.authorWeights, row.author_id, signal * 0.8); - if (row.event_type === "like") profile.likedPostIds.add(row.post_id); - if (row.event_type === "collect") profile.likedPostIds.add(row.post_id); - } - if (userId) { - const [reactions] = await pool2.query( - `SELECT r.type, r.post_id, c.slug AS category_slug, pp.tags, pp.user_id AS author_id, r.created_at - FROM plaza_reactions r - JOIN plaza_posts pp ON pp.id = r.post_id AND pp.status = 'published' - JOIN plaza_categories c ON c.id = pp.category_id - WHERE r.user_id = ? AND r.created_at >= ?`, - [userId, since] - ); - for (const row of reactions) { - const ageMs = now - Number(row.created_at); - const decay = decayFactor(ageMs, halfLifeMs); - const signal = (row.type === "collect" ? 6.5 : row.type === "share" ? 3.5 : 5.5) * decay; - bumpWeight(profile.categoryWeights, row.category_slug, signal); - const tags = Array.isArray(row.tags) ? row.tags : JSON.parse(row.tags ?? "[]"); - for (const tag of tags) bumpWeight(profile.tagWeights, String(tag).toLowerCase(), signal * 0.65); - bumpWeight(profile.authorWeights, row.author_id, signal); - profile.likedPostIds.add(row.post_id); - profile.seenPostIds.add(row.post_id); - } - const [follows] = await pool2.query( - `SELECT followee_id FROM plaza_follows WHERE follower_id = ?`, - [userId] - ); - for (const row of follows) { - profile.followedAuthorIds.add(row.followee_id); - bumpWeight(profile.authorWeights, row.followee_id, 4); - } - const [comments] = await pool2.query( - `SELECT c.post_id, cat.slug AS category_slug, pp.tags, pp.user_id AS author_id, c.created_at - FROM plaza_comments c - JOIN plaza_posts pp ON pp.id = c.post_id AND pp.status = 'published' - JOIN plaza_categories cat ON cat.id = pp.category_id - WHERE c.user_id = ? AND c.status = 'visible' AND c.created_at >= ?`, - [userId, since] - ); - for (const row of comments) { - const ageMs = now - Number(row.created_at); - const decay = decayFactor(ageMs, halfLifeMs); - const signal = 4.5 * decay; - bumpWeight(profile.categoryWeights, row.category_slug, signal); - const tags = Array.isArray(row.tags) ? row.tags : JSON.parse(row.tags ?? "[]"); - for (const tag of tags) bumpWeight(profile.tagWeights, String(tag).toLowerCase(), signal * 0.65); - bumpWeight(profile.authorWeights, row.author_id, signal * 0.85); - profile.seenPostIds.add(row.post_id); - } - } - profile.categoryWeights = normalizeWeights(profile.categoryWeights); - profile.tagWeights = normalizeWeights(profile.tagWeights); - profile.authorWeights = normalizeWeights(profile.authorWeights); - profile.feedCategoryWeights = normalizeWeights(profile.feedCategoryWeights); - profile.profileVersion = 1 + profile.eventCount + profile.likedPostIds.size * 3 + profile.dislikedPostIds.size * 5; - return profile; - }; - const getProfile = async ({ userId = null, sessionId, halfLifeMs = 7 * 24 * 60 * 60 * 1e3 }) => { - if (!userId && !sessionId) return emptyProfile(); - const key = cacheKey({ userId, sessionId }); - const cached = profileCache.get(key); - if (cached && cached.expiresAt > Date.now()) { - return cached.profile; - } - const profile = await buildProfileFromSignals({ userId, sessionId, halfLifeMs }); - profileCache.set(key, { profile, expiresAt: Date.now() + PROFILE_CACHE_TTL_MS }); - return profile; - }; - return { - recordEvents, - getProfile, - invalidateProfileCache, - internals: { - emptyProfile, - decayFactor, - eventSignal, - bumpWeight, - normalizeWeights, - PLAZA_EVENT_WEIGHTS - } - }; -} - -// plaza-recommend.mjs -var RECALL_LIMIT = 80; -var CANDIDATE_POOL_LIMIT = 320; -var RANK_CACHE_TTL_MS = 18e4; -var RANK_CACHE_MAX = 240; -var DEFAULT_RECOMMEND_CONFIG = { - w_category: 0.26, - w_tag: 0.18, - w_author: 0.15, - w_browse: 0.08, - w_hot: 0.11, - w_fresh: 0.1, - w_quality: 0.07, - w_ctr: 0.05, - w_seen_penalty: 0.38, - w_impression_penalty: 0.14, - w_dislike_penalty: 1.35, - w_category_dislike: 0.55, - mmr_lambda: 0.72, - profile_half_life_days: 7, - explore_ratio: 0.1, - follow_boost: 0.35, - channel_boost_cap: 0.24 -}; -var CHANNEL_WEIGHTS = { - interest: 1, - follow: 0.95, - similar: 0.88, - tag: 0.92, - tag_similar: 0.86, - fresh: 0.82, - hot: 0.65, - explore: 0.55 -}; -var rankSessionCache = /* @__PURE__ */ new Map(); -function parseTags(raw) { - if (Array.isArray(raw)) return raw.map((tag) => String(tag).toLowerCase()); - try { - return JSON.parse(raw ?? "[]").map((tag) => String(tag).toLowerCase()); - } catch { - return []; - } -} -function parseRecommendCursor(cursor) { - if (!cursor) return { offset: 0, profileVersion: null }; - try { - const parsed = JSON.parse(Buffer.from(String(cursor), "base64url").toString("utf8")); - return { - offset: Math.max(0, Number(parsed.o) || 0), - profileVersion: parsed.p == null ? null : Number(parsed.p) - }; - } catch { - return { offset: 0, profileVersion: null }; - } -} -function encodeRecommendCursor({ offset, profileVersion }) { - return Buffer.from(JSON.stringify({ o: offset, p: profileVersion }), "utf8").toString("base64url"); -} -function jaccard(a, b) { - const left = new Set(a); - const right = new Set(b); - if (left.size === 0 || right.size === 0) return 0; - let inter = 0; - for (const item of left) { - if (right.has(item)) inter += 1; - } - const union = left.size + right.size - inter; - return union > 0 ? inter / union : 0; -} -function freshnessScore(publishedAt, now = Date.now()) { - const ageHours = Math.max(0, (now - publishedAt) / 36e5); - return Math.exp(-ageHours / 72); -} -function qualityScore(row) { - const views = Number(row.view_count ?? 0); - const likes = Number(row.like_count ?? 0); - const comments = Number(row.comment_count ?? 0); - return Math.log1p(views) * 0.35 + Math.log1p(likes) * 0.45 + Math.log1p(comments) * 0.2; -} -function ctrPrior(row) { - const views = Math.max(1, Number(row.view_count ?? 0)); - const likes = Number(row.like_count ?? 0); - const collects = Number(row.collect_count ?? 0); - return (likes * 2 + collects * 3) / views; -} -function itemSimilarity(a, b) { - if (a.category_slug === b.category_slug) return 1; - if (a.author_id === b.author_id) return 0.82; - return jaccard(a.tags, b.tags) * 0.75; -} -function mmrRerank(items, { lambda, limit }) { - const selected = []; - const remaining = [...items]; - while (selected.length < limit && remaining.length > 0) { - let bestIndex = 0; - let bestScore = -Infinity; - for (let index = 0; index < remaining.length; index += 1) { - const candidate = remaining[index]; - let maxSimilarity = 0; - for (const chosen of selected) { - maxSimilarity = Math.max(maxSimilarity, itemSimilarity(candidate, chosen)); - } - const mmrScore = candidate.rank_score - lambda * maxSimilarity; - if (mmrScore > bestScore) { - bestScore = mmrScore; - bestIndex = index; - } - } - selected.push(remaining.splice(bestIndex, 1)[0]); - } - return selected; -} -function topWeightedKeys(weightMap, limit = 5) { - return Object.entries(weightMap ?? {}).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([key]) => key); -} -function hashSeed(value) { - let hash = 2166136261; - for (let index = 0; index < value.length; index += 1) { - hash ^= value.charCodeAt(index); - hash = Math.imul(hash, 16777619); - } - return hash >>> 0; -} -function seededShuffle(items, seed) { - const list = [...items]; - let state = hashSeed(String(seed)); - for (let index = list.length - 1; index > 0; index -= 1) { - state = Math.imul(state, 1664525) + 1013904223 >>> 0; - const swapIndex = state % (index + 1); - [list[index], list[swapIndex]] = [list[swapIndex], list[index]]; - } - return list; -} -function coldStartInterleave(candidates, sessionSeed = "anon") { - const groups = /* @__PURE__ */ new Map(); - for (const candidate of candidates) { - const key = candidate.category_slug || "other"; - if (!groups.has(key)) groups.set(key, []); - groups.get(key).push(candidate); - } - for (const list of groups.values()) { - list.sort((a, b) => b.hot_score - a.hot_score || b.published_at - a.published_at); - } - const categories = seededShuffle([...groups.keys()], sessionSeed); - const ordered = []; - let progress = true; - while (progress && ordered.length < candidates.length) { - progress = false; - for (const category of categories) { - const list = groups.get(category); - if (!list || list.length === 0) continue; - ordered.push(list.shift()); - progress = true; - } - } - return ordered.map((candidate, index) => ({ - ...candidate, - rank_score: 1 - index * 8e-4 - })); -} -function rankCacheKey(viewerId, sessionId, categorySlug, profileVersion) { - return `${viewerId || sessionId || "anon"}:${categorySlug || "all"}:${profileVersion}`; -} -function getRankCache(key) { - const cached = rankSessionCache.get(key); - if (!cached) return null; - if (cached.expiresAt <= Date.now()) { - rankSessionCache.delete(key); - return null; - } - return cached; -} -function setRankCache(key, orderedItems) { - if (rankSessionCache.size >= RANK_CACHE_MAX) { - const oldestKey = rankSessionCache.keys().next().value; - if (oldestKey) rankSessionCache.delete(oldestKey); - } - rankSessionCache.set(key, { - orderedItems, - expiresAt: Date.now() + RANK_CACHE_TTL_MS - }); -} -function buildTagMatchClause(tags, params) { - if (tags.length === 0) return { clause: " AND 1 = 0", params }; - const parts = tags.map((tag) => { - params.push(JSON.stringify(tag)); - return 'JSON_CONTAINS(pp.tags, ?, "$")'; - }); - return { clause: ` AND (${parts.join(" OR ")})`, params }; -} -function createPlazaRecommendService(pool2, { - eventService, - formatPostRow: formatPostRow2, - loadViewerReactions = null, - algorithmConfig = null, - config: configOverride = null -}) { - const config = { ...DEFAULT_RECOMMEND_CONFIG, ...configOverride }; - const rowToCandidate = (row) => ({ - id: row.id, - category_slug: row.category_slug, - category_name: row.category_name, - category_icon: row.category_icon, - author_id: row.user_id, - tags: parseTags(row.tags), - hot_score: Number(row.hot_score ?? 0), - published_at: Number(row.published_at), - view_count: Number(row.view_count ?? 0), - like_count: Number(row.like_count ?? 0), - collect_count: Number(row.collect_count ?? 0), - comment_count: Number(row.comment_count ?? 0), - row - }); - const loadPublishedRows = async ({ - categorySlug = null, - limit = CANDIDATE_POOL_LIMIT, - orderBy = "pp.hot_score DESC, pp.id DESC" - }) => { - const params = ["published"]; - let filter = ""; - if (categorySlug) { - filter += " AND c.slug = ?"; - params.push(categorySlug); - } - params.push(limit); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.status = ? ${filter} - ORDER BY ${orderBy} - LIMIT ?`, - params - ); - return rows; - }; - const scoreCandidate = (candidate, profile, now, maxHotScore, categorySlug) => { - if (profile.dislikedPostIds.has(candidate.id)) { - return { ...candidate, rank_score: -999, features: { filtered: true } }; - } - if (profile.dislikedCategorySlugs.has(candidate.category_slug)) { - return { ...candidate, rank_score: -999, features: { filtered: true } }; - } - const categoryAffinity = profile.categoryWeights[candidate.category_slug] ?? 0; - const tagAffinity = jaccard(candidate.tags, topWeightedKeys(profile.tagWeights, 20)); - const authorAffinity = profile.authorWeights[candidate.author_id] ?? 0; - const browseAffinity = !categorySlug && profile.feedCategoryWeights ? profile.feedCategoryWeights[candidate.category_slug] ?? 0 : 0; - const followBoost = profile.followedAuthorIds.has(candidate.author_id) ? config.follow_boost : 0; - const hot = candidate.hot_score / Math.max(maxHotScore, 1); - const fresh = freshnessScore(candidate.published_at, now); - const quality = qualityScore(candidate); - const ctr = ctrPrior(candidate); - const seenPenalty = profile.deepSeenPostIds.has(candidate.id) ? config.w_seen_penalty : profile.seenPostIds.has(candidate.id) ? config.w_impression_penalty : 0; - const dislikePenalty = profile.dislikedPostIds.has(candidate.id) ? config.w_dislike_penalty : 0; - const categoryDislikePenalty = profile.dislikedCategorySlugs.has(candidate.category_slug) ? config.w_category_dislike : 0; - const channelBoost = Math.min( - config.channel_boost_cap, - (candidate.recall_channels ?? []).reduce( - (sum, channel) => sum + (CHANNEL_WEIGHTS[channel] ?? 0) * 0.05, - 0 - ) - ); - const rankScore = config.w_category * categoryAffinity + config.w_tag * tagAffinity + config.w_author * (authorAffinity + followBoost) + config.w_browse * browseAffinity + config.w_hot * hot + config.w_fresh * fresh + config.w_quality * quality + config.w_ctr * ctr + channelBoost - seenPenalty - dislikePenalty - categoryDislikePenalty; - return { - ...candidate, - rank_score: rankScore, - features: { - category_affinity: categoryAffinity, - tag_affinity: tagAffinity, - author_affinity: authorAffinity, - browse_affinity: browseAffinity, - follow_boost: followBoost, - hot, - fresh, - quality, - ctr, - seen_penalty: seenPenalty, - dislike_penalty: dislikePenalty, - category_dislike_penalty: categoryDislikePenalty, - channel_boost: channelBoost - } - }; - }; - const mergeRecall = (target, rows, channel) => { - for (const row of rows) { - const candidate = rowToCandidate(row); - const existing = target.get(candidate.id); - if (existing) { - existing.recall_channels = [.../* @__PURE__ */ new Set([...existing.recall_channels ?? [], channel])]; - existing.recall_score = Math.max(existing.recall_score ?? 0, CHANNEL_WEIGHTS[channel] ?? 0); - continue; - } - target.set(candidate.id, { - ...candidate, - recall_channels: [channel], - recall_score: CHANNEL_WEIGHTS[channel] ?? 0 - }); - } - }; - const recallInterest = async (profile, categorySlug) => { - const categories = topWeightedKeys(profile.categoryWeights, 4); - if (categories.length === 0) return []; - const params = ["published", ...categories]; - let filter = ` AND c.slug IN (${categories.map(() => "?").join(",")})`; - if (categorySlug) { - filter += " AND c.slug = ?"; - params.push(categorySlug); - } - params.push(RECALL_LIMIT); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.status = ? ${filter} - ORDER BY pp.hot_score DESC, pp.published_at DESC - LIMIT ?`, - params - ); - return rows; - }; - const recallFollow = async (profile, categorySlug) => { - const authors = [...profile.followedAuthorIds]; - if (authors.length === 0) return []; - const params = ["published", ...authors]; - let filter = ` AND pp.user_id IN (${authors.map(() => "?").join(",")})`; - if (categorySlug) { - filter += " AND c.slug = ?"; - params.push(categorySlug); - } - params.push(RECALL_LIMIT); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.status = ? ${filter} - ORDER BY pp.published_at DESC - LIMIT ?`, - params - ); - return rows; - }; - const recallSimilar = async (profile, userId, categorySlug) => { - const seeds = [...profile.likedPostIds].slice(0, 12); - if (seeds.length === 0) return []; - const params = ["published", ...seeds]; - let filter = ` AND r1.post_id IN (${seeds.map(() => "?").join(",")})`; - if (userId) { - filter += " AND r2.user_id <> ?"; - params.push(userId); - } - if (categorySlug) { - filter += " AND c.slug = ?"; - params.push(categorySlug); - } - params.push(RECALL_LIMIT); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, - pr.public_url, - COUNT(*) AS overlap_score - FROM plaza_reactions r1 - JOIN plaza_reactions r2 - ON r2.user_id = r1.user_id - AND r2.type IN ('like', 'collect') - AND r2.post_id <> r1.post_id - JOIN plaza_posts pp ON pp.id = r2.post_id AND pp.status = 'published' - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE r1.type IN ('like', 'collect') ${filter} - GROUP BY pp.id, c.name, c.slug, c.icon - ORDER BY overlap_score DESC, pp.hot_score DESC - LIMIT ?`, - params - ); - return rows; - }; - const recallFresh = async (profile, categorySlug) => { - const categories = topWeightedKeys(profile.categoryWeights, 3); - const params = ["published"]; - let filter = ""; - if (categorySlug) { - filter += " AND c.slug = ?"; - params.push(categorySlug); - } else if (categories.length > 0) { - filter += ` AND c.slug IN (${categories.map(() => "?").join(",")})`; - params.push(...categories); - } - params.push(RECALL_LIMIT); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.status = ? ${filter} - ORDER BY pp.published_at DESC - LIMIT ?`, - params - ); - return rows; - }; - const recallExplore = async (profile, categorySlug) => { - const dominant = new Set(topWeightedKeys(profile.categoryWeights, 2)); - const params = ["published"]; - let filter = ""; - if (categorySlug) { - filter += " AND c.slug = ?"; - params.push(categorySlug); - } else if (dominant.size > 0) { - filter += ` AND c.slug NOT IN (${[...dominant].map(() => "?").join(",")})`; - params.push(...dominant); - } - params.push(Math.max(12, Math.floor(RECALL_LIMIT * config.explore_ratio))); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.status = ? ${filter} - ORDER BY pp.published_at DESC, pp.hot_score DESC - LIMIT ?`, - params - ); - return rows; - }; - const recallByTags = async (profile, categorySlug) => { - const tags = topWeightedKeys(profile.tagWeights, 6); - if (tags.length === 0) return []; - const params = ["published"]; - let filter = ""; - if (categorySlug) { - filter += " AND c.slug = ?"; - params.push(categorySlug); - } - const tagMatch = buildTagMatchClause(tags, params); - filter += tagMatch.clause; - params.push(RECALL_LIMIT); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.status = ? ${filter} - ORDER BY pp.hot_score DESC, pp.published_at DESC - LIMIT ?`, - params - ); - return rows; - }; - const recallContentSimilar = async (profile, categorySlug) => { - const seeds = [...profile.likedPostIds].slice(0, 8); - if (seeds.length === 0) return []; - const [seedRows] = await pool2.query( - `SELECT tags FROM plaza_posts WHERE id IN (${seeds.map(() => "?").join(",")}) AND status = 'published'`, - seeds - ); - const tagSet = /* @__PURE__ */ new Set(); - for (const row of seedRows) { - for (const tag of parseTags(row.tags)) tagSet.add(tag); - } - const tags = [...tagSet].slice(0, 10); - if (tags.length === 0) return []; - const params = ["published"]; - let filter = ""; - if (categorySlug) { - filter += " AND c.slug = ?"; - params.push(categorySlug); - } - if (seeds.length > 0) { - filter += ` AND pp.id NOT IN (${seeds.map(() => "?").join(",")})`; - params.push(...seeds); - } - const tagMatch = buildTagMatchClause(tags, params); - filter += tagMatch.clause; - params.push(RECALL_LIMIT); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon, pr.public_url - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - JOIN h5_publish_records pr ON pr.id = pp.publication_id - WHERE pp.status = ? ${filter} - ORDER BY pp.published_at DESC, pp.hot_score DESC - LIMIT ?`, - params - ); - return rows; - }; - const recallHotFallback = async (categorySlug) => loadPublishedRows({ - categorySlug, - limit: RECALL_LIMIT, - orderBy: "pp.hot_score DESC, pp.id DESC" - }); - const buildRecallPool = async ({ profile, viewerId, categorySlug }) => { - const poolMap = /* @__PURE__ */ new Map(); - const [ - interestRows, - followRows, - similarRows, - tagRows, - tagSimilarRows, - freshRows, - exploreRows, - hotRows - ] = await Promise.all([ - recallInterest(profile, categorySlug), - recallFollow(profile, categorySlug), - recallSimilar(profile, viewerId, categorySlug), - recallByTags(profile, categorySlug), - recallContentSimilar(profile, categorySlug), - recallFresh(profile, categorySlug), - recallExplore(profile, categorySlug), - recallHotFallback(categorySlug) - ]); - mergeRecall(poolMap, interestRows, "interest"); - mergeRecall(poolMap, followRows, "follow"); - mergeRecall(poolMap, similarRows, "similar"); - mergeRecall(poolMap, tagRows, "tag"); - mergeRecall(poolMap, tagSimilarRows, "tag_similar"); - mergeRecall(poolMap, freshRows, "fresh"); - mergeRecall(poolMap, exploreRows, "explore"); - mergeRecall(poolMap, hotRows, "hot"); - if (poolMap.size < 24) { - mergeRecall( - poolMap, - await loadPublishedRows({ categorySlug, limit: CANDIDATE_POOL_LIMIT }), - "hot" - ); - } - return poolMap; - }; - const listRecommendedFeed = async ({ - viewerId = null, - sessionId = null, - categorySlug = null, - cursor = null, - limit = 20 - } = {}) => { - const pageLimit = Math.min(Math.max(1, Number(limit) || 20), 50); - const halfLifeMs = Number(config.profile_half_life_days ?? 7) * 24 * 60 * 60 * 1e3; - const profile = await eventService.getProfile({ - userId: viewerId, - sessionId, - halfLifeMs - }); - const parsedCursor = parseRecommendCursor(cursor); - if (parsedCursor.profileVersion != null && parsedCursor.profileVersion !== profile.profileVersion && parsedCursor.offset > 0) { - parsedCursor.offset = 0; - } - const cacheKey = rankCacheKey(viewerId, sessionId, categorySlug, profile.profileVersion); - const isColdStart = profile.eventCount === 0 && profile.likedPostIds.size === 0; - let reranked = null; - if (parsedCursor.offset > 0) { - const cached = getRankCache(cacheKey); - if (cached) reranked = cached.orderedItems; - } - const recallPool = reranked ? null : await buildRecallPool({ - profile, - viewerId, - categorySlug - }); - const now = Date.now(); - const hotConfig = algorithmConfig ?? {}; - if (!reranked) { - const scored = [...recallPool.values()].map((candidate) => { - if (!candidate.hot_score) { - candidate.hot_score = computeHotScore(candidate.row, hotConfig, now); - } - return candidate; - }); - const maxHotScore = Math.max(...scored.map((candidate) => candidate.hot_score), 1); - let ranked; - if (isColdStart && !categorySlug) { - ranked = coldStartInterleave(scored, sessionId || viewerId || "anon"); - } else { - ranked = scored.map((candidate) => scoreCandidate(candidate, profile, now, maxHotScore, categorySlug)).filter((candidate) => candidate.rank_score > -100).sort((a, b) => b.rank_score - a.rank_score); - } - reranked = mmrRerank(ranked, { - lambda: config.mmr_lambda, - limit: Math.max(ranked.length, parsedCursor.offset + pageLimit + 24) - }); - setRankCache(cacheKey, reranked); - } - const pageItems = reranked.slice(parsedCursor.offset, parsedCursor.offset + pageLimit); - const hasMore = reranked.length > parsedCursor.offset + pageLimit; - let reactionMap = /* @__PURE__ */ new Map(); - if (viewerId && loadViewerReactions) { - reactionMap = await loadViewerReactions( - viewerId, - pageItems.map((item) => item.id) - ); - } - const posts = pageItems.map( - (item) => formatPostRow2(item.row, { - viewerReacted: viewerId ? reactionMap.get(item.id) ?? null : null - }) - ); - return { - posts, - featured: { homepage_banner: [], trending: [] }, - next_cursor: hasMore ? encodeRecommendCursor({ - offset: parsedCursor.offset + pageLimit, - profileVersion: profile.profileVersion - }) : null, - has_more: hasMore, - recommend_meta: { - profile_version: profile.profileVersion, - candidate_count: recallPool?.size ?? reranked.length, - event_count: profile.eventCount, - cold_start: isColdStart, - top_categories: topWeightedKeys(profile.categoryWeights, 3), - top_tags: topWeightedKeys(profile.tagWeights, 5), - session_cached: parsedCursor.offset > 0 - } - }; - }; - return { - listRecommendedFeed, - internals: { - parseRecommendCursor, - encodeRecommendCursor, - scoreCandidate, - mmrRerank, - jaccard, - itemSimilarity, - DEFAULT_RECOMMEND_CONFIG, - coldStartInterleave, - seededShuffle, - rankCacheKey - } - }; -} - -// plaza-interactions.mjs -import crypto17 from "node:crypto"; -var REACTION_TYPES = /* @__PURE__ */ new Set(["like", "collect", "share"]); -var COUNTER_BY_TYPE = { - like: "like_count", - collect: "collect_count", - share: "share_count" -}; -function plazaError3(message, code, details) { - return Object.assign(new Error(message), { code, details }); -} -function clampLimit2(value, fallback = 20, max = 50) { - const limit = Number(value ?? fallback); - if (!Number.isFinite(limit) || limit < 1) return fallback; - return Math.min(Math.floor(limit), max); -} -function normalizeReactionType(value) { - const type = String(value ?? "").trim(); - if (!REACTION_TYPES.has(type)) throw plazaError3("\u4E0D\u652F\u6301\u7684\u4E92\u52A8\u7C7B\u578B", "invalid_input"); - return type; -} -function formatCommentAuthor(row) { - return { - user_id: row.user_id, - slug: row.author_slug || row.slug || row.username, - display_name: row.author_display_name || row.display_name || row.username, - avatar_url: "" - }; -} -function createPlazaInteractionService(pool2, { idFactory = () => crypto17.randomUUID(), formatPostRow: formatPostRow2, plazaRedis: plazaRedis2 = null } = {}) { - const requirePublishedPost = async (postId, conn = pool2) => { - const [rows] = await conn.query( - `SELECT * FROM plaza_posts WHERE id = ? AND status = 'published' LIMIT 1`, - [postId] - ); - if (!rows[0]) throw plazaError3("\u5E16\u5B50\u4E0D\u5B58\u5728\u6216\u5DF2\u9690\u85CF", "POST_NOT_FOUND"); - return rows[0]; - }; - const loadViewerReactions = async (viewerId, postIds) => { - const map = /* @__PURE__ */ new Map(); - if (!viewerId || postIds.length === 0) return map; - const placeholders = postIds.map(() => "?").join(", "); - const [rows] = await pool2.query( - `SELECT post_id, type FROM plaza_reactions - WHERE user_id = ? AND post_id IN (${placeholders})`, - [viewerId, ...postIds] - ); - for (const id of postIds) { - map.set(id, { liked: false, collected: false }); - } - for (const row of rows) { - const current = map.get(row.post_id) ?? { liked: false, collected: false }; - if (row.type === "like") current.liked = true; - if (row.type === "collect") current.collected = true; - map.set(row.post_id, current); - } - return map; - }; - const bumpCounter = async (conn, postId, field, delta) => { - const now = Date.now(); - if (delta > 0) { - await conn.query( - `UPDATE plaza_posts SET ${field} = ${field} + ?, updated_at = ? WHERE id = ?`, - [delta, now, postId] - ); - return; - } - await conn.query( - `UPDATE plaza_posts SET ${field} = GREATEST(CAST(${field} AS SIGNED) + ?, 0), updated_at = ? WHERE id = ?`, - [delta, now, postId] - ); - }; - const addReaction = async (userId, postId, typeInput) => { - const type = normalizeReactionType(typeInput); - const field = COUNTER_BY_TYPE[type]; - await requirePublishedPost(postId); - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [existing] = await conn.query( - `SELECT id FROM plaza_reactions WHERE post_id = ? AND user_id = ? AND type = ? LIMIT 1`, - [postId, userId, type] - ); - if (!existing[0]) { - await conn.query( - `INSERT INTO plaza_reactions (id, post_id, user_id, type, created_at) VALUES (?, ?, ?, ?, ?)`, - [idFactory(), postId, userId, type, Date.now()] - ); - await bumpCounter(conn, postId, field, 1); - } - await conn.commit(); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - return { post_id: postId, type, active: true }; - }; - const removeReaction = async (userId, postId, typeInput) => { - const type = normalizeReactionType(typeInput); - const field = COUNTER_BY_TYPE[type]; - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [result] = await conn.query( - `DELETE FROM plaza_reactions WHERE post_id = ? AND user_id = ? AND type = ?`, - [postId, userId, type] - ); - if (result.affectedRows > 0) { - await bumpCounter(conn, postId, field, -1); - } - await conn.commit(); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - return { post_id: postId, type, active: false }; - }; - const listComments = async (postId, { cursor = null, limit = 20, parentId = null, viewerId = null } = {}) => { - await requirePublishedPost(postId); - const pageLimit = clampLimit2(limit); - const params = [postId, "visible"]; - let parentClause = "AND c.parent_id IS NULL"; - if (parentId) { - parentClause = "AND c.parent_id = ?"; - params.push(parentId); - } - let cursorClause = ""; - if (cursor) { - cursorClause = "AND c.created_at < (SELECT created_at FROM plaza_comments WHERE id = ? LIMIT 1)"; - params.push(cursor); - } - params.push(pageLimit + 1); - const [rows] = await pool2.query( - `SELECT c.*, u.slug AS author_slug, u.username, u.display_name AS author_display_name - FROM plaza_comments c - JOIN h5_users u ON u.id = c.user_id - WHERE c.post_id = ? AND c.status = ? ${parentClause} ${cursorClause} - ORDER BY c.created_at DESC - LIMIT ?`, - params - ); - const hasMore = rows.length > pageLimit; - const pageRows = hasMore ? rows.slice(0, pageLimit) : rows; - let likedSet = /* @__PURE__ */ new Set(); - if (viewerId && pageRows.length > 0) { - const ids = pageRows.map((row) => row.id); - const [likedRows] = await pool2.query( - `SELECT comment_id FROM plaza_comment_reactions - WHERE user_id = ? AND comment_id IN (${ids.map(() => "?").join(", ")})`, - [viewerId, ...ids] - ); - likedSet = new Set(likedRows.map((row) => row.comment_id)); - } - const comments = pageRows.map((row) => ({ - id: row.id, - content: row.content, - author: formatCommentAuthor(row), - like_count: Number(row.like_count ?? 0), - reply_count: Number(row.reply_count ?? 0), - viewer_liked: viewerId ? likedSet.has(row.id) : false, - created_at: new Date(Number(row.created_at)).toISOString(), - status: row.status, - parent_id: row.parent_id - })); - return { - comments, - next_cursor: hasMore ? pageRows[pageRows.length - 1].id : null, - has_more: hasMore - }; - }; - const createComment = async (userId, postId, { content, parent_id: parentId = null }) => { - const text = String(content ?? "").trim(); - if (!text) throw plazaError3("\u8BC4\u8BBA\u4E0D\u80FD\u4E3A\u7A7A", "invalid_input"); - if (text.length > 500) throw plazaError3("\u8BC4\u8BBA\u8D85\u51FA 500 \u5B57\u7B26", "COMMENT_TOO_LONG"); - const post = await requirePublishedPost(postId); - if (!post.allow_comment) throw plazaError3("\u8BE5\u5E16\u5B50\u5DF2\u5173\u95ED\u8BC4\u8BBA", "COMMENT_DISABLED"); - const [users] = await pool2.query( - `SELECT plaza_comment_banned FROM h5_users WHERE id = ? LIMIT 1`, - [userId] - ); - if (users[0]?.plaza_comment_banned) { - throw plazaError3("\u65E0\u8BC4\u8BBA\u6743\u9650", "POST_PERMISSION_DENIED"); - } - if (plazaRedis2?.checkCommentRateLimit) { - const rate = await plazaRedis2.checkCommentRateLimit(userId, postId); - if (!rate.allowed) { - throw plazaError3("\u8BC4\u8BBA\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5", "COMMENT_RATE_LIMITED"); - } - } else { - const hourAgo = Date.now() - 36e5; - const [recent] = await pool2.query( - `SELECT COUNT(*) AS count FROM plaza_comments - WHERE user_id = ? AND post_id = ? AND created_at >= ? AND status = 'visible'`, - [userId, postId, hourAgo] - ); - if (Number(recent[0]?.count ?? 0) >= 10) { - throw plazaError3("\u8BC4\u8BBA\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5", "COMMENT_RATE_LIMITED"); - } - } - let parentComment = null; - if (parentId) { - const [parents] = await pool2.query( - `SELECT * FROM plaza_comments WHERE id = ? AND post_id = ? AND status = 'visible' LIMIT 1`, - [parentId, postId] - ); - parentComment = parents[0]; - if (!parentComment) throw plazaError3("\u7236\u8BC4\u8BBA\u4E0D\u5B58\u5728", "COMMENT_NOT_FOUND"); - if (parentComment.parent_id) { - throw plazaError3("\u4E0D\u5141\u8BB8\u4E09\u7EA7\u5D4C\u5957\u56DE\u590D", "REPLY_DEPTH_EXCEEDED"); - } - } - const now = Date.now(); - const commentId = idFactory(); - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - await conn.query( - `INSERT INTO plaza_comments - (id, post_id, user_id, parent_id, content, status, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, 'visible', ?, ?)`, - [commentId, postId, userId, parentId, text, now, now] - ); - if (parentComment) { - await conn.query( - `UPDATE plaza_comments SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?`, - [now, parentId] - ); - } else { - await conn.query( - `UPDATE plaza_posts SET comment_count = comment_count + 1, updated_at = ? WHERE id = ?`, - [now, postId] - ); - } - await conn.commit(); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - await plazaRedis2?.markCommentRateLimit?.(userId, postId); - return { id: commentId }; - }; - const deleteComment = async (userId, commentId) => { - const [rows] = await pool2.query( - `SELECT * FROM plaza_comments WHERE id = ? AND user_id = ? LIMIT 1`, - [commentId, userId] - ); - if (!rows[0]) throw plazaError3("\u8BC4\u8BBA\u4E0D\u5B58\u5728", "COMMENT_NOT_FOUND"); - const now = Date.now(); - await pool2.query( - `UPDATE plaza_comments - SET content = '', status = 'deleted', deleted_by = 'user', updated_at = ? - WHERE id = ?`, - [now, commentId] - ); - return { id: commentId, status: "deleted" }; - }; - const toggleCommentLike = async (userId, commentId, liked) => { - const [rows] = await pool2.query( - `SELECT c.* FROM plaza_comments c - JOIN plaza_posts p ON p.id = c.post_id - WHERE c.id = ? AND c.status = 'visible' AND p.status = 'published' - LIMIT 1`, - [commentId] - ); - if (!rows[0]) throw plazaError3("\u8BC4\u8BBA\u4E0D\u5B58\u5728", "COMMENT_NOT_FOUND"); - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - if (liked) { - const [existing] = await conn.query( - `SELECT id FROM plaza_comment_reactions WHERE comment_id = ? AND user_id = ? LIMIT 1`, - [commentId, userId] - ); - if (!existing[0]) { - await conn.query( - `INSERT INTO plaza_comment_reactions (id, comment_id, user_id, created_at) VALUES (?, ?, ?, ?)`, - [idFactory(), commentId, userId, Date.now()] - ); - await conn.query( - `UPDATE plaza_comments SET like_count = like_count + 1, updated_at = ? WHERE id = ?`, - [Date.now(), commentId] - ); - } - } else { - const [result] = await conn.query( - `DELETE FROM plaza_comment_reactions WHERE comment_id = ? AND user_id = ?`, - [commentId, userId] - ); - if (result.affectedRows > 0) { - await conn.query( - `UPDATE plaza_comments SET like_count = GREATEST(CAST(like_count AS SIGNED) - 1, 0), updated_at = ? WHERE id = ?`, - [Date.now(), commentId] - ); - } - } - await conn.commit(); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - return { id: commentId, liked }; - }; - const resolveUserBySlug = async (slug) => { - const [rows] = await pool2.query( - `SELECT id, slug, username, display_name, - plaza_post_count, plaza_follower_count, plaza_following_count - FROM h5_users WHERE slug = ? OR username = ? LIMIT 1`, - [slug, slug] - ); - const row = rows[0]; - if (!row) throw plazaError3("\u7528\u6237\u4E0D\u5B58\u5728", "user_not_found"); - return row; - }; - const followUser = async (followerId, followeeSlug) => { - const followee = await resolveUserBySlug(followeeSlug); - if (followee.id === followerId) throw plazaError3("\u4E0D\u80FD\u5173\u6CE8\u81EA\u5DF1", "SELF_FOLLOW"); - const now = Date.now(); - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [result] = await conn.query( - `INSERT IGNORE INTO plaza_follows (follower_id, followee_id, created_at) VALUES (?, ?, ?)`, - [followerId, followee.id, now] - ); - if (result.affectedRows > 0) { - await conn.query( - `UPDATE h5_users SET plaza_following_count = plaza_following_count + 1, updated_at = ? WHERE id = ?`, - [now, followerId] - ); - await conn.query( - `UPDATE h5_users SET plaza_follower_count = plaza_follower_count + 1, updated_at = ? WHERE id = ?`, - [now, followee.id] - ); - } - await conn.commit(); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - return { following: true }; - }; - const unfollowUser = async (followerId, followeeSlug) => { - const followee = await resolveUserBySlug(followeeSlug); - const now = Date.now(); - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [result] = await conn.query( - `DELETE FROM plaza_follows WHERE follower_id = ? AND followee_id = ?`, - [followerId, followee.id] - ); - if (result.affectedRows > 0) { - await conn.query( - `UPDATE h5_users SET plaza_following_count = GREATEST(CAST(plaza_following_count AS SIGNED) - 1, 0), updated_at = ? WHERE id = ?`, - [now, followerId] - ); - await conn.query( - `UPDATE h5_users SET plaza_follower_count = GREATEST(CAST(plaza_follower_count AS SIGNED) - 1, 0), updated_at = ? WHERE id = ?`, - [now, followee.id] - ); - } - await conn.commit(); - } catch (error) { - await conn.rollback(); - throw error; - } finally { - conn.release(); - } - return { following: false }; - }; - const getUserProfile = async (slug, viewerId = null) => { - const user = await resolveUserBySlug(slug); - const [likeRows] = await pool2.query( - `SELECT COALESCE(SUM(like_count), 0) AS total_likes - FROM plaza_posts WHERE user_id = ? AND status = 'published'`, - [user.id] - ); - const [recentRows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - WHERE pp.user_id = ? AND pp.status = 'published' - ORDER BY pp.published_at DESC - LIMIT 6`, - [user.id] - ); - let viewerFollowing = false; - if (viewerId && viewerId !== user.id) { - const [followRows] = await pool2.query( - `SELECT 1 FROM plaza_follows WHERE follower_id = ? AND followee_id = ? LIMIT 1`, - [viewerId, user.id] - ); - viewerFollowing = followRows.length > 0; - } - const recentPosts = recentRows.map( - (row) => formatPostRow2 ? formatPostRow2(row, { viewerReacted: null }) : row - ); - return { - user: { - user_id: user.id, - slug: user.slug || user.username, - display_name: user.display_name || user.username, - avatar_url: "", - bio: "", - stats: { - post_count: Number(user.plaza_post_count ?? 0), - follower_count: Number(user.plaza_follower_count ?? 0), - following_count: Number(user.plaza_following_count ?? 0), - total_likes: Number(likeRows[0]?.total_likes ?? 0) - }, - viewer_following: viewerId ? viewerFollowing : void 0 - }, - recent_posts: recentPosts - }; - }; - const listUserPosts = async (slug, { cursor = null, limit = 20, viewerId = null } = {}) => { - const user = await resolveUserBySlug(slug); - const pageLimit = clampLimit2(limit); - const params = [user.id, "published"]; - let cursorClause = ""; - if (cursor) { - cursorClause = "AND (pp.published_at < (SELECT published_at FROM plaza_posts WHERE id = ? LIMIT 1) OR (pp.published_at = (SELECT published_at FROM plaza_posts WHERE id = ? LIMIT 1) AND pp.id < ?))"; - params.push(cursor, cursor, cursor); - } - params.push(pageLimit + 1); - const [rows] = await pool2.query( - `SELECT pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - WHERE pp.user_id = ? AND pp.status = ? ${cursorClause} - ORDER BY pp.published_at DESC, pp.id DESC - LIMIT ?`, - params - ); - const hasMore = rows.length > pageLimit; - const pageRows = hasMore ? rows.slice(0, pageLimit) : rows; - const reactions = await loadViewerReactions( - viewerId, - pageRows.map((row) => row.id) - ); - const posts = pageRows.map( - (row) => formatPostRow2 ? formatPostRow2(row, { viewerReacted: viewerId ? reactions.get(row.id) ?? null : null }) : row - ); - return { - posts, - next_cursor: hasMore ? pageRows[pageRows.length - 1].id : null, - has_more: hasMore - }; - }; - return { - loadViewerReactions, - addReaction, - removeReaction, - listComments, - createComment, - deleteComment, - toggleCommentLike, - followUser, - unfollowUser, - getUserProfile, - listUserPosts - }; -} - -// plaza-redis.mjs -import crypto18 from "node:crypto"; -var SYNC_SET = "plaza:sync:post_ids"; -var FEED_GEN_KEY = "plaza:feed:gen"; -function hashIp(ip) { - return crypto18.createHash("sha256").update(String(ip ?? "unknown")).digest("hex").slice(0, 16); -} -function counterKey(postId, field) { - return `plaza:post:${postId}:${field}`; -} -function viewDedupKey(postId, ipHash) { - return `plaza:view:${ipHash}:${postId}`; -} -function feedCacheKey(gen, sort, categorySlug, cursor) { - const category = categorySlug || "all"; - const page = cursor || "root"; - return `plaza:feed:${sort}:${category}:cursor:${page}:g${gen}`; -} -function createNoopPlazaRedis(pool2 = null) { - const viewDedup = /* @__PURE__ */ new Map(); - return { - enabled: false, - async connect() { - }, - async disconnect() { - }, - async recordView(postId, ip) { - if (!pool2) return { counted: false }; - const ipHash = hashIp(ip); - const dedupKey = `${ipHash}:${postId}`; - if (viewDedup.has(dedupKey)) return { counted: false }; - viewDedup.set(dedupKey, Date.now()); - setTimeout(() => viewDedup.delete(dedupKey), 864e5).unref?.(); - const now = Date.now(); - const [result] = await pool2.query( - `UPDATE plaza_posts SET view_count = view_count + 1, updated_at = ? - WHERE id = ? AND status = 'published'`, - [now, postId] - ); - return { counted: (result.affectedRows ?? 0) > 0 }; - }, - async getFeedCache() { - return null; - }, - async setFeedCache() { - }, - async invalidateFeedCaches() { - }, - async syncCountersToMySQL() { - return { synced: 0 }; - }, - async checkCommentRateLimit() { - return { allowed: true }; - }, - async markCommentRateLimit() { - } - }; -} -async function createPlazaRedis(redisUrl, pool2) { - if (!redisUrl) return createNoopPlazaRedis(pool2); - const { createClient } = await import("redis"); - const client = createClient({ url: redisUrl }); - client.on("error", (error) => { - console.error("Plaza Redis error:", error.message); - }); - await client.connect(); - const getFeedGen = async () => Number(await client.get(FEED_GEN_KEY) ?? 0); - return { - enabled: true, - client, - async disconnect() { - if (client.isOpen) await client.disconnect(); - }, - async recordView(postId, ip) { - const ipHash = hashIp(ip); - const dedup = viewDedupKey(postId, ipHash); - const inserted = await client.set(dedup, "1", { NX: true, EX: 86400 }); - if (!inserted) return { counted: false }; - await client.incr(counterKey(postId, "view_count")); - await client.sAdd(SYNC_SET, postId); - return { counted: true }; - }, - async getFeedCache(sort, categorySlug, cursor) { - const gen = await getFeedGen(); - const key = feedCacheKey(gen, sort, categorySlug, cursor); - const raw = await client.get(key); - if (!raw) return null; - try { - return JSON.parse(raw); - } catch { - return null; - } - }, - async setFeedCache(sort, categorySlug, cursor, payload, ttlSeconds = 300) { - const gen = await getFeedGen(); - const key = feedCacheKey(gen, sort, categorySlug, cursor); - await client.set(key, JSON.stringify(payload), { EX: ttlSeconds }); - }, - async invalidateFeedCaches() { - await client.incr(FEED_GEN_KEY); - }, - async syncCountersToMySQL() { - const postIds = await client.sMembers(SYNC_SET); - if (postIds.length === 0) return { synced: 0 }; - let synced = 0; - const now = Date.now(); - for (const postId of postIds) { - const delta = Number(await client.get(counterKey(postId, "view_count")) ?? 0); - if (delta <= 0) { - await client.sRem(SYNC_SET, postId); - continue; - } - await pool2.query( - `UPDATE plaza_posts SET view_count = view_count + ?, updated_at = ? WHERE id = ?`, - [delta, now, postId] - ); - await client.decrBy(counterKey(postId, "view_count"), delta); - const remaining = Number(await client.get(counterKey(postId, "view_count")) ?? 0); - if (remaining <= 0) { - await client.del(counterKey(postId, "view_count")); - await client.sRem(SYNC_SET, postId); - } - synced += 1; - } - return { synced }; - }, - async checkCommentRateLimit(userId, postId, maxPerHour = 10) { - const key = `plaza:comment_rate:${userId}:${postId}`; - const count = Number(await client.get(key) ?? 0); - return { allowed: count < maxPerHour, count }; - }, - async markCommentRateLimit(userId, postId) { - const key = `plaza:comment_rate:${userId}:${postId}`; - const count = await client.incr(key); - if (count === 1) await client.expire(key, 3600); - return count; - } - }; -} - -// plaza-tasks.mjs -function startPlazaTasks({ - pool: pool2, - plazaRedis: plazaRedis2, - recalculateHotScores: recalculateHotScores2, - writebackPublications: writebackPublications2 -}) { - const timers = []; - const schedule = (fn, intervalMs, label) => { - const run = async () => { - try { - await fn(); - } catch (error) { - console.error(`Plaza task ${label} failed:`, error.message); - } - }; - void run(); - const timer = setInterval(run, intervalMs); - timer.unref?.(); - timers.push(timer); - }; - schedule(async () => { - const result = await recalculateHotScores2(pool2); - if (result.updated > 0) await plazaRedis2.invalidateFeedCaches(); - }, 10 * 60 * 1e3, "hot_score"); - schedule(async () => { - await plazaRedis2.syncCountersToMySQL(); - }, 5 * 60 * 1e3, "redis_sync"); - schedule(async () => { - await writebackPublications2(pool2); - }, 60 * 60 * 1e3, "publication_writeback"); - return { - stop() { - for (const timer of timers) clearInterval(timer); - } - }; -} -async function writebackPublications(pool2) { - const now = Date.now(); - const [result] = await pool2.query( - `UPDATE h5_publish_records p - JOIN plaza_posts pp ON pp.publication_id = p.id - SET p.plaza_view_count = pp.view_count, - p.plaza_like_count = pp.like_count - WHERE pp.status = 'published'` - ); - return { updated: result.affectedRows ?? 0, at: now }; -} - -// plaza-seo.mjs -import crypto19 from "node:crypto"; -function hashIp2(ip) { - return crypto19.createHash("sha256").update(String(ip ?? "unknown")).digest("hex").slice(0, 32); -} -function normalizeUtm(value, maxLen = 100) { - return String(value ?? "").trim().slice(0, maxLen); -} -function normalizeEventType(value) { - const type = String(value ?? "").trim(); - if (type === "landing" || type === "signup") return type; - throw Object.assign(new Error("\u4E0D\u652F\u6301\u7684 attribution \u4E8B\u4EF6\u7C7B\u578B"), { code: "invalid_input" }); -} -function buildPostPublicUrl(postId, siteBase = process.env.PLAZA_PUBLIC_BASE ?? "https://plaza.tkmind.cn") { - const base = String(siteBase).replace(/\/$/, ""); - return `${base}/plaza/p/${postId}`; -} -function createPlazaSeoService(pool2, { - idFactory = () => crypto19.randomUUID(), - siteBase = process.env.PLAZA_PUBLIC_BASE ?? "https://plaza.tkmind.cn", - baiduToken = process.env.PLAZA_BAIDU_PUSH_TOKEN ?? "", - baiduSite = process.env.PLAZA_BAIDU_SITE ?? "plaza.tkmind.cn" -} = {}) { - const listSitemapData = async ({ postLimit = 1e3, userLimit = 500 } = {}) => { - const [categoryRows] = await pool2.query( - `SELECT slug, name FROM plaza_categories WHERE is_active = 1 ORDER BY sort_order ASC` - ); - const [postRows] = await pool2.query( - `SELECT id, updated_at, published_at - FROM plaza_posts - WHERE status = 'published' - ORDER BY published_at DESC - LIMIT ?`, - [Math.min(Math.max(Number(postLimit) || 1e3, 1), 5e3)] - ); - const [userRows] = await pool2.query( - `SELECT u.slug, u.username, MAX(pp.published_at) AS last_post_at - FROM h5_users u - JOIN plaza_posts pp ON pp.user_id = u.id AND pp.status = 'published' - GROUP BY u.id, u.slug, u.username - ORDER BY last_post_at DESC - LIMIT ?`, - [Math.min(Math.max(Number(userLimit) || 500, 1), 2e3)] - ); - return { - categories: categoryRows.map((row) => ({ - slug: row.slug, - name: row.name - })), - posts: postRows.map((row) => ({ - id: row.id, - updated_at: new Date(Number(row.updated_at ?? row.published_at)).toISOString(), - published_at: new Date(Number(row.published_at)).toISOString() - })), - users: userRows.map((row) => ({ - slug: row.slug || row.username, - last_post_at: row.last_post_at ? new Date(Number(row.last_post_at)).toISOString() : (/* @__PURE__ */ new Date()).toISOString() - })) - }; - }; - const recordAttribution = async ({ - event_type: eventTypeInput, - utm_source: utmSource, - utm_medium: utmMedium, - utm_campaign: utmCampaign, - ref_id: refId, - user_id: userId = null - }, ip = "unknown") => { - const eventType = normalizeEventType(eventTypeInput); - const source = normalizeUtm(utmSource); - if (!source) throw Object.assign(new Error("utm_source \u4E0D\u80FD\u4E3A\u7A7A"), { code: "invalid_input" }); - const id = idFactory(); - const now = Date.now(); - await pool2.query( - `INSERT INTO plaza_attribution_events - (id, event_type, utm_source, utm_medium, utm_campaign, ref_id, user_id, ip_hash, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - id, - eventType, - source, - normalizeUtm(utmMedium), - normalizeUtm(utmCampaign), - normalizeUtm(refId, 200), - userId, - hashIp2(ip), - now - ] - ); - return { id, event_type: eventType }; - }; - const pingBaidu = async (urls) => { - const list = Array.isArray(urls) ? urls.filter(Boolean) : [urls].filter(Boolean); - if (!baiduToken || list.length === 0) { - return { pushed: false, reason: "disabled_or_empty" }; - } - const endpoint = `http://data.zz.baidu.com/urls?site=${encodeURIComponent(baiduSite)}&token=${encodeURIComponent(baiduToken)}`; - const response = await fetch(endpoint, { - method: "POST", - headers: { "Content-Type": "text/plain" }, - body: list.join("\n") - }); - const payload = await response.json().catch(() => ({})); - if (!response.ok) { - throw Object.assign(new Error("\u767E\u5EA6\u63A8\u9001\u5931\u8D25"), { code: "baidu_push_failed", details: payload }); - } - return { pushed: true, result: payload }; - }; - const notifyPostPublished = async (postId) => { - const url = buildPostPublicUrl(postId, siteBase); - try { - return await pingBaidu([url]); - } catch (error) { - console.error("Plaza Baidu push failed:", error.message); - return { pushed: false, reason: error.message }; - } - }; - return { - listSitemapData, - recordAttribution, - pingBaidu, - notifyPostPublished - }; -} - -// plaza-ops.mjs -import crypto20 from "node:crypto"; -var REPORT_REASONS = /* @__PURE__ */ new Set(["spam", "violence", "porn", "political", "privacy", "other"]); -var FEATURED_POSITIONS = /* @__PURE__ */ new Set(["homepage_banner", "category_top", "trending"]); -var OPS_ROLE_RANK = { none: 0, reviewer: 1, editor: 2, ops_admin: 3 }; -function opsError(message, code, details) { - return Object.assign(new Error(message), { code, details }); -} -function clampLimit3(value, fallback = 20, max = 100) { - const limit = Number(value ?? fallback); - if (!Number.isFinite(limit) || limit < 1) return fallback; - return Math.min(Math.floor(limit), max); -} -function hasOpsRole(role, minimum) { - return (OPS_ROLE_RANK[role] ?? 0) >= (OPS_ROLE_RANK[minimum] ?? 0); -} -function createPlazaOpsService(pool2, { - idFactory = () => crypto20.randomUUID(), - formatPostRow: formatPostRow2, - reviewPost, - invalidateFeedCaches = null -} = {}) { - const loadOperatorRole = async (userId) => { - const [rows] = await pool2.query(`SELECT ops_role FROM h5_users WHERE id = ? LIMIT 1`, [userId]); - return rows[0]?.ops_role ?? "none"; - }; - const writeAuditLog = async (operatorId, action, targetType, targetId, detail) => { - await pool2.query( - `INSERT INTO ops_audit_log (id, operator_id, action, target_type, target_id, detail, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - [idFactory(), operatorId, action, targetType, targetId, JSON.stringify(detail ?? {}), Date.now()] - ); - }; - const listReviewQueue = async ({ - status = "pending_review", - cursor = null, - limit = 20, - keyword = null - } = {}) => { - const pageLimit = clampLimit3(limit, 20, 100); - const params = [status]; - let cursorClause = ""; - if (cursor) { - cursorClause = "AND pp.published_at < (SELECT published_at FROM plaza_posts WHERE id = ? LIMIT 1)"; - params.push(cursor); - } - let keywordClause = ""; - if (keyword) { - keywordClause = "AND (pp.title LIKE ? OR pp.user_display_name LIKE ? OR pp.user_slug LIKE ?)"; - const pattern = `%${String(keyword).trim()}%`; - params.push(pattern, pattern, pattern); - } - params.push(pageLimit + 1); - const [rows] = await pool2.query( - `SELECT pp.id, pp.title, pp.summary, pp.cover_url, pp.status, pp.user_display_name, pp.user_slug, - pp.published_at, pp.created_at, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon - FROM plaza_posts pp - JOIN plaza_categories c ON c.id = pp.category_id - WHERE pp.status = ? ${cursorClause} ${keywordClause} - ORDER BY pp.published_at DESC, pp.id DESC - LIMIT ?`, - params - ); - const hasMore = rows.length > pageLimit; - const pageRows = hasMore ? rows.slice(0, pageLimit) : rows; - const now = Date.now(); - const posts = pageRows.map((row) => ({ - id: row.id, - title: row.title, - summary: row.summary ?? "", - cover_url: row.cover_url ?? "", - status: row.status, - author: { display_name: row.user_display_name, slug: row.user_slug }, - category: { name: row.category_name, slug: row.category_slug, icon: row.category_icon ?? "" }, - published_at: new Date(Number(row.published_at)).toISOString(), - sla_warning: status === "pending_review" && now - Number(row.published_at) > 2 * 36e5 - })); - return { - posts, - next_cursor: hasMore ? pageRows[pageRows.length - 1].id : null, - has_more: hasMore - }; - }; - const reviewPostAsOps = async (operatorId, postId, action, { reason = null } = {}) => { - const [beforeRows] = await pool2.query(`SELECT status FROM plaza_posts WHERE id = ? LIMIT 1`, [postId]); - const before = beforeRows[0]; - if (!before) throw opsError("\u5E16\u5B50\u4E0D\u5B58\u5728", "POST_NOT_FOUND"); - const result = await reviewPost(postId, action, { reason }); - await writeAuditLog(operatorId, `${action}_post`, "post", postId, { - before_status: before.status, - after_status: result.status, - reason: reason ?? null - }); - return result; - }; - const batchReviewPosts = async (operatorId, { post_ids: postIds, action, reason = null } = {}) => { - const ids = Array.isArray(postIds) ? postIds.map((id) => String(id).trim()).filter(Boolean) : []; - if (ids.length === 0) throw opsError("post_ids \u4E0D\u80FD\u4E3A\u7A7A", "invalid_input"); - if (ids.length > 50) throw opsError("\u5355\u6B21\u6700\u591A\u5BA1\u6838 50 \u6761", "invalid_input"); - const posts = []; - for (const postId of ids) { - posts.push(await reviewPostAsOps(operatorId, postId, action, { reason })); - } - return { posts }; - }; - const createReport = async (reporterId, { target_type: targetType, target_id: targetId, reason, detail }) => { - const type = String(targetType ?? "").trim(); - if (type !== "post" && type !== "comment") { - throw opsError("\u4E0D\u652F\u6301\u7684\u4E3E\u62A5\u7C7B\u578B", "invalid_input"); - } - const reasonValue = String(reason ?? "").trim(); - if (!REPORT_REASONS.has(reasonValue)) throw opsError("\u4E0D\u652F\u6301\u7684\u4E3E\u62A5\u539F\u56E0", "invalid_input"); - if (type === "post") { - const [rows] = await pool2.query( - `SELECT id FROM plaza_posts WHERE id = ? AND status = 'published' LIMIT 1`, - [targetId] - ); - if (!rows[0]) throw opsError("\u5E16\u5B50\u4E0D\u5B58\u5728", "POST_NOT_FOUND"); - } else { - const [rows] = await pool2.query( - `SELECT id FROM plaza_comments WHERE id = ? AND status = 'visible' LIMIT 1`, - [targetId] - ); - if (!rows[0]) throw opsError("\u8BC4\u8BBA\u4E0D\u5B58\u5728", "COMMENT_NOT_FOUND"); - } - const id = idFactory(); - const now = Date.now(); - await pool2.query( - `INSERT INTO plaza_reports - (id, target_type, target_id, reporter_id, reason, detail, status, created_at) - VALUES (?, ?, ?, ?, ?, ?, 'pending', ?)`, - [id, type, targetId, reporterId, reasonValue, String(detail ?? "").trim().slice(0, 500), now] - ); - return { id, status: "pending" }; - }; - const listReports = async ({ status = "pending", limit = 50 } = {}) => { - const pageLimit = clampLimit3(limit, 50, 100); - const [rows] = await pool2.query( - `SELECT r.*, - (SELECT COUNT(*) FROM plaza_reports r2 - WHERE r2.target_type = r.target_type AND r2.target_id = r.target_id AND r2.status = 'pending') AS target_report_count - FROM plaza_reports r - WHERE r.status = ? - ORDER BY r.created_at DESC - LIMIT ?`, - [status, pageLimit] - ); - return { - reports: rows.map((row) => ({ - id: row.id, - target_type: row.target_type, - target_id: row.target_id, - reason: row.reason, - detail: row.detail, - status: row.status, - target_report_count: Number(row.target_report_count ?? 1), - created_at: new Date(Number(row.created_at)).toISOString() - })) - }; - }; - const processReport = async (operatorId, reportId, { action, action_taken: actionTaken = "" } = {}) => { - const [rows] = await pool2.query(`SELECT * FROM plaza_reports WHERE id = ? LIMIT 1`, [reportId]); - const report = rows[0]; - if (!report) throw opsError("\u4E3E\u62A5\u4E0D\u5B58\u5728", "report_not_found"); - const now = Date.now(); - const status = action === "dismiss" ? "dismissed" : "processed"; - await pool2.query( - `UPDATE plaza_reports - SET status = ?, processed_by = ?, processed_at = ?, action_taken = ? - WHERE id = ?`, - [status, operatorId, now, String(actionTaken).slice(0, 200), reportId] - ); - if (action === "hide_post" && report.target_type === "post") { - await pool2.query( - `UPDATE plaza_posts SET status = 'hidden', updated_at = ? WHERE id = ?`, - [now, report.target_id] - ); - await invalidateFeedCaches?.(); - } - await writeAuditLog(operatorId, "process_report", "report", reportId, { - action, - target_type: report.target_type, - target_id: report.target_id - }); - return { id: reportId, status }; - }; - const setFeatured = async (operatorId, { post_id: postId, position, sort_order: sortOrder = 0, expires_at: expiresAt = null }) => { - const pos = String(position ?? "").trim(); - if (!FEATURED_POSITIONS.has(pos)) throw opsError("\u4E0D\u652F\u6301\u7684\u7CBE\u9009\u4F4D", "invalid_input"); - const [postRows] = await pool2.query( - `SELECT id FROM plaza_posts WHERE id = ? AND status = 'published' LIMIT 1`, - [postId] - ); - if (!postRows[0]) throw opsError("\u5E16\u5B50\u4E0D\u5B58\u5728\u6216\u672A\u53D1\u5E03", "POST_NOT_FOUND"); - const now = Date.now(); - const id = idFactory(); - const expiresMs = expiresAt ? new Date(expiresAt).getTime() : null; - await pool2.query( - `INSERT INTO plaza_featured (id, post_id, position, sort_order, starts_at, expires_at, created_by, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [id, postId, pos, Number(sortOrder) || 0, now, expiresMs, operatorId, now] - ); - await invalidateFeedCaches?.(); - await writeAuditLog(operatorId, "set_featured", "featured", id, { post_id: postId, position: pos }); - return { id, post_id: postId, position: pos }; - }; - const listFeatured = async () => { - const now = Date.now(); - const [rows] = await pool2.query( - `SELECT f.*, pp.title, pp.cover_url, pp.user_display_name - FROM plaza_featured f - JOIN plaza_posts pp ON pp.id = f.post_id - WHERE f.starts_at <= ? AND (f.expires_at IS NULL OR f.expires_at > ?) - ORDER BY f.position ASC, f.sort_order ASC, f.created_at DESC`, - [now, now] - ); - return { - items: rows.map((row) => ({ - id: row.id, - post_id: row.post_id, - position: row.position, - sort_order: row.sort_order, - title: row.title, - cover_url: row.cover_url, - author: row.user_display_name, - expires_at: row.expires_at ? new Date(Number(row.expires_at)).toISOString() : null - })) - }; - }; - const removeFeatured = async (operatorId, featuredId) => { - const [result] = await pool2.query(`DELETE FROM plaza_featured WHERE id = ?`, [featuredId]); - if ((result.affectedRows ?? 0) === 0) throw opsError("\u7CBE\u9009\u4E0D\u5B58\u5728", "featured_not_found"); - await invalidateFeedCaches?.(); - await writeAuditLog(operatorId, "remove_featured", "featured", featuredId, {}); - return { id: featuredId }; - }; - const loadActiveFeaturedPosts = async (viewerId = null) => { - const now = Date.now(); - const [rows] = await pool2.query( - `SELECT f.position, f.sort_order, pp.*, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon - FROM plaza_featured f - JOIN plaza_posts pp ON pp.id = f.post_id AND pp.status = 'published' - JOIN plaza_categories c ON c.id = pp.category_id - WHERE f.starts_at <= ? AND (f.expires_at IS NULL OR f.expires_at > ?) - ORDER BY f.position ASC, f.sort_order ASC`, - [now, now] - ); - const grouped = { homepage_banner: [], trending: [], category_top: {} }; - for (const row of rows) { - const post = formatPostRow2 ? formatPostRow2(row, { viewerReacted: null }) : row; - if (row.position === "homepage_banner") grouped.homepage_banner.push(post); - if (row.position === "trending") grouped.trending.push(post); - if (row.position === "category_top") { - const slug = row.category_slug; - if (!grouped.category_top[slug]) grouped.category_top[slug] = []; - grouped.category_top[slug].push(post); - } - } - return grouped; - }; - const getAnalyticsOverview = async () => { - const now = Date.now(); - const dayMs = 864e5; - const todayStart = now - now % dayMs; - const yesterdayStart = todayStart - dayMs; - const [[todayPosts]] = await pool2.query( - `SELECT COUNT(*) AS count FROM plaza_posts WHERE status = 'published' AND published_at >= ?`, - [todayStart] - ); - const [[yesterdayPosts]] = await pool2.query( - `SELECT COUNT(*) AS count FROM plaza_posts WHERE status = 'published' AND published_at >= ? AND published_at < ?`, - [yesterdayStart, todayStart] - ); - const [[pendingReview]] = await pool2.query( - `SELECT COUNT(*) AS count FROM plaza_posts WHERE status = 'pending_review'` - ); - const [[signupFromPlaza]] = await pool2.query( - `SELECT COUNT(*) AS count FROM plaza_attribution_events - WHERE event_type = 'signup' AND utm_source = 'plaza' AND created_at >= ?`, - [todayStart] - ); - const [categoryRows] = await pool2.query( - `SELECT c.name, COUNT(p.id) AS count - FROM plaza_categories c - LEFT JOIN plaza_posts p ON p.category_id = c.id AND p.status = 'published' - GROUP BY c.id, c.name - ORDER BY count DESC` - ); - const [topCreators] = await pool2.query( - `SELECT user_slug AS slug, user_display_name AS display_name, COUNT(*) AS post_count, - SUM(like_count) AS total_likes - FROM plaza_posts - WHERE status = 'published' - GROUP BY user_id, user_slug, user_display_name - ORDER BY total_likes DESC - LIMIT 10` - ); - const [dailyPosts] = await pool2.query( - `SELECT DATE(FROM_UNIXTIME(published_at / 1000)) AS day, COUNT(*) AS count - FROM plaza_posts - WHERE status = 'published' AND published_at >= ? - GROUP BY day - ORDER BY day ASC`, - [now - 14 * dayMs] - ); - return { - today: { - new_posts: Number(todayPosts?.count ?? 0), - plaza_signups: Number(signupFromPlaza?.count ?? 0), - pending_review: Number(pendingReview?.count ?? 0) - }, - yesterday: { - new_posts: Number(yesterdayPosts?.count ?? 0) - }, - categories: categoryRows.map((row) => ({ - name: row.name, - count: Number(row.count ?? 0) - })), - top_creators: topCreators.map((row) => ({ - slug: row.slug, - display_name: row.display_name, - post_count: Number(row.post_count ?? 0), - total_likes: Number(row.total_likes ?? 0) - })), - daily_posts: dailyPosts.map((row) => ({ - day: row.day, - count: Number(row.count ?? 0) - })) - }; - }; - const listCreators = async ({ keyword = null, limit = 50 } = {}) => { - const pageLimit = clampLimit3(limit, 50, 100); - const params = []; - let keywordClause = ""; - if (keyword) { - keywordClause = "AND (u.username LIKE ? OR u.display_name LIKE ? OR u.slug LIKE ?)"; - const pattern = `%${String(keyword).trim()}%`; - params.push(pattern, pattern, pattern); - } - params.push(pageLimit); - const [rows] = await pool2.query( - `SELECT u.id, u.slug, u.username, u.display_name, u.plaza_post_count, u.plaza_follower_count, - u.plaza_verified, u.plaza_post_banned, u.plaza_comment_banned, u.ops_role - FROM h5_users u - WHERE u.plaza_post_count > 0 ${keywordClause} - ORDER BY u.plaza_post_count DESC - LIMIT ?`, - params - ); - return { - creators: rows.map((row) => ({ - user_id: row.id, - slug: row.slug || row.username, - display_name: row.display_name || row.username, - post_count: Number(row.plaza_post_count ?? 0), - follower_count: Number(row.plaza_follower_count ?? 0), - verified: Boolean(row.plaza_verified), - post_banned: Boolean(row.plaza_post_banned), - comment_banned: Boolean(row.plaza_comment_banned) - })) - }; - }; - const updateCreator = async (operatorId, userId, patch) => { - const updates = []; - const params = []; - if (patch?.verified != null) { - updates.push("plaza_verified = ?"); - params.push(patch.verified ? 1 : 0); - } - if (patch?.post_banned != null) { - updates.push("plaza_post_banned = ?"); - params.push(patch.post_banned ? 1 : 0); - } - if (patch?.comment_banned != null) { - updates.push("plaza_comment_banned = ?"); - params.push(patch.comment_banned ? 1 : 0); - } - if (updates.length === 0) throw opsError("\u6CA1\u6709\u53EF\u66F4\u65B0\u7684\u5B57\u6BB5", "invalid_input"); - updates.push("updated_at = ?"); - params.push(Date.now(), userId); - const [result] = await pool2.query( - `UPDATE h5_users SET ${updates.join(", ")} WHERE id = ?`, - params - ); - if ((result.affectedRows ?? 0) === 0) throw opsError("\u7528\u6237\u4E0D\u5B58\u5728", "user_not_found"); - await writeAuditLog(operatorId, "update_creator", "user", userId, patch); - return { user_id: userId }; - }; - return { - loadOperatorRole, - hasOpsRole, - listReviewQueue, - reviewPostAsOps, - batchReviewPosts, - createReport, - listReports, - processReport, - setFeatured, - listFeatured, - removeFeatured, - loadActiveFeaturedPosts, - getAnalyticsOverview, - listCreators, - updateCreator - }; -} - -// word-filter.mjs -import crypto21 from "node:crypto"; -function createWordFilterService(pool2) { - async function listBlockedWords() { - const [rows] = await pool2.query( - "SELECT id, word, replacement, note, status, created_at, updated_at FROM h5_blocked_words ORDER BY created_at DESC" - ); - return rows; - } - async function createBlockedWord({ word, replacement, note }) { - const trimmed = String(word ?? "").trim(); - if (!trimmed) return { ok: false, message: "\u8BCD\u8BED\u4E0D\u80FD\u4E3A\u7A7A" }; - const id = crypto21.randomUUID(); - const now = Date.now(); - try { - await pool2.query( - "INSERT INTO h5_blocked_words (id, word, replacement, note, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", - [id, trimmed, replacement ?? "***", note ?? "", "active", now, now] - ); - const [rows] = await pool2.query("SELECT * FROM h5_blocked_words WHERE id = ?", [id]); - return { ok: true, blockedWord: rows[0] }; - } catch (err) { - if (err?.code === "ER_DUP_ENTRY") return { ok: false, message: "\u8BE5\u8BCD\u8BED\u5DF2\u5B58\u5728" }; - throw err; - } - } - async function updateBlockedWord(id, { word, replacement, note, status }) { - const updates = []; - const values = []; - if (word !== void 0) { - const trimmed = String(word).trim(); - if (!trimmed) return { ok: false, message: "\u8BCD\u8BED\u4E0D\u80FD\u4E3A\u7A7A" }; - updates.push("word = ?"); - values.push(trimmed); - } - if (replacement !== void 0) { - updates.push("replacement = ?"); - values.push(replacement); - } - if (note !== void 0) { - updates.push("note = ?"); - values.push(note); - } - if (status !== void 0) { - updates.push("status = ?"); - values.push(status); - } - if (!updates.length) return { ok: false, message: "\u6CA1\u6709\u53EF\u66F4\u65B0\u7684\u5B57\u6BB5" }; - updates.push("updated_at = ?"); - values.push(Date.now(), id); - const [result] = await pool2.query( - `UPDATE h5_blocked_words SET ${updates.join(", ")} WHERE id = ?`, - values - ); - if (!result.affectedRows) return { ok: false, message: "\u8BB0\u5F55\u4E0D\u5B58\u5728" }; - const [rows] = await pool2.query("SELECT * FROM h5_blocked_words WHERE id = ?", [id]); - return { ok: true, blockedWord: rows[0] }; - } - async function deleteBlockedWord(id) { - const [result] = await pool2.query("DELETE FROM h5_blocked_words WHERE id = ?", [id]); - if (!result.affectedRows) return { ok: false, message: "\u8BB0\u5F55\u4E0D\u5B58\u5728" }; - return { ok: true }; - } - async function listAllForFrontend() { - const [rows] = await pool2.query( - "SELECT word, replacement FROM h5_blocked_words WHERE status = ? ORDER BY word", - ["active"] - ); - return rows; - } - return { listBlockedWords, createBlockedWord, updateBlockedWord, deleteBlockedWord, listAllForFrontend }; -} - -// plaza-embed.mjs -function isPlazaEmbedRequest(query = {}) { - return String(query?.embed ?? "").toLowerCase() === "plaza"; -} -function publishedPageCspForEmbed(isFullHtml) { - if (isFullHtml) { - return "default-src 'none'; style-src 'unsafe-inline' https:; img-src data: https:; font-src https: data:; base-uri 'none'; form-action 'self'; frame-ancestors *; script-src 'unsafe-inline'"; - } - return "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors *; script-src 'unsafe-inline'"; -} -function allowPlazaEmbedFrame(res) { - res.removeHeader?.("X-Frame-Options"); -} -var EMBED_BOOTSTRAP = ``; -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(//gi, " ").replace(//gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); - return stripped.slice(0, 180); -} -function dedupeDuplicatedFilename(filename) { - const match = String(filename ?? "").match(/^(.+)\.html$/i); - if (!match) return filename; - let base = match[1]; - const half = Math.floor(base.length / 2); - if (half >= 2 && base.slice(0, half) === base.slice(half)) { - base = base.slice(0, half); - } - base = base.replace(/(.+?)\1+/g, "$1"); - base = base.replace(/-v-v(\d+)/g, "-v$1"); - return `${base}.html`; -} -function sanitizeMessageContentForSave(content) { - let text = String(content ?? ""); - text = text.replace(/httpshttps:\/+/gi, "https://"); - text = text.replace(/https:(?:\/\/)+/gi, "https://"); - text = text.replace(/(https?:\/\/[^\s<>"')\]]+?)(?:\1)+/gi, "$1"); - text = text.replace(/([\u4e00-\u9fffA-Za-z0-9._-]+)\s+\1/g, "$1"); - text = text.replace(/(\.html)\.html\b/gi, ".html"); - text = text.replace(/((?:MindSpace|temp)\/[^/\s<>"')\]]+\/)(public\/)\2/gi, "$1$2"); - return text; -} -function extractHtmlFilenameHints(content) { - const hints = /* @__PURE__ */ new Set(); - for (const match of String(content ?? "").matchAll(/([a-z0-9][a-z0-9._-]*\.html)/gi)) { - hints.add(dedupeDuplicatedFilename(match[1].toLowerCase())); - } - return [...hints]; -} -function analyzeChatMessageForSave({ - content, - userId, - username, - h5Root, - selectedLinkIndex = 0 -}) { - const sanitized = sanitizeMessageContentForSave(content); - const links = extractStaticPageLinks(sanitized, { userId, username }); - const text = sanitized.replace(/\s+/g, " ").trim(); - const suggestedTitleFromText = text.replace(/^#{1,6}\s*/, "").replace(/[*_`~[\]]/g, "").trim().slice(0, 48); - if (links.length === 0) { - return { - contentMode: "markdown", - links: [], - selectedLink: null, - suggestedTitle: suggestedTitleFromText || "AI \u521B\u4F5C\u9875\u9762", - suggestedSummary: text.slice(0, 160), - previewUrl: null, - relativePath: null, - filename: null - }; - } - const index = Math.min(Math.max(0, selectedLinkIndex), links.length - 1); - const selectedLink = links[index]; - return { - contentMode: "static_html", - links, - selectedLink, - suggestedTitle: selectedLink.filename.replace(/\.html$/i, "").replace(/[-_]/g, " ") || suggestedTitleFromText || "AI \u751F\u6210\u9875\u9762", - suggestedSummary: text.slice(0, 160), - previewUrl: selectedLink.publicUrl, - relativePath: selectedLink.relativePath, - filename: selectedLink.filename, - h5Root, - userId, - username - }; -} -async function resolveStaticHtmlContent(analysis) { - if (analysis.contentMode !== "static_html" || !analysis.selectedLink) { - return null; - } - const loaded = await findPublishHtml( - analysis.h5Root, - analysis.userId, - analysis.selectedLink.relativePath - ); - const title = titleFromHtml2(loaded.content); - const summary = summaryFromHtml(loaded.content); - return { - ...loaded, - suggestedTitle: title || analysis.suggestedTitle, - suggestedSummary: summary || analysis.suggestedSummary, - publicUrl: analysis.selectedLink.publicUrl - }; -} -async function resolveChatSaveAnalysis({ - content, - userId, - username, - h5Root, - selectedLinkIndex = 0 -}) { - const sanitized = sanitizeMessageContentForSave(content); - let analysis = analyzeChatMessageForSave({ - content: sanitized, - userId, - username, - h5Root, - selectedLinkIndex - }); - if (analysis.contentMode === "static_html" && analysis.selectedLink) { - try { - const resolvedHtml = await resolveStaticHtmlContent(analysis); - return { analysis, resolvedHtml }; - } catch { - } - } - for (const hint of extractHtmlFilenameHints(sanitized)) { - try { - const loaded = await findPublishHtml(h5Root, userId, hint); - const syntheticLink = { - publicUrl: buildWorkspaceAssetUrl(userId, loaded.relativePath) ?? `/${PUBLISH_ROOT_DIR}/${userId}/${loaded.relativePath}`, - owner: String(userId ?? "").trim().toLowerCase(), - relativePath: loaded.relativePath, - filename: loaded.filename - }; - analysis = { - contentMode: "static_html", - links: [syntheticLink], - selectedLink: syntheticLink, - suggestedTitle: titleFromHtml2(loaded.content) || syntheticLink.filename.replace(/\.html$/i, "").replace(/[-_]/g, " ") || "AI \u751F\u6210\u9875\u9762", - suggestedSummary: summaryFromHtml(loaded.content) || sanitized.slice(0, 160), - previewUrl: syntheticLink.publicUrl, - relativePath: loaded.relativePath, - filename: loaded.filename, - h5Root, - userId, - username - }; - return { - analysis, - resolvedHtml: { - ...loaded, - suggestedTitle: analysis.suggestedTitle, - suggestedSummary: analysis.suggestedSummary, - publicUrl: syntheticLink.publicUrl - } - }; - } catch { - } - } - return { analysis, resolvedHtml: null }; -} - -// mindspace-public-finish-sync.mjs -import fs25 from "node:fs"; -import path29 from "node:path"; -var PUBLIC_HTML_PATH_PATTERN = /(?:^|[^a-z0-9_./-])(public\/[a-z0-9][a-z0-9._/-]{0,255}\.html)\b/i; -var PUBLIC_HTML_PATH_PATTERN_GLOBAL = /(?:^|[^a-z0-9_./-])(public\/[a-z0-9][a-z0-9._/-]{0,255}\.html)\b/gi; -var PUBLIC_DOWNLOAD_ATTR_PATTERN = /(?:href|src)=["']([^"'#?\s]+)["']/gi; -var DEFAULT_DOWNLOAD_SYNC_MIN_SIZE = 1024; -function normalizePublicDownloadHref(href) { - const clean = String(href ?? "").split("?")[0].split("#")[0].replace(/^\.\//, "").trim(); - if (!clean || clean.includes("://") || clean.startsWith("data:") || !DOWNLOADABLE_FILE_PATTERN.test(clean)) { - return null; - } - if (clean.startsWith("../oa/")) return clean.slice(3); - if (clean.startsWith("oa/")) return clean; - if (!clean.includes("/")) return `public/${clean}`; - return clean; -} -function extractPublicDownloadReferencesFromHtml(publishDir) { - const root = path29.resolve(String(publishDir ?? "")); - const publicDir = path29.join(root, "public"); - if (!fs25.existsSync(publicDir) || !fs25.statSync(publicDir).isDirectory()) return []; - const refs = /* @__PURE__ */ new Map(); - for (const file of fs25.readdirSync(publicDir)) { - if (!file.toLowerCase().endsWith(".html")) continue; - const content = fs25.readFileSync(path29.join(publicDir, file), "utf8"); - const htmlRelativePath = `public/${file}`; - for (const match of content.matchAll(PUBLIC_DOWNLOAD_ATTR_PATTERN)) { - const relativePath = normalizePublicDownloadHref(match[1]); - if (relativePath && !refs.has(relativePath)) { - refs.set(relativePath, { relativePath, htmlRelativePath }); - } - } - } - return [...refs.values()]; -} -function listOaDownloadCandidates(publishDir, extension, { minSize = DEFAULT_DOWNLOAD_SYNC_MIN_SIZE } = {}) { - const oaDir = path29.join(path29.resolve(String(publishDir ?? "")), "oa"); - if (!fs25.existsSync(oaDir) || !fs25.statSync(oaDir).isDirectory()) return []; - return fs25.readdirSync(oaDir).filter((name) => name.toLowerCase().endsWith(extension)).map((name) => { - const absolutePath = path29.join(oaDir, name); - const stat = fs25.statSync(absolutePath); - return { name, absolutePath, size: stat.size, mtimeMs: stat.mtimeMs }; - }).filter((item) => item.size >= minSize).sort((a, b) => b.mtimeMs - a.mtimeMs || b.size - a.size); -} -function resolvePublicDownloadSyncSource(root, relativePath, htmlRelativePath, { minSize = DEFAULT_DOWNLOAD_SYNC_MIN_SIZE } = {}) { - const extension = path29.extname(relativePath).toLowerCase(); - const basename = path29.basename(relativePath); - const htmlStemFilename = path29.posix.basename(htmlRelativePath).replace(/\.html$/i, extension); - const exactCandidates = [ - path29.posix.join("oa", basename), - path29.posix.join("oa", htmlStemFilename) - ]; - for (const candidate of exactCandidates) { - const absolutePath = path29.resolve(root, candidate); - if (absolutePath === path29.resolve(root, relativePath)) continue; - try { - const stat = fs25.statSync(absolutePath); - if (stat.isFile() && stat.size >= minSize) { - return { - name: path29.basename(candidate), - absolutePath, - size: stat.size, - strategy: "exact_oa_match" - }; - } - } catch { - } - } - const fallbackCandidates = listOaDownloadCandidates(root, extension, { minSize }); - if (fallbackCandidates.length === 1) { - return { ...fallbackCandidates[0], strategy: "single_oa_fallback" }; - } - return null; -} -function syncPublicDocxDownloads({ - publishDir, - minCompleteSize = DEFAULT_DOWNLOAD_SYNC_MIN_SIZE -} = {}) { - const root = path29.resolve(String(publishDir ?? "")); - if (!root) return { synced: [], skipped: [], missing: [], source: null, sourceByRelativePath: {} }; - const synced = []; - const skipped = []; - const missing = []; - const sourceByRelativePath = {}; - for (const reference of extractPublicDownloadReferencesFromHtml(root)) { - const { relativePath, htmlRelativePath } = reference; - const destination = path29.resolve(root, relativePath); - if (destination !== root && !destination.startsWith(`${root}${path29.sep}`)) { - skipped.push(relativePath); - continue; - } - const source = resolvePublicDownloadSyncSource(root, relativePath, htmlRelativePath, { - minSize: minCompleteSize - }); - const destinationExists = fs25.existsSync(destination) && fs25.statSync(destination).isFile(); - if (destinationExists && !source) { - skipped.push(relativePath); - continue; - } - if (!source) { - missing.push(relativePath); - continue; - } - if (destinationExists) { - try { - if (fs25.statSync(destination).size >= source.size) { - skipped.push(relativePath); - continue; - } - } catch { - } - } - try { - fs25.mkdirSync(path29.dirname(destination), { recursive: true }); - fs25.copyFileSync(source.absolutePath, destination); - synced.push(relativePath); - sourceByRelativePath[relativePath] = source.name; - } catch { - skipped.push(relativePath); - } - } - const sourceNames = [...new Set(Object.values(sourceByRelativePath))]; - return { - synced, - skipped, - missing, - source: sourceNames.length === 1 ? sourceNames[0] : null, - sourceByRelativePath - }; -} -function messageText(message) { - const textParts = Array.isArray(message?.content) ? message.content.filter((item) => item?.type === "text" && typeof item.text === "string").map((item) => item.text.trim()).filter(Boolean) : []; - const displayText = typeof message?.metadata?.displayText === "string" ? message.metadata.displayText.trim() : ""; - return [...textParts, displayText].filter(Boolean).join("\n"); -} -function messageId(message) { - const id = message?.id ?? message?.message_id ?? message?.metadata?.messageId ?? message?.metadata?.message_id ?? null; - const normalized = String(id ?? "").trim(); - return normalized || null; -} -function normalizePublicHtmlRelativePath(relativePath) { - const parts = String(relativePath ?? "").replace(/^\/+/, "").split("/").filter((part) => part && part !== "." && part !== ".."); - if (parts.length === 0) return ""; - 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 publicHtmlFileExists(publishDir, relativePath) { - if (!publishDir) return false; - const root = path29.resolve(String(publishDir ?? "")); - const normalized = normalizePublicHtmlRelativePath(relativePath); - if (!root || !normalized.toLowerCase().endsWith(".html")) return false; - const target = path29.resolve(root, normalized); - if (target !== root && !target.startsWith(`${root}${path29.sep}`)) return false; - return fs25.existsSync(target) && fs25.statSync(target).isFile(); -} -function collectOwnPublicHtmlArtifactRefs({ - messages, - currentUser, - publishDir, - materialized = [], - skipped = [] -} = {}) { - const refs = /* @__PURE__ */ new Map(); - const add = (relativePath, sourceMessageId = null) => { - const normalized = normalizePublicHtmlRelativePath(relativePath); - if (!normalized || !publicHtmlFileExists(publishDir, normalized)) return; - const existing = refs.get(normalized); - refs.set(normalized, { - relativePath: normalized, - messageId: existing?.messageId ?? sourceMessageId ?? null - }); - }; - for (const relativePath of [...materialized, ...skipped]) add(relativePath); - for (const artifact of extractPublicHtmlWriteArtifacts(messages, { publishDir })) { - add(artifact.relativePath, artifact.messageId ?? null); - } - for (const message of Array.isArray(messages) ? messages : []) { - const sourceMessageId = messageId(message); - const text = messageText(message); - if (!text) continue; - for (const link of extractStaticPageLinks(text, { - userId: currentUser?.id, - username: currentUser?.username ?? null - })) { - add(link.relativePath, sourceMessageId); - } - for (const match of text.matchAll(PUBLIC_HTML_PATH_PATTERN_GLOBAL)) { - add(match[1], sourceMessageId); - } - } - return [...refs.values()].sort((a, b) => a.relativePath.localeCompare(b.relativePath)); -} -function isWriteLikeHtmlTool(name, args) { - const action = String(args?.action ?? "").toLowerCase(); - const normalizedName = String(name ?? "").trim(); - const writeLikeDeveloper = normalizedName === "developer" && (action === "write" || action === "edit") || normalizedName === "write" || normalizedName.endsWith("__write"); - const writeLikeSandbox = (normalizedName === "write_file" || normalizedName === "edit_file" || normalizedName.endsWith("__write_file") || normalizedName.endsWith("__edit_file")) && typeof args?.path === "string"; - return writeLikeDeveloper && typeof args?.path === "string" || writeLikeSandbox; -} -function isEditLikeHtmlTool(name, args) { - const normalizedName = String(name ?? "").trim(); - return (normalizedName === "edit_file" || normalizedName.endsWith("__edit_file") || normalizedName === "developer" && String(args?.action ?? "").toLowerCase() === "edit") && typeof args?.path === "string"; -} -function readPublicHtmlBaseline(publishDir, relativePath) { - const root = path29.resolve(String(publishDir ?? "")); - if (!root) return ""; - const destination = path29.resolve(root, relativePath); - if (destination !== root && !destination.startsWith(`${root}${path29.sep}`)) return ""; - if (!fs25.existsSync(destination) || !fs25.statSync(destination).isFile()) return ""; - try { - return fs25.readFileSync(destination, "utf8"); - } catch { - return ""; - } -} -function applyPublicHtmlEdit(existing, args) { - if (typeof args?.new_str !== "string") return null; - const oldStr = typeof args.old_str === "string" ? args.old_str : ""; - if (oldStr && !existing.includes(oldStr)) return null; - return oldStr ? existing.replace(oldStr, args.new_str) : args.new_str; -} -function extractPublicHtmlWriteArtifacts(messages = [], { publishDir = null } = {}) { - const artifacts = /* @__PURE__ */ new Map(); - for (const message of messages) { - const sourceMessageId = messageId(message); - for (const item of message?.content ?? []) { - if (item?.type !== "toolRequest") continue; - const toolCall = item.toolCall?.value; - const args = toolCall?.arguments ?? {}; - if (!isWriteLikeHtmlTool(toolCall?.name, args)) continue; - const candidate = String(args.path ?? "").trim(); - if (!candidate.toLowerCase().endsWith(".html")) continue; - const relativePath = normalizePublicHtmlRelativePath(candidate); - if (!relativePath) continue; - let content = null; - if (typeof args.content === "string") { - content = args.content; - } else if (isEditLikeHtmlTool(toolCall?.name, args)) { - const baseline = artifacts.get(relativePath)?.content ?? readPublicHtmlBaseline(publishDir, relativePath); - content = applyPublicHtmlEdit(baseline, args); - } else if (typeof args.new_str === "string" && !args.old_str) { - content = args.new_str; - } - if (content == null) continue; - artifacts.set(relativePath, { relativePath, content, messageId: sourceMessageId }); - } - } - return [...artifacts.values()]; -} -var PUBLIC_HTML_STUB_MARKERS = ["\u4E34\u65F6\u8865\u51FA", "\u670D\u52A1\u53F7\u515C\u5E95", "\u670D\u52A1\u53F7\u81EA\u52A8\u8865\u51FA\u7B80\u7248\u9875\u9762"]; -function isStubPublicHtmlContent(content) { - const value = String(content ?? ""); - return PUBLIC_HTML_STUB_MARKERS.some((marker) => value.includes(marker)); -} -function shouldReplaceExistingPublicHtml(destination, nextContent) { - if (!fs25.existsSync(destination) || !fs25.statSync(destination).isFile()) return true; - try { - const existing = fs25.readFileSync(destination, "utf8"); - if (isStubPublicHtmlContent(existing)) return true; - return typeof nextContent === "string" && nextContent.length > 0 && existing !== nextContent; - } catch { - return Boolean(nextContent); - } -} -function materializeMissingPublicHtmlWrites({ messages, publishDir }) { - const root = path29.resolve(String(publishDir ?? "")); - if (!root) return { materialized: [], skipped: [] }; - const materialized = []; - const skipped = []; - for (const artifact of extractPublicHtmlWriteArtifacts(messages, { publishDir: root })) { - const destination = path29.resolve(root, artifact.relativePath); - if (destination !== root && !destination.startsWith(`${root}${path29.sep}`)) { - skipped.push(artifact.relativePath); - continue; - } - if (fs25.existsSync(destination) && fs25.statSync(destination).isFile()) { - if (!shouldReplaceExistingPublicHtml(destination, artifact.content)) { - skipped.push(artifact.relativePath); - continue; - } - } - try { - fs25.mkdirSync(path29.dirname(destination), { recursive: true }); - fs25.writeFileSync(destination, artifact.content, "utf8"); - materialized.push(artifact.relativePath); - } catch { - skipped.push(artifact.relativePath); - } - } - return { materialized, skipped }; -} -function materializePublicHtmlWritesFromSessionEvent(event, { publishDir, recentCount = 20 } = {}) { - if (!event || !publishDir) { - return { materialized: [], skipped: [] }; - } - if (event.type === "Message" && event.message) { - return materializeMissingPublicHtmlWrites({ - messages: [event.message], - publishDir - }); - } - if (event.type === "UpdateConversation" && Array.isArray(event.conversation)) { - return materializeMissingPublicHtmlWrites({ - messages: event.conversation.slice(-Math.max(1, recentCount)), - publishDir - }); - } - return { materialized: [], skipped: [] }; -} -function messageHasPublicHtmlToolRequest(message, { publishDir = null } = {}) { - if (extractPublicHtmlWriteArtifacts([message], { publishDir }).length > 0) { - return true; - } - for (const item of message?.content ?? []) { - if (item?.type !== "toolRequest") continue; - const toolCall = item.toolCall?.value; - const args = toolCall?.arguments ?? {}; - if (!isWriteLikeHtmlTool(toolCall?.name, args)) continue; - const candidate = String(args.path ?? "").trim(); - if (!candidate.toLowerCase().endsWith(".html")) continue; - if (normalizePublicHtmlRelativePath(candidate)) return true; - } - return false; -} -function hasRecentOwnPublicHtmlReference(messages, currentUser, { recentCount = 80, publishDir = null } = {}) { - if (!Array.isArray(messages) || messages.length === 0 || !currentUser?.id) return false; - const recentMessages = messages.slice(-Math.max(1, recentCount)); - for (const message of recentMessages) { - if (messageHasPublicHtmlToolRequest(message, { publishDir })) { - return true; - } - const text = messageText(message); - if (!text) continue; - if (extractStaticPageLinks(text, { - userId: currentUser.id, - username: currentUser.username ?? null - }).length > 0) { - return true; - } - if (PUBLIC_HTML_PATH_PATTERN.test(text)) { - return true; - } - } - return false; -} -async function syncPublicHtmlAfterFinish({ - messages, - currentUser, - publishDir, - syncWorkspaceAssets, - registerPublicHtmlArtifacts, - sessionId = null -} = {}) { - if (!hasRecentOwnPublicHtmlReference(messages, currentUser, { publishDir })) { - return { materialized: [], skipped: [], synced: false }; - } - const { materialized, skipped } = materializeMissingPublicHtmlWrites({ messages, publishDir }); - const docxSync = syncPublicDocxDownloads({ publishDir }); - const publicHtmlArtifactRefs = collectOwnPublicHtmlArtifactRefs({ - messages, - currentUser, - publishDir, - materialized, - skipped - }); - const publicHtmlRelativePaths = publicHtmlArtifactRefs.map((ref) => ref.relativePath); - const sourceMessageId = publicHtmlArtifactRefs.find((ref) => ref.messageId)?.messageId ?? null; - let synced = false; - if (typeof syncWorkspaceAssets === "function" && currentUser?.id) { - await syncWorkspaceAssets(currentUser.id, { - categoryCode: "public", - sourceSessionId: sessionId, - sourceMessageId - }); - synced = true; - } - if (typeof registerPublicHtmlArtifacts === "function" && currentUser?.id && sessionId) { - await registerPublicHtmlArtifacts(currentUser.id, { - sessionId, - relativePaths: publicHtmlRelativePaths, - artifactRefs: publicHtmlArtifactRefs - }); - } - return { materialized, skipped, synced, docxSync, publicHtmlRelativePaths, publicHtmlArtifactRefs }; -} - -// mindspace-page-sync.mjs -import fs26 from "node:fs/promises"; -import path30 from "node:path"; -var PUBLIC_HTML_SKIP_DIRS = /* @__PURE__ */ new Set([ - ".tmp-images", - "assets", - "images", - "shared", - "node_modules" -]); -function normalizePublicHtmlPath(relativePath) { - return normalizeWorkspaceRelativePath2(relativePath); -} -function titleFromRelativePath(relativePath) { - const basename = path30.basename(String(relativePath ?? ""), ".html"); - return basename.replace(/[-_]+/g, " ").trim() || "\u751F\u6210\u7684\u9875\u9762"; -} -function extractHtmlTitle(content) { - const match = String(content).match(/]*>([^<]+)<\/title>/i); - return match?.[1]?.trim() ?? ""; -} -function extractHtmlSummary(content) { - const match = String(content).match(/]+name=["']description["'][^>]+content=["']([^"']+)["']/i); - return match?.[1]?.trim().slice(0, 1e3) ?? ""; -} -async function findPageByWorkspaceRelativePath(pool2, userId, relativePath) { - const [rows] = await pool2.query( - `SELECT p.id, p.updated_at - FROM h5_page_records p - JOIN h5_page_versions pv ON pv.id = p.current_version_id - WHERE p.user_id = ? - AND p.status <> 'deleted' - AND JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) = ? - ORDER BY p.updated_at DESC, p.id DESC - LIMIT 1`, - [userId, relativePath] - ); - const row = rows[0]; - if (!row) return null; - return { id: row.id, updatedAt: Number(row.updated_at ?? 0) }; -} -function parseJsonColumn2(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 }; - } -} -async function loadIndexedWorkspacePages(pool2, userId) { - const [rows] = await pool2.query( - `SELECT p.id, p.updated_at, p.source_asset_id, pv.source_snapshot_json - FROM h5_page_records p - JOIN h5_page_versions pv ON pv.id = p.current_version_id - WHERE p.user_id = ? AND p.status <> 'deleted'`, - [userId] - ); - const byPath = /* @__PURE__ */ new Map(); - for (const row of rows) { - const snapshot = parseJsonColumn2(row.source_snapshot_json); - const relativePath = normalizePublicHtmlPath(snapshot.relative_path); - if (relativePath) { - byPath.set(relativePath, { - id: row.id, - updatedAt: Number(row.updated_at ?? 0) - }); - } - if (row.source_asset_id && relativePath) { - byPath.set(`asset:${row.source_asset_id}`, { id: row.id, updatedAt: Number(row.updated_at ?? 0) }); - } - } - return byPath; -} -async function listPublishPublicHtmlFiles(publishDir) { - const publicDir = path30.join(publishDir, "public"); - const files = []; - async function walk(absDir, relWithinPublic) { - let entries; - try { - entries = await fs26.readdir(absDir, { withFileTypes: true }); - } catch { - return; - } - for (const entry of entries) { - if (entry.name.startsWith(".") || PUBLIC_HTML_SKIP_DIRS.has(entry.name)) continue; - const absPath = path30.join(absDir, entry.name); - const relPath = relWithinPublic ? `${relWithinPublic}/${entry.name}` : entry.name; - if (entry.isDirectory()) { - await walk(absPath, relPath); - continue; - } - if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".html")) continue; - const stat = await fs26.stat(absPath); - files.push({ - relativePath: normalizePublicHtmlPath(`public/${relPath}`), - absolutePath: absPath, - mtimeMs: stat.mtimeMs - }); - } - } - await walk(publicDir, ""); - return files; -} -async function upsertWorkspaceHtmlPage({ - pool: pool2, - pageService, - userId, - indexedPages, - relativePath, - content, - assetId = null, - sourceMtimeMs = null -}) { - const title = extractHtmlTitle(content) || titleFromRelativePath(relativePath); - const summary = extractHtmlSummary(content); - let existing = indexedPages.get(relativePath) ?? (assetId ? indexedPages.get(`asset:${assetId}`) : null); - if (!existing && pool2) { - existing = await findPageByWorkspaceRelativePath(pool2, userId, relativePath); - if (existing) { - indexedPages.set(relativePath, existing); - if (assetId) indexedPages.set(`asset:${assetId}`, existing); - } - } - const pageInput = { - title, - summary, - content, - contentFormat: "html", - pageType: "html", - templateId: "editorial", - categoryCode: "draft" - }; - const snapshot = { - ...assetId ? { source_asset_id: assetId } : {}, - source_category: "public", - content_mode: "static_html", - relative_path: relativePath, - auto_synced: true - }; - if (existing) { - if (sourceMtimeMs != null && existing.updatedAt >= sourceMtimeMs) { - return "skipped"; - } - await pageService.updatePage(userId, existing.id, { - ...pageInput, - changeNote: "\u540C\u6B65\u5DE5\u4F5C\u533A\u9875\u9762\u66F4\u65B0" - }); - indexedPages.set(relativePath, { id: existing.id, updatedAt: Date.now() }); - return "updated"; - } - const page = await pageService.createFromChat(userId, pageInput, { - ...assetId ? { assetId } : {}, - snapshot, - createdAt: sourceMtimeMs != null && Number.isFinite(Number(sourceMtimeMs)) ? Math.floor(Number(sourceMtimeMs)) : void 0 - }); - indexedPages.set(relativePath, { id: page.id, updatedAt: Date.now() }); - if (assetId) indexedPages.set(`asset:${assetId}`, { id: page.id, updatedAt: Date.now() }); - return "created"; -} -async function syncGeneratedPagesFromPublicAssets({ - pool: pool2, - pageService, - assetService, - userId, - publishDir = null, - syncWorkspaceAssets = null -} = {}) { - if (!pool2 || !pageService || !userId) { - return { created: 0, skipped: 0, updated: 0 }; - } - if (typeof syncWorkspaceAssets === "function") { - await syncWorkspaceAssets(userId, { categoryCode: "public" }).catch(() => { - }); - } - const indexedPages = await loadIndexedWorkspacePages(pool2, userId); - let created = 0; - let updated = 0; - let skipped = 0; - if (publishDir) { - const htmlFiles = await listPublishPublicHtmlFiles(publishDir); - for (const file of htmlFiles) { - try { - const content = await fs26.readFile(file.absolutePath, "utf8"); - if (!content.trim()) { - skipped += 1; - continue; - } - const result = await upsertWorkspaceHtmlPage({ - pool: pool2, - pageService, - userId, - indexedPages, - relativePath: file.relativePath, - content, - sourceMtimeMs: file.mtimeMs - }); - if (result === "created") created += 1; - else if (result === "updated") updated += 1; - else skipped += 1; - } catch (error) { - console.warn( - `[MindSpace] page sync failed for ${file.relativePath}:`, - error?.message ?? error - ); - skipped += 1; - } - } - } - if (!assetService) { - return { created, updated, skipped }; - } - const [rows] = await pool2.query( - `SELECT a.id, a.display_name, a.original_filename, a.updated_at - FROM h5_assets a - JOIN h5_space_categories c ON c.id = a.category_id AND c.user_id = a.user_id - WHERE a.user_id = ? - AND c.category_code = 'public' - AND a.mime_type = 'text/html' - AND a.status = 'ready' - ORDER BY a.updated_at DESC - LIMIT 200`, - [userId] - ); - for (const asset of rows) { - const relativePath = normalizePublicHtmlPath( - String(asset.original_filename ?? "").includes("/") ? asset.original_filename : `public/${asset.original_filename}` - ); - if (indexedPages.has(relativePath)) { - skipped += 1; - continue; - } - try { - const { path: assetPath } = await assetService.readAsset(userId, asset.id); - const content = await fs26.readFile(assetPath, "utf8"); - const result = await upsertWorkspaceHtmlPage({ - pool: pool2, - pageService, - userId, - indexedPages, - relativePath, - content, - assetId: asset.id, - sourceMtimeMs: Number(asset.updated_at ?? 0) - }); - if (result === "created") created += 1; - else if (result === "updated") updated += 1; - else skipped += 1; - } catch (error) { - console.warn( - `[MindSpace] asset page sync failed for ${asset.original_filename}:`, - error?.message ?? error - ); - skipped += 1; - } - } - return { created, updated, skipped }; -} - -// mindspace-og-tags.mjs -var PLATFORM_SITE_NAME = "TKMind \u667A\u8DA3"; -var PLATFORM_BRAND_ICON_PATH = "/brand/tkmind-icon.png"; -function rawTitleFromHtml(html) { - return String(html).match(/]*>([^<]*)<\/title>/i)?.[1]?.trim() ?? ""; -} -function escapeAttr(value) { - return String(value).replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">"); -} -function resolveImageUrl(image, { origin, pageDirUrl }) { - if (!image) return null; - const trimmed = String(image).trim(); - if (!trimmed || trimmed.startsWith("data:")) return null; - if (/\.svg(?:[?#]|$)/i.test(trimmed)) return null; - if (/^https?:\/\//i.test(trimmed)) return trimmed; - if (trimmed.startsWith("//")) return `https:${trimmed}`; - if (trimmed.startsWith("/")) return `${origin}${trimmed}`; - return `${pageDirUrl}${trimmed}`; -} -function rawMetaContent(html, attr, name) { - const pattern = new RegExp( - `]+${attr}=["']${name}["'][^>]+content=["']([^"']*)["']|]+content=["']([^"']*)["'][^>]+${attr}=["']${name}["']`, - "i" - ); - const match = String(html).match(pattern); - return match?.[1] || match?.[2] || ""; -} -function hasMeta(html, attr, name) { - return new RegExp(`]+${attr}=["']${name}["']`, "i").test(String(html)); -} -function hasLinkRel(html, rel) { - return new RegExp(`]+rel=["'][^"']*\\b${rel}\\b[^"']*["']`, "i").test(String(html)); -} -function escapeScriptString(value) { - return JSON.stringify(String(value ?? "")).replace(//i.test(source)) { - return source.replace(/<\/head>/i, `${block} -`); - } - if (/]*>/i.test(source)) { - return source.replace(/(]*>)/i, `$1${block}`); - } - return `${block} -${source}`; -} -function resolveShareDescription(html, { siteName = PLATFORM_SITE_NAME, meta = {} } = {}) { - return rawMetaContent(html, "property", "og:description") || rawMetaContent(html, "name", "description") || extractCoverSignals(html, meta).subtitle || `${siteName} \xB7 \u7CBE\u9009\u4F5C\u54C1`; -} -function resolveShareImageUrl(html, { origin, pageDirUrl, fallbackImageUrl = "", meta = {} } = {}) { - const existing = rawMetaContent(html, "property", "og:image"); - if (existing) return existing; - const signals = extractCoverSignals(html, meta); - return resolveImageUrl(signals.image, { origin, pageDirUrl }) || fallbackImageUrl || ""; -} -function extractSharePreviewMeta(html, { - origin = "", - pageUrl = "", - pageDirUrl = "", - fallbackImageUrl = "", - siteName = PLATFORM_SITE_NAME, - brandIconUrl = "", - meta = {} -} = {}) { - const source = String(html ?? ""); - const title = rawMetaContent(source, "property", "og:title") || rawTitleFromHtml(source) || "MindSpace \u9875\u9762"; - const description = resolveShareDescription(source, { siteName, meta }); - const imageUrl = resolveShareImageUrl(source, { origin, pageDirUrl, fallbackImageUrl, meta }); - const resolvedSiteName = rawMetaContent(source, "property", "og:site_name") || siteName; - const iconUrl = brandIconUrl || (origin ? `${origin}${PLATFORM_BRAND_ICON_PATH}` : PLATFORM_BRAND_ICON_PATH); - return { - title, - description, - imageUrl, - siteName: resolvedSiteName, - iconUrl, - pageUrl - }; -} -function injectOgTags(html, { - origin, - pageUrl, - pageDirUrl, - fallbackImageUrl = "", - siteName = PLATFORM_SITE_NAME, - brandIconUrl = "", - meta = {} -} = {}) { - const source = String(html); - const preview = extractSharePreviewMeta(source, { - origin, - pageUrl, - pageDirUrl, - fallbackImageUrl, - siteName, - brandIconUrl, - meta - }); - const tags = []; - if (!hasMeta(source, "property", "og:type")) { - tags.push(''); - } - if (pageUrl && !hasMeta(source, "property", "og:url")) { - tags.push(``); - } - if (preview.title && !hasMeta(source, "property", "og:title")) { - tags.push(``); - } - if (preview.description && !hasMeta(source, "property", "og:description")) { - tags.push(``); - } - if (preview.imageUrl && !hasMeta(source, "property", "og:image")) { - tags.push(``); - } - if (preview.siteName && !hasMeta(source, "property", "og:site_name")) { - tags.push(``); - } - const twitterCard = preview.imageUrl ? "summary_large_image" : "summary"; - if (!hasMeta(source, "name", "twitter:card")) { - tags.push(``); - } - if (preview.title && !hasMeta(source, "name", "twitter:title")) { - tags.push(``); - } - if (preview.description && !hasMeta(source, "name", "twitter:description")) { - tags.push(``); - } - if (preview.imageUrl && !hasMeta(source, "name", "twitter:image")) { - tags.push(``); - } - const links = []; - if (preview.iconUrl && !hasLinkRel(source, "icon")) { - links.push(``); - } - if (preview.iconUrl && !hasLinkRel(source, "apple-touch-icon")) { - links.push(``); - } - const block = [...tags, ...links].map((t) => ` ${t}`).join("\n"); - if (!block) return source; - return appendBeforeHeadClose(source, ` -${block} -`); -} -function injectWechatShareBridge(html, { - pageUrl, - signatureEndpoint = "/auth/wechat/public-js-sdk-signature", - siteName = PLATFORM_SITE_NAME -} = {}) { - const source = String(html ?? ""); - if (!pageUrl) return source; - if (source.includes('data-tkmind-wechat-share="1"')) return source; - const title = rawMetaContent(source, "property", "og:title") || rawTitleFromHtml(source); - const resolvedSiteName = rawMetaContent(source, "property", "og:site_name") || siteName; - const description = resolveShareDescription(source, { siteName: resolvedSiteName }); - const imageUrl = rawMetaContent(source, "property", "og:image") || ""; - const script = ` - `; - 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}

` : ""} -
- - ${siteName} -
-
- ${imageUrl ? `` : `
\u65E0\u56FE
`} -
-
-
og:title ${title}
-
og:description ${description || "\uFF08\u7A7A\uFF09"}
-
og:site_name ${siteName}
-
og:image ${imageUrl || "\uFF08\u7A7A\uFF09"}
-
icon ${iconUrl}
-
-
- -`; -} - -// 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(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16))); -} -function extractPlainTextFromHtml(html) { - return decodeHtmlEntities( - String(html ?? "").replace(//gi, " ").replace(//gi, " ").replace(/<\/(?:p|div|section|article|header|footer|h[1-6]|li|tr|blockquote)>/gi, "\n").replace(//gi, "\n").replace(/<[^>]+>/g, " ").replace(/[ \t\r\f\v]+/g, " ").replace(/\n[ \t]+/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim() - ); -} -function normalizeParagraphs(content) { - return String(content ?? "").split(/\n{1,}/).map((line) => line.replace(/^#{1,6}\s+/, "").replace(/[*_`~]/g, "").trim()).filter(Boolean).slice(0, 500); -} -function paragraphXml(text, { heading = false } = {}) { - const style = heading ? '' : ""; - const runStyle = heading ? '' : ""; - return `${style}${runStyle}${escapeXml3(text)}`; -} -function documentXml({ title, paragraphs }) { - const body = [ - title ? paragraphXml(title, { heading: true }) : "", - ...paragraphs.map((paragraph) => paragraphXml(paragraph)) - ].join(""); - return ` - - ${body} -`; -} -var CRC_TABLE = (() => { - const table = new Uint32Array(256); - for (let n = 0; n < 256; n += 1) { - let c = n; - for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1; - table[n] = c >>> 0; - } - return table; -})(); -function crc32(buffer) { - let crc = 4294967295; - for (const byte of buffer) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8; - return (crc ^ 4294967295) >>> 0; -} -function dosDateTime(date = /* @__PURE__ */ new Date()) { - const year = Math.max(1980, date.getFullYear()); - const dosTime = date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2); - const dosDate = year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate(); - return { dosTime, dosDate }; -} -function writeZipEntry(name, content, offset) { - const nameBuffer = Buffer.from(name); - const data = Buffer.isBuffer(content) ? content : Buffer.from(String(content), "utf8"); - const checksum = crc32(data); - const { dosTime, dosDate } = dosDateTime(); - const local = Buffer.alloc(30 + nameBuffer.length); - local.writeUInt32LE(67324752, 0); - local.writeUInt16LE(20, 4); - local.writeUInt16LE(0, 6); - local.writeUInt16LE(0, 8); - local.writeUInt16LE(dosTime, 10); - local.writeUInt16LE(dosDate, 12); - local.writeUInt32LE(checksum, 14); - local.writeUInt32LE(data.length, 18); - local.writeUInt32LE(data.length, 22); - local.writeUInt16LE(nameBuffer.length, 26); - nameBuffer.copy(local, 30); - const central = Buffer.alloc(46 + nameBuffer.length); - central.writeUInt32LE(33639248, 0); - central.writeUInt16LE(20, 4); - central.writeUInt16LE(20, 6); - central.writeUInt16LE(0, 8); - central.writeUInt16LE(0, 10); - central.writeUInt16LE(dosTime, 12); - central.writeUInt16LE(dosDate, 14); - central.writeUInt32LE(checksum, 16); - central.writeUInt32LE(data.length, 20); - central.writeUInt32LE(data.length, 24); - central.writeUInt16LE(nameBuffer.length, 28); - central.writeUInt32LE(offset, 42); - nameBuffer.copy(central, 46); - return { local, data, central }; -} -function zip(entries) { - let offset = 0; - const locals = []; - const centrals = []; - for (const [name, content] of entries) { - const entry = writeZipEntry(name, content, offset); - locals.push(entry.local, entry.data); - centrals.push(entry.central); - offset += entry.local.length + entry.data.length; - } - const centralSize = centrals.reduce((sum, item) => sum + item.length, 0); - const end = Buffer.alloc(22); - end.writeUInt32LE(101010256, 0); - end.writeUInt16LE(entries.length, 8); - end.writeUInt16LE(entries.length, 10); - end.writeUInt32LE(centralSize, 12); - end.writeUInt32LE(offset, 16); - return Buffer.concat([...locals, ...centrals, end]); -} -function generateDocxBuffer({ title = "\u6587\u6863", content = "", contentFormat = "markdown" } = {}) { - const plain = contentFormat === "html" ? extractPlainTextFromHtml(content) : String(content ?? ""); - const paragraphs = normalizeParagraphs(plain); - return zip([ - ["[Content_Types].xml", CONTENT_TYPES], - ["_rels/.rels", ROOT_RELS], - ["word/document.xml", documentXml({ title, paragraphs: paragraphs.length > 0 ? paragraphs : [""] })], - ["word/_rels/document.xml.rels", DOCUMENT_RELS] - ]); -} - -// mindspace-chat-docx-package.mjs -import crypto25 from "node:crypto"; -var DOCX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; -async function registerChatDocxArtifactForConversation({ - registry, - user, - bundle, - requestBody = {}, - filename, - buffer, - now = Date.now() -}) { - if (!registry || !user?.id || !filename || !Buffer.isBuffer(buffer)) return null; - const sessionId = String(requestBody.session_id ?? requestBody.sessionId ?? "").trim(); - const messageId2 = String(requestBody.message_id ?? requestBody.messageId ?? "").trim(); - if (!sessionId || !messageId2) return null; - const selectedLinkIndex = Number(requestBody.selected_link_index ?? requestBody.selectedLinkIndex ?? 0); - const hash = crypto25.createHash("sha256").update(`${user.id}:${sessionId}:${messageId2}:${selectedLinkIndex}:${filename}`).digest("hex").slice(0, 16); - const artifactId = `ca_docx_${hash}`; - const relativePath = `artifacts/${artifactId}/${filename}`; - const { packageRecord, writeResult } = await registry.putObjectForSession({ - userId: user.id, - sessionId, - title: bundle?.source?.session?.name ?? null, - relativePath, - body: buffer - }); - const canonicalUrl = `/api/mindspace/v1/conversation-packages/${encodeURIComponent(sessionId)}/artifacts/${encodeURIComponent(artifactId)}/download`; - const artifact = await registry.recordArtifact({ - id: artifactId, - packageId: packageRecord.id, - artifactKind: "docx", - messageId: messageId2, - displayName: filename, - mimeType: DOCX_MIME_TYPE, - sizeBytes: buffer.length, - storageKey: writeResult.key, - canonicalUrl, - sortOrder: now, - now - }); - await registry.writeManifestForSession({ - userId: user.id, - sessionId - }); - return artifact; -} - -// billing-subscription.mjs -import crypto26 from "node:crypto"; -var PLAN_CATALOG = { - free: { - name: "\u514D\u8D39\u7248", - priceCents: 0, - periodTokens: 45e4, - periodImages: 10, - modelTier: "basic", - overageRate: 1, - periodDays: 30 - }, - lite: { - name: "\u8F7B\u91CF\u7248", - priceCents: 990, - periodTokens: 36e5, - periodImages: 50, - modelTier: "basic", - overageRate: 0.5, - periodDays: 30 - }, - standard: { - name: "\u6807\u51C6\u7248", - priceCents: 2900, - periodTokens: 135e5, - periodImages: 200, - modelTier: "standard", - overageRate: 0.7, - periodDays: 30 - }, - pro: { - name: "\u4E13\u4E1A\u7248", - priceCents: 7900, - periodTokens: 0, - periodImages: 0, - modelTier: "premium", - overageRate: 0.8, - periodDays: 30 - } -}; -var PLAN_TOKEN_ALLOWANCE_MIGRATIONS = [ - ["free", 15e4, PLAN_CATALOG.free.periodTokens], - ["lite", 12e5, PLAN_CATALOG.lite.periodTokens], - ["standard", 45e5, PLAN_CATALOG.standard.periodTokens] -]; -function getPlanDef(planType) { - return PLAN_CATALOG[planType] ?? null; -} -var PLAN_ORDER = Object.fromEntries( - Object.keys(PLAN_CATALOG).map((key, i) => [key, i]) -); -function mapSubRow(row) { - if (!row) return null; - return { - id: row.id, - userId: row.user_id, - planType: row.plan_type, - status: row.status, - periodTokensLimit: Number(row.period_tokens_limit ?? 0), - periodTokensUsed: Number(row.period_tokens_used ?? 0), - periodImagesLimit: Number(row.period_images_limit ?? 0), - periodImagesUsed: Number(row.period_images_used ?? 0), - periodStart: Number(row.period_start), - periodEnd: Number(row.period_end), - expiresAt: Number(row.expires_at), - overageRate: Number(row.overage_rate ?? 1), - autoRenew: Boolean(row.auto_renew), - operatorId: row.operator_id ?? null, - note: row.note ?? null, - createdAt: Number(row.created_at), - updatedAt: Number(row.updated_at) - }; -} -function createSubscriptionService(pool2, { getPlanAsync = null } = {}) { - const resolvePlan = async (planType) => { - if (getPlanAsync) { - const p2 = await getPlanAsync(planType); - if (p2) return p2; - } - const p = getPlanDef(planType); - return p ? { ...p, periodImages: p.periodImages ?? 0 } : null; - }; - const getActiveSubscription = async (userId) => { - const now = Date.now(); - const [rows] = await pool2.query( - `SELECT * FROM h5_subscriptions - WHERE user_id = ? AND status = 'active' AND expires_at > ? - ORDER BY expires_at DESC LIMIT 1`, - [userId, now] - ); - return mapSubRow(rows[0]); - }; - const grantSubscription = async (userId, planType, durationDays, operatorId = null, note = "") => { - const plan = await resolvePlan(planType); - if (!plan) return { ok: false, message: `\u672A\u77E5\u5957\u9910\u7C7B\u578B: ${planType}` }; - const now = Date.now(); - const periodDays = durationDays ?? plan.periodDays; - const periodEnd = now + periodDays * 24 * 60 * 60 * 1e3; - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - await conn.query( - `UPDATE h5_subscriptions SET status = 'cancelled', updated_at = ? - WHERE user_id = ? AND status = 'active'`, - [now, userId] - ); - const id = crypto26.randomUUID(); - await conn.query( - `INSERT INTO h5_subscriptions - (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used, - period_images_limit, period_images_used, - period_start, period_end, expires_at, overage_rate, auto_renew, - operator_id, note, created_at, updated_at) - VALUES (?, ?, ?, 'active', ?, 0, ?, 0, ?, ?, ?, ?, 0, ?, ?, ?, ?)`, - [ - id, - userId, - planType, - plan.periodTokens, - plan.periodImages ?? 0, - now, - periodEnd, - periodEnd, - Number(plan.overageRate.toFixed(2)), - operatorId, - note || null, - now, - now - ] - ); - await conn.query( - `UPDATE h5_users SET plan_type = ?, updated_at = ? WHERE id = ?`, - [planType, now, userId] - ); - await conn.commit(); - const [rows] = await pool2.query(`SELECT * FROM h5_subscriptions WHERE id = ?`, [id]); - return { ok: true, subscription: mapSubRow(rows[0]) }; - } catch (err) { - await conn.rollback(); - throw err; - } finally { - conn.release(); - } - }; - const consumeQuota = async (userId, deltaTokens, conn) => { - if (!deltaTokens || deltaTokens <= 0) { - return { fullyCovers: true, overageRate: 1 }; - } - const now = Date.now(); - const [rows] = await conn.query( - `SELECT * FROM h5_subscriptions - WHERE user_id = ? AND status = 'active' AND expires_at > ? - ORDER BY expires_at DESC LIMIT 1 - FOR UPDATE`, - [userId, now] - ); - const sub = mapSubRow(rows[0]); - if (!sub) return { fullyCovers: false, overageRate: 1 }; - const unlimited = sub.periodTokensLimit === 0; - const remaining = unlimited ? Infinity : sub.periodTokensLimit - sub.periodTokensUsed; - if (unlimited || remaining >= deltaTokens) { - await conn.query( - `UPDATE h5_subscriptions - SET period_tokens_used = period_tokens_used + ?, updated_at = ? - WHERE id = ?`, - [deltaTokens, now, sub.id] - ); - return { fullyCovers: true, overageRate: sub.overageRate }; - } - if (remaining > 0) { - await conn.query( - `UPDATE h5_subscriptions - SET period_tokens_used = period_tokens_limit, updated_at = ? - WHERE id = ?`, - [now, sub.id] - ); - } - return { fullyCovers: false, overageRate: sub.overageRate }; - }; - const renewSubscription = async (subId, operatorId = null) => { - const [rows] = await pool2.query(`SELECT * FROM h5_subscriptions WHERE id = ?`, [subId]); - const sub = mapSubRow(rows[0]); - if (!sub) return { ok: false, message: "\u8BA2\u9605\u4E0D\u5B58\u5728" }; - const plan = await resolvePlan(sub.planType); - if (!plan) return { ok: false, message: "\u5957\u9910\u5B9A\u4E49\u5DF2\u5931\u6548" }; - const now = Date.now(); - const newPeriodEnd = now + plan.periodDays * 24 * 60 * 60 * 1e3; - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const newId = crypto26.randomUUID(); - await conn.query( - `UPDATE h5_subscriptions SET status = 'expired', updated_at = ? WHERE id = ?`, - [now, subId] - ); - await conn.query( - `INSERT INTO h5_subscriptions - (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used, - period_images_limit, period_images_used, - period_start, period_end, expires_at, overage_rate, auto_renew, - operator_id, note, created_at, updated_at) - VALUES (?, ?, ?, 'active', ?, 0, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - newId, - sub.userId, - sub.planType, - plan.periodTokens, - plan.periodImages ?? 0, - now, - newPeriodEnd, - newPeriodEnd, - Number(plan.overageRate.toFixed(2)), - sub.autoRenew ? 1 : 0, - operatorId, - "\u81EA\u52A8\u7EED\u8BA2", - now, - now - ] - ); - await conn.commit(); - const [newRows] = await pool2.query(`SELECT * FROM h5_subscriptions WHERE id = ?`, [newId]); - return { ok: true, subscription: mapSubRow(newRows[0]) }; - } catch (err) { - await conn.rollback(); - throw err; - } finally { - conn.release(); - } - }; - const expireStaleSubscriptions = async () => { - const now = Date.now(); - const [result] = await pool2.query( - `UPDATE h5_subscriptions SET status = 'expired', updated_at = ? - WHERE status = 'active' AND expires_at <= ?`, - [now, now] - ); - if (result.affectedRows > 0) { - await pool2.query( - `UPDATE h5_users u - LEFT JOIN h5_subscriptions s - ON s.user_id = u.id AND s.status = 'active' AND s.expires_at > ? - SET u.plan_type = 'free', u.updated_at = ? - WHERE u.plan_type != 'free' AND s.id IS NULL`, - [now, now] - ); - } - return result.affectedRows; - }; - const cancelSubscription = async (userId, operatorId = null) => { - const now = Date.now(); - const [result] = await pool2.query( - `UPDATE h5_subscriptions SET status = 'cancelled', updated_at = ? - WHERE user_id = ? AND status = 'active'`, - [now, userId] - ); - if (result.affectedRows > 0) { - await pool2.query( - `UPDATE h5_users SET plan_type = 'free', updated_at = ? WHERE id = ?`, - [now, userId] - ); - } - return { ok: true, cancelled: result.affectedRows > 0 }; - }; - const listSubscriptions = async ({ userId = null, status = null, page = 1, pageSize = 20 } = {}) => { - 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 (userId) { - clauses.push("s.user_id = ?"); - params.push(userId); - } - if (status) { - clauses.push("s.status = ?"); - params.push(status); - } - const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""; - const [[{ total }]] = await pool2.query( - `SELECT COUNT(*) AS total FROM h5_subscriptions s ${where}`, - params - ); - const [rows] = await pool2.query( - `SELECT s.*, u.username, u.display_name - FROM h5_subscriptions s JOIN h5_users u ON u.id = s.user_id - ${where} ORDER BY s.created_at DESC LIMIT ${safePageSize} OFFSET ${offset}`, - params - ); - return { - total: Number(total), - page: safePage, - pageSize: safePageSize, - items: rows.map((r) => ({ - ...mapSubRow(r), - username: r.username, - displayName: r.display_name - })) - }; - }; - const purchaseSubscription = async (userId, planType, autoRenew = false) => { - const plan = await resolvePlan(planType); - if (!plan || plan.priceCents === 0) { - return { ok: false, message: "\u65E0\u6548\u5957\u9910\u6216\u514D\u8D39\u5957\u9910\u4E0D\u53EF\u8D2D\u4E70" }; - } - const now = Date.now(); - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [currentSubRows] = await conn.query( - `SELECT * FROM h5_subscriptions - WHERE user_id = ? AND status = 'active' AND expires_at > ? - ORDER BY expires_at DESC LIMIT 1 FOR UPDATE`, - [userId, now] - ); - const currentSub = mapSubRow(currentSubRows[0]); - if (currentSub) { - const currentOrder = PLAN_ORDER[currentSub.planType] ?? 0; - const newOrder = PLAN_ORDER[planType] ?? 0; - if (newOrder < currentOrder) { - await conn.rollback(); - return { - ok: false, - code: "DOWNGRADE_NOT_ALLOWED", - message: "\u5F53\u524D\u5957\u9910\u6709\u6548\u671F\u5185\u4E0D\u53EF\u964D\u7EA7\uFF0C\u5230\u671F\u540E\u5C06\u81EA\u52A8\u964D\u4E3A\u514D\u8D39\u7248", - currentPlanType: currentSub.planType - }; - } - } - 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 < plan.priceCents) { - await conn.rollback(); - return { - ok: false, - code: "INSUFFICIENT_BALANCE", - message: "\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u8BA2\u9605", - balanceCents, - requiredCents: plan.priceCents, - shortfallCents: plan.priceCents - balanceCents - }; - } - await conn.query( - `UPDATE h5_user_wallets SET balance_cents = balance_cents - ?, updated_at = ? WHERE user_id = ?`, - [plan.priceCents, 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, plan.priceCents, `subscription:${planType}`, now] - ); - await conn.query( - `UPDATE h5_subscriptions SET status = 'cancelled', updated_at = ? - WHERE user_id = ? AND status = 'active'`, - [now, userId] - ); - const id = crypto26.randomUUID(); - const periodEnd = now + plan.periodDays * 24 * 60 * 60 * 1e3; - await conn.query( - `INSERT INTO h5_subscriptions - (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used, - period_images_limit, period_images_used, - period_start, period_end, expires_at, overage_rate, auto_renew, - operator_id, note, created_at, updated_at) - VALUES (?, ?, ?, 'active', ?, 0, ?, 0, ?, ?, ?, ?, ?, NULL, '\u7528\u6237\u81EA\u52A9\u8D2D\u4E70', ?, ?)`, - [ - id, - userId, - planType, - plan.periodTokens, - plan.periodImages ?? 0, - now, - periodEnd, - periodEnd, - Number(plan.overageRate.toFixed(2)), - autoRenew ? 1 : 0, - now, - now - ] - ); - await conn.query( - `UPDATE h5_users SET plan_type = ?, updated_at = ? WHERE id = ?`, - [planType, now, userId] - ); - await conn.commit(); - const [subRows] = await pool2.query(`SELECT * FROM h5_subscriptions WHERE id = ?`, [id]); - return { - ok: true, - subscription: mapSubRow(subRows[0]), - balanceCents: balanceCents - plan.priceCents - }; - } catch (err) { - await conn.rollback(); - throw err; - } finally { - conn.release(); - } - }; - const setAutoRenew = async (userId, enabled) => { - const now = Date.now(); - const [result] = await pool2.query( - `UPDATE h5_subscriptions SET auto_renew = ?, updated_at = ? - WHERE user_id = ? AND status = 'active' AND expires_at > ?`, - [enabled ? 1 : 0, now, userId, now] - ); - return { ok: true, updated: result.affectedRows > 0 }; - }; - const processAutoRenewals = async () => { - const now = Date.now(); - const [rows] = await pool2.query( - `SELECT s.*, w.balance_cents - FROM h5_subscriptions s - JOIN h5_user_wallets w ON w.user_id = s.user_id - WHERE s.status = 'active' AND s.auto_renew = 1 AND s.expires_at <= ?`, - [now] - ); - let renewed = 0; - let failed = 0; - for (const row of rows) { - const sub = mapSubRow(row); - const plan = await resolvePlan(sub.planType); - if (!plan || plan.priceCents === 0) continue; - const balance = Number(row.balance_cents ?? 0); - const conn = await pool2.getConnection(); - try { - await conn.beginTransaction(); - const [walletRows] = await conn.query( - `SELECT balance_cents FROM h5_user_wallets WHERE user_id = ? FOR UPDATE`, - [sub.userId] - ); - const balanceCents = Number(walletRows[0]?.balance_cents ?? 0); - if (balanceCents < plan.priceCents) { - await conn.rollback(); - failed++; - console.log(`Auto-renew failed for user ${sub.userId} (${sub.planType}): insufficient balance`); - continue; - } - await conn.query( - `UPDATE h5_user_wallets SET balance_cents = balance_cents - ?, updated_at = ? WHERE user_id = ?`, - [plan.priceCents, now, sub.userId] - ); - await conn.query( - `INSERT INTO h5_billing_ledger (user_id, type, amount_cents, tokens, note, operator_id, created_at) - VALUES (?, 'deduct', ?, 0, ?, NULL, ?)`, - [sub.userId, plan.priceCents, `auto_renew:${sub.planType}`, now] - ); - await conn.query( - `UPDATE h5_subscriptions SET status = 'expired', auto_renew = 0, updated_at = ? WHERE id = ?`, - [now, sub.id] - ); - const newId = crypto26.randomUUID(); - const newPeriodEnd = now + plan.periodDays * 24 * 60 * 60 * 1e3; - await conn.query( - `INSERT INTO h5_subscriptions - (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used, - period_images_limit, period_images_used, - period_start, period_end, expires_at, overage_rate, auto_renew, - operator_id, note, created_at, updated_at) - VALUES (?, ?, ?, 'active', ?, 0, ?, 0, ?, ?, ?, ?, 1, NULL, '\u81EA\u52A8\u7EED\u8D39', ?, ?)`, - [ - newId, - sub.userId, - sub.planType, - plan.periodTokens, - plan.periodImages ?? 0, - now, - newPeriodEnd, - newPeriodEnd, - Number(plan.overageRate.toFixed(2)), - now, - now - ] - ); - await conn.query( - `UPDATE h5_users SET plan_type = ?, updated_at = ? WHERE id = ?`, - [sub.planType, now, sub.userId] - ); - await conn.commit(); - renewed++; - console.log(`Auto-renewed ${sub.planType} for user ${sub.userId}`); - } catch (err) { - await conn.rollback(); - console.warn(`Auto-renew error for user ${sub.userId}:`, err); - failed++; - } finally { - conn.release(); - } - } - return { renewed, failed }; - }; - const consumeImageQuotaTx = async (userId, count, conn) => { - if (!count || count <= 0) return { fullyCovers: true }; - const now = Date.now(); - const [rows] = await conn.query( - `SELECT * FROM h5_subscriptions - WHERE user_id = ? AND status = 'active' AND expires_at > ? - ORDER BY expires_at DESC LIMIT 1 FOR UPDATE`, - [userId, now] - ); - const sub = mapSubRow(rows[0]); - if (!sub) return { fullyCovers: false }; - const unlimited = sub.periodImagesLimit === 0; - const remaining = unlimited ? Infinity : sub.periodImagesLimit - sub.periodImagesUsed; - if (unlimited || remaining >= count) { - await conn.query( - `UPDATE h5_subscriptions - SET period_images_used = period_images_used + ?, updated_at = ? - WHERE id = ?`, - [count, now, sub.id] - ); - return { fullyCovers: true }; - } - return { fullyCovers: false }; - }; - const consumeImageQuota = async (userId, count, conn = null) => { - if (conn) return consumeImageQuotaTx(userId, count, conn); - const ownConn = await pool2.getConnection(); - try { - await ownConn.beginTransaction(); - const result = await consumeImageQuotaTx(userId, count, ownConn); - await ownConn.commit(); - return result; - } catch (err) { - await ownConn.rollback(); - throw err; - } finally { - ownConn.release(); - } - }; - return { - getActiveSubscription, - grantSubscription, - purchaseSubscription, - setAutoRenew, - processAutoRenewals, - consumeQuota, - consumeImageQuota, - renewSubscription, - expireStaleSubscriptions, - cancelSubscription, - listSubscriptions - }; -} -async function ensurePlanCatalogSchema(pool2) { - await pool2.query(` - 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 - `); - for (const col of [ - "ALTER TABLE h5_subscriptions ADD COLUMN period_images_limit INT NOT NULL DEFAULT 0", - "ALTER TABLE h5_subscriptions ADD COLUMN period_images_used INT NOT NULL DEFAULT 0" - ]) { - try { - await pool2.query(col); - } catch (_) { - } - } - const [[{ cnt }]] = await pool2.query(`SELECT COUNT(*) AS cnt FROM h5_plan_catalog`); - if (Number(cnt) === 0) { - const now2 = Date.now(); - const entries = Object.entries(PLAN_CATALOG); - for (const [i, [planType, plan]] of entries.entries()) { - await pool2.query( - `INSERT IGNORE INTO h5_plan_catalog - (plan_type, name, price_cents, period_days, period_tokens, period_images, - model_tier, overage_rate, sort_order, is_active, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`, - [ - planType, - plan.name, - plan.priceCents, - plan.periodDays, - plan.periodTokens, - plan.periodImages ?? 0, - plan.modelTier, - plan.overageRate, - i, - now2, - now2 - ] - ); - } - } - const now = Date.now(); - for (const [planType, previousTokens, nextTokens] of PLAN_TOKEN_ALLOWANCE_MIGRATIONS) { - if (nextTokens <= previousTokens) continue; - await pool2.query( - `UPDATE h5_plan_catalog - SET period_tokens = ?, updated_at = ? - WHERE plan_type = ? AND period_tokens = ?`, - [nextTokens, now, planType, previousTokens] - ); - await pool2.query( - `UPDATE h5_subscriptions - SET period_tokens_limit = ?, updated_at = ? - WHERE status = 'active' AND plan_type = ? AND period_tokens_limit = ?`, - [nextTokens, now, planType, previousTokens] - ); - } -} -function createPlanCatalogService(pool2) { - function mapPlanRow(row) { - return { - planType: row.plan_type, - name: row.name, - priceCents: Number(row.price_cents), - periodDays: Number(row.period_days), - periodTokens: Number(row.period_tokens), - periodImages: Number(row.period_images), - modelTier: row.model_tier, - overageRate: Number(row.overage_rate), - sortOrder: Number(row.sort_order), - isActive: Boolean(row.is_active), - description: row.description ?? null - }; - } - const listPlans = async ({ includeInactive = true } = {}) => { - const where = includeInactive ? "" : "WHERE is_active = 1"; - const [rows] = await pool2.query( - `SELECT * FROM h5_plan_catalog ${where} ORDER BY sort_order ASC, price_cents ASC` - ); - return rows.map(mapPlanRow); - }; - const getPlan = async (planType) => { - const [rows] = await pool2.query( - `SELECT * FROM h5_plan_catalog WHERE plan_type = ?`, - [planType] - ); - return rows[0] ? mapPlanRow(rows[0]) : null; - }; - const upsertPlan = async (planType, data) => { - if (!planType || !/^[a-z0-9_]{1,32}$/.test(planType)) { - return { ok: false, message: "\u5957\u9910\u6807\u8BC6\u53EA\u80FD\u4F7F\u7528\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u4E0B\u5212\u7EBF\uFF0C\u6700\u957F32\u4F4D" }; - } - const now = Date.now(); - await pool2.query( - `INSERT INTO h5_plan_catalog - (plan_type, name, price_cents, period_days, period_tokens, period_images, - model_tier, overage_rate, sort_order, is_active, description, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - name = VALUES(name), price_cents = VALUES(price_cents), - period_days = VALUES(period_days), period_tokens = VALUES(period_tokens), - period_images = VALUES(period_images), model_tier = VALUES(model_tier), - overage_rate = VALUES(overage_rate), sort_order = VALUES(sort_order), - is_active = VALUES(is_active), description = VALUES(description), - updated_at = VALUES(updated_at)`, - [ - planType, - String(data.name ?? "").trim() || planType, - Math.max(0, Math.round(Number(data.priceCents ?? 0))), - Math.max(1, Math.round(Number(data.periodDays ?? 30))), - Math.max(0, Math.round(Number(data.periodTokens ?? 0))), - Math.max(0, Math.round(Number(data.periodImages ?? 0))), - ["basic", "standard", "premium"].includes(data.modelTier) ? data.modelTier : "basic", - Math.min(2, Math.max(0, Number((data.overageRate ?? 1).toFixed(2)))), - Math.round(Number(data.sortOrder ?? 0)), - data.isActive !== false ? 1 : 0, - data.description ? String(data.description).trim() : null, - now, - now - ] - ); - return { ok: true, plan: await getPlan(planType) }; - }; - const deletePlan = async (planType) => { - if (planType === "free") return { ok: false, message: "\u514D\u8D39\u5957\u9910\u4E0D\u53EF\u5220\u9664" }; - const [result] = await pool2.query( - `DELETE FROM h5_plan_catalog WHERE plan_type = ?`, - [planType] - ); - return { ok: true, deleted: result.affectedRows > 0 }; - }; - return { listPlans, getPlan, upsertPlan, deletePlan }; -} - -// wechat-pay.mjs -import crypto27 from "node:crypto"; -import fs29 from "node:fs"; -import { fetch as fetch2 } from "undici"; -var API_BASE = "https://api.mch.weixin.qq.com"; -function readPrivateKey(config) { - if (config.privateKeyPem) return config.privateKeyPem; - if (config.privateKeyPath && fs29.existsSync(config.privateKeyPath)) { - return fs29.readFileSync(config.privateKeyPath, "utf8"); - } - return null; -} -function readPlatformCert(config) { - if (config.platformCertPem) return config.platformCertPem; - if (config.platformCertPath && fs29.existsSync(config.platformCertPath)) { - return fs29.readFileSync(config.platformCertPath, "utf8"); - } - return null; -} -function resolveNotifyUrl() { - const notifyUrl = process.env.H5_WECHAT_NOTIFY_URL?.trim() ?? ""; - const publicBaseUrl = process.env.H5_PUBLIC_BASE_URL?.trim() ?? ""; - return notifyUrl || (publicBaseUrl ? `${publicBaseUrl.replace(/\/$/, "")}/webhooks/wechat-pay/notify` : ""); -} -function detectApiVersion({ explicitVersion, apiV2Key, v3Ready }) { - const normalized = explicitVersion?.trim().toLowerCase(); - if (normalized === "v2" || normalized === "2") return "v2"; - if (normalized === "v3" || normalized === "3") return "v3"; - if (v3Ready) return "v3"; - if (apiV2Key) return "v2"; - return null; -} -function loadWechatPayConfig() { - const enabled = process.env.H5_WECHAT_PAY_ENABLED === "1"; - const appId = process.env.H5_WECHAT_APP_ID?.trim() ?? ""; - const mchId = process.env.H5_WECHAT_MCH_ID?.trim() ?? ""; - const apiV2Key = process.env.H5_WECHAT_API_V2_KEY?.trim() ?? process.env.H5_WECHAT_API_KEY?.trim() ?? ""; - const apiV3Key = process.env.H5_WECHAT_API_V3_KEY?.trim() ?? ""; - const serialNo = process.env.H5_WECHAT_SERIAL_NO?.trim() ?? ""; - const privateKeyPath = process.env.H5_WECHAT_PRIVATE_KEY_PATH?.trim() ?? ""; - const privateKeyPem = process.env.H5_WECHAT_PRIVATE_KEY?.replace(/\\n/g, "\n").trim() ?? ""; - const platformCertPath = process.env.H5_WECHAT_PLATFORM_CERT_PATH?.trim() ?? ""; - const platformCertPem = process.env.H5_WECHAT_PLATFORM_CERT?.replace(/\\n/g, "\n").trim() ?? ""; - const resolvedNotifyUrl = resolveNotifyUrl(); - const skipNotifyVerify = process.env.H5_WECHAT_SKIP_NOTIFY_VERIFY === "1"; - const privateKey = readPrivateKey({ privateKeyPath, privateKeyPem }); - const platformCert = readPlatformCert({ platformCertPath, platformCertPem }); - const v3Ready = Boolean(appId && mchId && apiV3Key && serialNo && privateKey && resolvedNotifyUrl); - const apiVersion = detectApiVersion({ - explicitVersion: process.env.H5_WECHAT_API_VERSION, - apiV2Key, - v3Ready: Boolean(apiV3Key && serialNo && privateKey) - }); - if (apiVersion === "v2") { - const configured2 = Boolean(appId && mchId && apiV2Key && resolvedNotifyUrl); - return { - enabled: enabled && configured2, - apiVersion: "v2", - appId, - mchId, - apiKey: apiV2Key, - notifyUrl: resolvedNotifyUrl, - skipNotifyVerify - }; - } - const configured = v3Ready; - return { - enabled: enabled && configured, - apiVersion: configured ? "v3" : apiVersion, - appId, - mchId, - apiV3Key, - serialNo, - privateKey, - platformCert, - notifyUrl: resolvedNotifyUrl, - skipNotifyVerify - }; -} -function randomNonce(length = 32) { - return crypto27.randomBytes(length).toString("hex").slice(0, length); -} -function signV2Params(params, apiKey) { - const stringA = Object.keys(params).filter((key) => key !== "sign" && params[key] !== void 0 && params[key] !== "").sort().map((key) => `${key}=${params[key]}`).join("&"); - const stringSignTemp = `${stringA}&key=${apiKey}`; - return crypto27.createHash("md5").update(stringSignTemp, "utf8").digest("hex").toUpperCase(); -} -function buildXml(fields) { - const parts = [""]; - for (const [key, value] of Object.entries(fields)) { - if (value === void 0 || value === "") continue; - parts.push(`<${key}>`); - } - parts.push(""); - return parts.join(""); -} -function parseXmlFields(xml) { - const result = {}; - const re = /<(\w+)>(?:|([^<]*))<\/\1>/g; - let match = re.exec(xml); - while (match) { - result[match[1]] = match[2] ?? match[3] ?? ""; - match = re.exec(xml); - } - return result; -} -function normalizeV2Transaction(fields) { - return { - out_trade_no: fields.out_trade_no, - transaction_id: fields.transaction_id, - trade_state: fields.result_code === "SUCCESS" ? "SUCCESS" : fields.result_code, - result_code: fields.result_code, - return_code: fields.return_code, - total_fee: fields.total_fee ? Number(fields.total_fee) : void 0, - amount: fields.total_fee ? { total: Number(fields.total_fee) } : void 0 - }; -} -async function wechatV2Request(config, path35, fields) { - const payload = { - appid: config.appId, - mch_id: config.mchId, - nonce_str: randomNonce(), - ...fields - }; - payload.sign = signV2Params(payload, config.apiKey); - const body = buildXml(payload); - const res = await fetch2(`${API_BASE}${path35}`, { - method: "POST", - headers: { "Content-Type": "text/xml" }, - body - }); - const text = await res.text(); - const data = parseXmlFields(text); - if (!res.ok) { - throw new Error(`\u5FAE\u4FE1\u652F\u4ED8\u8BF7\u6C42\u5931\u8D25 (${res.status}): ${text}`); - } - if (data.return_code !== "SUCCESS") { - throw new Error(`\u5FAE\u4FE1\u652F\u4ED8\u8BF7\u6C42\u5931\u8D25: ${data.return_msg || data.return_code || text}`); - } - if (data.result_code !== "SUCCESS") { - throw new Error(`\u5FAE\u4FE1\u652F\u4ED8\u4E0B\u5355\u5931\u8D25: ${data.err_code_des || data.err_code || text}`); - } - return data; -} -function signMessage(message, privateKeyPem) { - return crypto27.createSign("RSA-SHA256").update(message).sign(privateKeyPem, "base64"); -} -function buildAuthorization({ mchId, serialNo, privateKey }, method, pathWithQuery, body) { - const timestamp = String(Math.floor(Date.now() / 1e3)); - const nonce = randomNonce(); - const payload = body ?? ""; - const message = `${method} -${pathWithQuery} -${timestamp} -${nonce} -${payload} -`; - const signature = signMessage(message, privateKey); - const authorization = [ - "WECHATPAY2-SHA256-RSA2048", - `mchid="${mchId}"`, - `nonce_str="${nonce}"`, - `signature="${signature}"`, - `timestamp="${timestamp}"`, - `serial_no="${serialNo}"` - ].join(","); - return { authorization, timestamp, nonce, signature }; -} -async function wechatV3Request(config, method, path35, bodyObj) { - const body = bodyObj ? JSON.stringify(bodyObj) : ""; - const { authorization } = buildAuthorization(config, method, path35, body); - const res = await fetch2(`${API_BASE}${path35}`, { - method, - headers: { - Authorization: authorization, - Accept: "application/json", - "Content-Type": "application/json" - }, - body: body || void 0 - }); - const text = await res.text(); - let data = null; - try { - data = text ? JSON.parse(text) : null; - } catch { - data = { raw: text }; - } - if (!res.ok) { - const detail = data?.message ?? data?.detail ?? text; - throw new Error(`\u5FAE\u4FE1\u652F\u4ED8\u8BF7\u6C42\u5931\u8D25 (${res.status}): ${detail}`); - } - return data; -} -function buildV2JsapiParams(config, prepayId) { - const timeStamp = String(Math.floor(Date.now() / 1e3)); - const nonceStr = randomNonce(16); - const pkg = `prepay_id=${prepayId}`; - const signType = "MD5"; - const paySign = signV2Params( - { - appId: config.appId, - timeStamp, - nonceStr, - package: pkg, - signType - }, - config.apiKey - ); - return { appId: config.appId, timeStamp, nonceStr, package: pkg, signType, paySign }; -} -function buildV3JsapiParams(config, prepayId) { - const timeStamp = String(Math.floor(Date.now() / 1e3)); - const nonceStr = randomNonce(16); - const pkg = `prepay_id=${prepayId}`; - const message = `${config.appId} -${timeStamp} -${nonceStr} -${pkg} -`; - const paySign = signMessage(message, config.privateKey); - return { appId: config.appId, timeStamp, nonceStr, package: pkg, signType: "RSA", paySign }; -} -function createV2Client(config) { - const createNativeOrder = async ({ outTradeNo, description, amountCents, clientIp }) => { - const data = await wechatV2Request(config, "/pay/unifiedorder", { - body: description, - out_trade_no: outTradeNo, - total_fee: String(amountCents), - spbill_create_ip: clientIp || "127.0.0.1", - notify_url: config.notifyUrl, - trade_type: "NATIVE" - }); - if (!data.code_url) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u672A\u8FD4\u56DE\u4E8C\u7EF4\u7801\u94FE\u63A5"); - } - return { codeUrl: data.code_url }; - }; - const createH5Order = async ({ outTradeNo, description, amountCents, clientIp }) => { - const appUrl = process.env.H5_PUBLIC_BASE_URL ?? "https://go.tkmind.cn"; - const data = await wechatV2Request(config, "/pay/unifiedorder", { - body: description, - out_trade_no: outTradeNo, - total_fee: String(amountCents), - spbill_create_ip: clientIp || "127.0.0.1", - notify_url: config.notifyUrl, - trade_type: "MWEB", - scene_info: JSON.stringify({ - h5_info: { - type: "Wap", - wap_url: appUrl, - wap_name: "TKMind" - } - }) - }); - if (!data.mweb_url) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u672A\u8FD4\u56DE H5 \u652F\u4ED8\u94FE\u63A5"); - } - return { h5Url: data.mweb_url }; - }; - const createJsapiOrder = async ({ outTradeNo, description, amountCents, clientIp, openid }) => { - if (!openid) { - throw new Error("JSAPI \u652F\u4ED8\u7F3A\u5C11 openid"); - } - const data = await wechatV2Request(config, "/pay/unifiedorder", { - body: description, - out_trade_no: outTradeNo, - total_fee: String(amountCents), - spbill_create_ip: clientIp || "127.0.0.1", - notify_url: config.notifyUrl, - trade_type: "JSAPI", - openid - }); - if (!data.prepay_id) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u672A\u8FD4\u56DE prepay_id"); - } - return { jsapiParams: buildV2JsapiParams(config, data.prepay_id) }; - }; - const verifyNotify = ({ headers: _headers, body }) => { - const bodyText = typeof body === "string" ? body : String(body ?? ""); - const fields = parseXmlFields(bodyText); - if (!fields.out_trade_no) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u56DE\u8C03\u7F3A\u5C11\u5546\u6237\u8BA2\u5355\u53F7"); - } - if (fields.return_code !== "SUCCESS") { - throw new Error(`\u5FAE\u4FE1\u652F\u4ED8\u56DE\u8C03\u5931\u8D25: ${fields.return_msg || fields.return_code}`); - } - if (!config.skipNotifyVerify) { - const sign = fields.sign; - if (!sign) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u56DE\u8C03\u7F3A\u5C11\u7B7E\u540D"); - } - const expected = signV2Params(fields, config.apiKey); - if (sign !== expected) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u56DE\u8C03\u9A8C\u7B7E\u5931\u8D25"); - } - } - return { transaction: normalizeV2Transaction(fields) }; - }; - return { - enabled: true, - apiVersion: "v2", - appId: config.appId, - createNativeOrder, - createH5Order, - createJsapiOrder, - verifyNotify - }; -} -function createV3Client(config) { - const createNativeOrder = async ({ outTradeNo, description, amountCents, clientIp }) => { - const data = await wechatV3Request(config, "POST", "/v3/pay/transactions/native", { - appid: config.appId, - mchid: config.mchId, - description, - out_trade_no: outTradeNo, - notify_url: config.notifyUrl, - amount: { total: amountCents, currency: "CNY" }, - scene_info: clientIp ? { payer_client_ip: clientIp } : void 0 - }); - return { codeUrl: data.code_url }; - }; - const createH5Order = async ({ outTradeNo, description, amountCents, clientIp }) => { - const data = await wechatV3Request(config, "POST", "/v3/pay/transactions/h5", { - appid: config.appId, - mchid: config.mchId, - description, - out_trade_no: outTradeNo, - notify_url: config.notifyUrl, - amount: { total: amountCents, currency: "CNY" }, - scene_info: { - payer_client_ip: clientIp || "127.0.0.1", - h5_info: { - type: "Wap", - app_name: "TKMind", - app_url: process.env.H5_PUBLIC_BASE_URL ?? "https://go.tkmind.cn" - } - } - }); - return { h5Url: data.h5_url }; - }; - const createJsapiOrder = async ({ outTradeNo, description, amountCents, clientIp, openid }) => { - if (!openid) { - throw new Error("JSAPI \u652F\u4ED8\u7F3A\u5C11 openid"); - } - const data = await wechatV3Request(config, "POST", "/v3/pay/transactions/jsapi", { - appid: config.appId, - mchid: config.mchId, - description, - out_trade_no: outTradeNo, - notify_url: config.notifyUrl, - amount: { total: amountCents, currency: "CNY" }, - payer: { openid }, - scene_info: clientIp ? { payer_client_ip: clientIp } : void 0 - }); - if (!data.prepay_id) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u672A\u8FD4\u56DE prepay_id"); - } - return { jsapiParams: buildV3JsapiParams(config, data.prepay_id) }; - }; - const verifyNotify = ({ headers, body }) => { - const timestamp = headers["wechatpay-timestamp"] ?? headers["Wechatpay-Timestamp"]; - const nonce = headers["wechatpay-nonce"] ?? headers["Wechatpay-Nonce"]; - const signature = headers["wechatpay-signature"] ?? headers["Wechatpay-Signature"]; - const serial = headers["wechatpay-serial"] ?? headers["Wechatpay-Serial"]; - if (!timestamp || !nonce || !signature) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u56DE\u8C03\u7F3A\u5C11\u7B7E\u540D\u5934"); - } - const bodyText = typeof body === "string" ? body : JSON.stringify(body); - const message = `${timestamp} -${nonce} -${bodyText} -`; - if (!config.skipNotifyVerify) { - if (!config.platformCert) { - throw new Error("\u672A\u914D\u7F6E\u5FAE\u4FE1\u652F\u4ED8\u5E73\u53F0\u8BC1\u4E66\uFF0C\u65E0\u6CD5\u9A8C\u7B7E"); - } - const verified = crypto27.createVerify("RSA-SHA256").update(message).verify(config.platformCert, signature, "base64"); - if (!verified) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u56DE\u8C03\u9A8C\u7B7E\u5931\u8D25"); - } - } - const payload = typeof body === "string" ? JSON.parse(body) : body; - const resource = payload?.resource; - if (!resource?.ciphertext || !resource?.nonce) { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u56DE\u8C03\u6570\u636E\u4E0D\u5B8C\u6574"); - } - const cipherBuf = Buffer.from(resource.ciphertext, "base64"); - const authTag = cipherBuf.subarray(cipherBuf.length - 16); - const dataBuf = cipherBuf.subarray(0, cipherBuf.length - 16); - const decipher = crypto27.createDecipheriv( - "aes-256-gcm", - Buffer.from(config.apiV3Key, "utf8"), - Buffer.from(resource.nonce, "utf8") - ); - decipher.setAuthTag(authTag); - if (resource.associated_data) { - decipher.setAAD(Buffer.from(resource.associated_data, "utf8")); - } - const plain = Buffer.concat([decipher.update(dataBuf), decipher.final()]).toString("utf8"); - const transaction = JSON.parse(plain); - return { serial, transaction }; - }; - return { - enabled: true, - apiVersion: "v3", - appId: config.appId, - createNativeOrder, - createH5Order, - createJsapiOrder, - verifyNotify - }; -} -function createWechatPayClient(config) { - if (!config?.enabled) { - return { - enabled: false, - apiVersion: config?.apiVersion ?? null, - appId: config?.appId ?? null, - async createNativeOrder() { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u672A\u914D\u7F6E"); - }, - async createH5Order() { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u672A\u914D\u7F6E"); - }, - async createJsapiOrder() { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u672A\u914D\u7F6E"); - }, - verifyNotify() { - throw new Error("\u5FAE\u4FE1\u652F\u4ED8\u672A\u914D\u7F6E"); - } - }; - } - if (config.apiVersion === "v2") { - return createV2Client(config); - } - return createV3Client(config); -} -var WECHAT_NOTIFY_SUCCESS_V2 = ""; - -// wechat-oauth.mjs -import crypto28 from "node:crypto"; -import { fetch as fetch3 } from "undici"; -var OAUTH_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize"; -var OAUTH_QRCONNECT_URL = "https://open.weixin.qq.com/connect/qrconnect"; -var OAUTH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"; -var OAUTH_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo"; -var STATE_TTL_MS = 10 * 60 * 1e3; -var VALID_INTENTS = /* @__PURE__ */ new Set(["login", "register", "bind"]); -var VALID_AUTH_MODES = /* @__PURE__ */ new Set(["mp", "open", "scan"]); -function isPollScanAuthMode(authMode) { - return authMode === "open" || authMode === "scan"; -} -function resolveCallbackUrl(env = process.env) { - const explicit = env.H5_WECHAT_OAUTH_CALLBACK_URL?.trim() ?? ""; - if (explicit) return explicit; - const base = env.H5_PUBLIC_BASE_URL?.trim() ?? ""; - if (base) return `${base.replace(/\/$/, "")}/auth/wechat/callback`; - return ""; -} -function isWechatUserAgent(userAgent = "") { - const ua = userAgent || ""; - return /MicroMessenger/i.test(ua) || /WindowsWechat/i.test(ua); -} -function loadWechatOAuthConfig(env = process.env) { - const enabledFlag = env.H5_WECHAT_OAUTH_ENABLED === "1"; - const appId = env.H5_WECHAT_APP_ID?.trim() ?? env.H5_WECHAT_OAUTH_APP_ID?.trim() ?? ""; - const appSecret = env.H5_WECHAT_APP_SECRET?.trim() ?? env.H5_WECHAT_OAUTH_APP_SECRET?.trim() ?? ""; - const openAppId = env.H5_WECHAT_OPEN_APP_ID?.trim() ?? ""; - const openAppSecret = env.H5_WECHAT_OPEN_APP_SECRET?.trim() ?? ""; - const scope = env.H5_WECHAT_OAUTH_SCOPE?.trim() || "snsapi_userinfo"; - const callbackUrl = resolveCallbackUrl(env); - const configured = Boolean(appId && appSecret && callbackUrl); - const scanConfigured = Boolean(openAppId && openAppSecret && callbackUrl); - return { - enabled: enabledFlag && configured, - appId, - appSecret, - openAppId, - openAppSecret, - scanEnabled: enabledFlag && scanConfigured, - scope, - callbackUrl - }; -} -function sanitizeWechatReturnTo(returnTo, req) { - if (!returnTo || typeof returnTo !== "string") return "/"; - const host = req.get("host"); - try { - const url = new URL(returnTo, `http://${host}`); - if (url.host !== host) return "/"; - if (!url.pathname.startsWith("/")) return "/"; - return `${url.pathname}${url.search}${url.hash}`; - } catch { - if (returnTo.startsWith("/") && !returnTo.startsWith("//")) return returnTo; - return "/"; - } -} -function normalizeIntent(intent) { - const value = typeof intent === "string" ? intent.trim().toLowerCase() : "login"; - return VALID_INTENTS.has(value) ? value : "login"; -} -function normalizeAuthMode(authMode) { - const value = typeof authMode === "string" ? authMode.trim().toLowerCase() : "mp"; - return VALID_AUTH_MODES.has(value) ? value : "mp"; -} -function buildAuthorizeUrl(config, { state, authMode = "mp" }) { - const isOpen = authMode === "open"; - const useMp = authMode === "mp" || authMode === "scan"; - const params = new URLSearchParams({ - appid: isOpen ? config.openAppId : config.appId, - redirect_uri: config.callbackUrl, - response_type: "code", - scope: isOpen ? "snsapi_login" : config.scope, - state - }); - const base = isOpen ? OAUTH_QRCONNECT_URL : OAUTH_AUTHORIZE_URL; - return `${base}?${params.toString()}#wechat_redirect`; -} -async function exchangeCode(config, code, authMode = "mp") { - const isOpen = authMode === "open"; - const params = new URLSearchParams({ - appid: isOpen ? config.openAppId : config.appId, - secret: isOpen ? config.openAppSecret : config.appSecret, - code, - grant_type: "authorization_code" - }); - const res = await fetch3(`${OAUTH_ACCESS_TOKEN_URL}?${params.toString()}`); - const data = await res.json(); - if (data.errcode) { - throw new Error(data.errmsg || `\u5FAE\u4FE1\u6388\u6743\u5931\u8D25 (${data.errcode})`); - } - if (!data.openid) { - throw new Error("\u5FAE\u4FE1\u6388\u6743\u672A\u8FD4\u56DE openid"); - } - return { - accessToken: data.access_token, - openid: data.openid, - unionid: data.unionid ?? null, - refreshToken: data.refresh_token ?? null, - appId: isOpen ? config.openAppId : config.appId - }; -} -async function fetchUserInfo(accessToken, openid) { - const params = new URLSearchParams({ - access_token: accessToken, - openid, - lang: "zh_CN" - }); - const res = await fetch3(`${OAUTH_USERINFO_URL}?${params.toString()}`); - const data = await res.json(); - if (data.errcode) { - throw new Error(data.errmsg || `\u83B7\u53D6\u5FAE\u4FE1\u7528\u6237\u4FE1\u606F\u5931\u8D25 (${data.errcode})`); - } - return { - nickname: typeof data.nickname === "string" ? data.nickname.slice(0, 128) : null, - avatarUrl: typeof data.headimgurl === "string" ? data.headimgurl.slice(0, 512) : null, - unionid: data.unionid ?? null - }; -} -function createWechatOAuthService(pool2, config, { userAuth: userAuth2 } = {}) { - if (!config?.enabled) { - return { - enabled: false, - publicConfig() { - return { enabled: false, inWechat: false, scanEnabled: false }; - } - }; - } - const pruneExpiredStates = async (now = Date.now()) => { - if (!pool2) return; - await pool2.query(`DELETE FROM h5_wechat_oauth_states WHERE expires_at <= ?`, [now]); - }; - const createState = async ({ - returnTo = "/", - utmSource = null, - utmMedium = null, - utmCampaign = null, - intent = "login", - bindUserId = null, - authMode = "mp", - now = Date.now() - } = {}) => { - const state = crypto28.randomBytes(24).toString("base64url"); - if (pool2) { - await pruneExpiredStates(now); - await pool2.query( - `INSERT INTO h5_wechat_oauth_states - (state, return_to, utm_source, utm_medium, utm_campaign, intent, bind_user_id, - auth_mode, status, expires_at, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`, - [ - state, - returnTo, - utmSource, - utmMedium, - utmCampaign, - normalizeIntent(intent), - bindUserId, - normalizeAuthMode(authMode), - now + STATE_TTL_MS, - now - ] - ); - } - return state; - }; - const loadState = async (state, now = Date.now()) => { - if (!state || !pool2) return null; - const [rows] = await pool2.query( - `SELECT state, return_to, utm_source, utm_medium, utm_campaign, intent, bind_user_id, - auth_mode, status, result_kind, result_token, result_message, expires_at - FROM h5_wechat_oauth_states - WHERE state = ? - LIMIT 1`, - [state] - ); - const row = rows[0]; - if (!row || Number(row.expires_at) <= now) return null; - return row; - }; - const consumeState = async (state, now = Date.now()) => { - const row = await loadState(state, now); - if (!row) return null; - await pool2.query(`DELETE FROM h5_wechat_oauth_states WHERE state = ?`, [state]); - return { - returnTo: row.return_to || "/", - utmSource: row.utm_source, - utmMedium: row.utm_medium, - utmCampaign: row.utm_campaign, - intent: row.intent || "login", - bindUserId: row.bind_user_id, - authMode: row.auth_mode || "mp" - }; - }; - const markStateComplete = async (state, { resultKind, resultToken = null, resultMessage = null }, now = Date.now()) => { - await pool2.query( - `UPDATE h5_wechat_oauth_states - SET status = 'done', result_kind = ?, result_token = ?, result_message = ?, expires_at = ? - WHERE state = ?`, - [resultKind, resultToken, resultMessage, now + STATE_TTL_MS, state] - ); - }; - const buildAuthorizeRedirect = async (req, { bindUserId = null } = {}) => { - const returnTo = sanitizeWechatReturnTo(req.query?.return_to, req); - const intent = normalizeIntent(req.query?.intent); - const authMode = normalizeAuthMode(req.query?.auth_mode); - if (authMode === "open" && !config.scanEnabled) { - throw new Error("\u5FAE\u4FE1\u626B\u7801\u767B\u5F55\u672A\u914D\u7F6E"); - } - const state = await createState({ - returnTo, - utmSource: typeof req.query?.utm_source === "string" ? req.query.utm_source : null, - utmMedium: typeof req.query?.utm_medium === "string" ? req.query.utm_medium : null, - utmCampaign: typeof req.query?.utm_campaign === "string" ? req.query.utm_campaign : null, - intent, - bindUserId, - authMode - }); - return buildAuthorizeUrl(config, { state, authMode }); - }; - const startScanLogin = async (req) => { - const returnTo = sanitizeWechatReturnTo(req.query?.return_to, req); - const utmSource = typeof req.query?.utm_source === "string" ? req.query.utm_source : null; - const utmMedium = typeof req.query?.utm_medium === "string" ? req.query.utm_medium : null; - const utmCampaign = typeof req.query?.utm_campaign === "string" ? req.query.utm_campaign : null; - const common = { - returnTo, - utmSource, - utmMedium, - utmCampaign, - intent: "login" - }; - if (config.scanEnabled) { - const state2 = await createState({ ...common, authMode: "open" }); - return { - mode: "open", - state: state2, - openAppId: config.openAppId, - redirectUri: config.callbackUrl, - expiresInMs: STATE_TTL_MS - }; - } - const state = await createState({ ...common, authMode: "scan" }); - return { - mode: "mp", - state, - qrUrl: buildAuthorizeUrl(config, { state, authMode: "scan" }), - expiresInMs: STATE_TTL_MS - }; - }; - const pollScanLogin = async (state, now = Date.now()) => { - const row = await loadState(state, now); - if (!row) return { status: "expired" }; - if (row.status !== "done") return { status: "pending" }; - await pool2.query(`DELETE FROM h5_wechat_oauth_states WHERE state = ?`, [state]); - if (row.result_kind === "error") { - return { status: "error", message: row.result_message || "\u5FAE\u4FE1\u767B\u5F55\u5931\u8D25" }; - } - if (row.result_kind === "binding_gate") { - return { status: "binding_gate", pendingToken: row.result_token }; - } - if (row.result_kind === "login") { - return { status: "complete", token: row.result_token }; - } - return { status: "error", message: "\u5FAE\u4FE1\u767B\u5F55\u7ED3\u679C\u65E0\u6548" }; - }; - const handleCallback = async ({ code, state, ip = "unknown" }) => { - if (!code) { - throw new Error("\u7F3A\u5C11\u5FAE\u4FE1\u6388\u6743 code"); - } - const stateRow = await loadState(state); - if (!stateRow) { - throw new Error("\u6388\u6743\u72B6\u6001\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - } - const authMode = stateRow.auth_mode || "mp"; - const tokenInfo = await exchangeCode(config, code, authMode); - let nickname = null; - let avatarUrl = null; - let unionid = tokenInfo.unionid; - if ((authMode === "mp" || authMode === "scan") && config.scope === "snsapi_userinfo") { - const profile = await fetchUserInfo(tokenInfo.accessToken, tokenInfo.openid); - nickname = profile.nickname; - avatarUrl = profile.avatarUrl; - unionid = profile.unionid ?? unionid; - } - if (authMode === "open") { - try { - const profile = await fetchUserInfo(tokenInfo.accessToken, tokenInfo.openid); - nickname = profile.nickname; - avatarUrl = profile.avatarUrl; - unionid = profile.unionid ?? unionid; - } catch { - } - } - if (!userAuth2?.resolveWechatAuth) { - throw new Error("\u7528\u6237\u7CFB\u7EDF\u672A\u5C31\u7EEA"); - } - const result = await userAuth2.resolveWechatAuth({ - appId: tokenInfo.appId, - openid: tokenInfo.openid, - unionid, - nickname, - avatarUrl, - intent: stateRow.intent || "login", - bindUserId: stateRow.bind_user_id, - returnTo: stateRow.return_to || "/", - utmSource: stateRow.utm_source, - utmMedium: stateRow.utm_medium, - utmCampaign: stateRow.utm_campaign, - ip - }); - if (!result.ok) { - if (isPollScanAuthMode(authMode)) { - await markStateComplete(state, { - resultKind: "error", - resultMessage: result.message || "\u5FAE\u4FE1\u767B\u5F55\u5931\u8D25" - }); - return { - authMode, - action: "poll_error", - message: result.message || "\u5FAE\u4FE1\u767B\u5F55\u5931\u8D25" - }; - } - throw new Error(result.message || "\u5FAE\u4FE1\u767B\u5F55\u5931\u8D25"); - } - if (result.action === "binding_gate") { - if (isPollScanAuthMode(authMode)) { - await markStateComplete(state, { - resultKind: "binding_gate", - resultToken: result.pendingToken - }); - return { - authMode, - action: "binding_gate", - pendingToken: result.pendingToken, - wechatProfile: result.wechatProfile, - returnTo: result.returnTo - }; - } - return { - authMode, - action: "binding_gate", - pendingToken: result.pendingToken, - wechatProfile: result.wechatProfile, - returnTo: result.returnTo, - utmSource: result.utmSource, - utmMedium: result.utmMedium, - utmCampaign: result.utmCampaign - }; - } - if (isPollScanAuthMode(authMode)) { - await markStateComplete(state, { - resultKind: "login", - resultToken: result.token - }); - return { - authMode, - action: "login", - token: result.token, - user: result.user, - isNewUser: result.isNewUser, - returnTo: stateRow.return_to || "/", - utmSource: stateRow.utm_source, - utmMedium: stateRow.utm_medium, - utmCampaign: stateRow.utm_campaign - }; - } - await pool2.query(`DELETE FROM h5_wechat_oauth_states WHERE state = ?`, [state]); - return { - authMode, - action: "login", - token: result.token, - user: result.user, - isNewUser: result.isNewUser, - bound: result.bound, - returnTo: stateRow.return_to || "/", - utmSource: stateRow.utm_source, - utmMedium: stateRow.utm_medium, - utmCampaign: stateRow.utm_campaign - }; - }; - return { - enabled: true, - publicConfig(req) { - const inWechat = req ? isWechatUserAgent(req.get?.("user-agent") || "") : false; - return { - enabled: true, - inWechat, - scanEnabled: true, - openScanEnabled: Boolean(config.scanEnabled), - appId: config.appId, - openAppId: config.scanEnabled ? config.openAppId : void 0, - oauthCallbackUrl: config.scanEnabled ? config.callbackUrl : void 0 - }; - }, - buildAuthorizeRedirect, - startScanLogin, - pollScanLogin, - handleCallback - }; -} - -// wechat-mp.mjs -import crypto30 from "node:crypto"; -import fs31 from "node:fs"; -import path33 from "node:path"; -import { fetch as undiciFetch7 } from "undici"; - -// schedule-intent.mjs -function normalizeText2(text) { - return String(text ?? "").replace(/\s+/g, "").trim(); -} -function chineseHourToNumber(value) { - const raw = String(value ?? "").trim(); - if (/^\d{1,2}$/.test(raw)) return Number(raw); - const map = { - \u96F6: 0, - \u4E00: 1, - \u4E8C: 2, - \u4E24: 2, - \u4E09: 3, - \u56DB: 4, - \u4E94: 5, - \u516D: 6, - \u4E03: 7, - \u516B: 8, - \u4E5D: 9, - \u5341: 10 - }; - if (raw === "\u5341") return 10; - if (raw.startsWith("\u5341")) return 10 + (map[raw.slice(1)] ?? 0); - if (raw.endsWith("\u5341")) return (map[raw[0]] ?? 0) * 10; - if (raw.includes("\u5341")) { - const [tens, ones] = raw.split("\u5341"); - return (map[tens] ?? 1) * 10 + (map[ones] ?? 0); - } - return map[raw] ?? null; -} -function parseHourMinute(text) { - const match = text.match(/(?:早上|上午|清晨|每天早上|每天上午)?([0-9]{1,2}|[零一二两三四五六七八九十]{1,3})(?:点|:|:)(半|[0-9]{1,2}分?)?/); - if (!match) return null; - const hour = chineseHourToNumber(match[1]); - if (hour === null || hour < 0 || hour > 23) return null; - let minute = 0; - if (match[2] === "\u534A") minute = 30; - else if (match[2]) minute = Number(String(match[2]).replace("\u5206", "")); - if (!Number.isFinite(minute) || minute < 0 || minute > 59) return null; - return { hour, minute }; -} -function extractTodoTitle(text) { - const original = String(text ?? "").trim(); - if (!original) return null; - const quoted = original.match(/[「『【((“"']([^」』】))”"']{1,80})[」』】))”"']/); - if (quoted?.[1]) { - const title = quoted[1].trim(); - if (title) return title; - } - const trailingCommand = original.match( - /^(.{1,80}?)(?:帮我)?(?:设|设置|设下|记)(?:一个|下一个)?(?:待办|代办|带办|代拜)(?:吧|一下)?$/u - ); - if (trailingCommand?.[1]) { - const title = trailingCommand[1].trim(); - if (title) return title; - } - const cleaned = original.replace( - /^.*?(?:不用提醒|先记一下|帮我记一下|帮我记|记一下|添加待办|添加任务|记个待办|记个任务|设置一个待办|设置一个代办|设置一个带办|设置一个代拜|设置下一个代拜|设下一个代拜|帮我设置一个待办|帮我设置一个代办|帮我设置一个带办|帮我设置一个代拜|待办|代办|带办|代拜|任务)(?:[::,,、\s]+)?/u, - "" - ).trim(); - return cleaned || null; -} -function parseScheduleIntent(text) { - const compact = normalizeText2(text); - if (!compact) return { action: "none" }; - const wantsDaily = /每天|每日|天天/.test(compact); - const wantsTodoDigest = /(待办|todo|任务).*(记录|列表|清单|安排|摘要)|一天的待办/.test(compact); - const wantsSend = /发|发送|推送|提醒|通知|给我/.test(compact); - if (wantsDaily && wantsTodoDigest && wantsSend) { - const time = parseHourMinute(compact); - if (!time) { - return { - action: "create_daily_todo_digest", - needsClarification: ["digest_time"] - }; - } - return { - action: "create_daily_todo_digest", - hour: time.hour, - minute: time.minute - }; - } - const wantsBalanceAlert = /(余额|钱包|账户).*(不足|低于|提醒|预警|通知)|余额不足|低余额|余额预警/.test(compact); - if (wantsBalanceAlert) { - const thresholdMatch = compact.match(/(?:低于|少于|不足|小于)(\d{1,6})(?:元|块|rmb|人民币|cny)?/i); - const thresholdYuan = thresholdMatch ? Number(thresholdMatch[1]) : null; - const thresholdCents = thresholdYuan == null ? null : Math.max(0, Math.round(thresholdYuan * 100)); - return thresholdCents == null ? { action: "create_balance_alert", needsClarification: ["threshold"] } : { action: "create_balance_alert", thresholdCents }; - } - if (/看看|查看|看下|列出/.test(compact) && /(待办|日程|行程|计划|任务)/.test(compact)) { - return { action: "query_schedule" }; - } - const wantsTodoRecord = /(不用提醒|先记一下|帮我记一下|帮我记|记一下|添加待办|添加任务|记个待办|记个任务|设置一个待办|设置一个代办|设置一个带办|设置一个代拜|设置下一个代拜|设下一个代拜|帮我设置一个待办|帮我设置一个代办|帮我设置一个带办|帮我设置一个代拜|待办|代办|带办|代拜|任务)/.test( - compact - ); - if (wantsTodoRecord) { - const explicitNoReminder = /(不用提醒|不提醒|先记一下)/.test(compact); - const hasScheduleTime = /(提醒|闹钟|叫我|今天|今晚|明天|后天|早上|上午|中午|下午|晚上|[0-9零一二两三四五六七八九十]{1,3}(点|:|:))/.test( - compact - ); - if (!explicitNoReminder && hasScheduleTime) { - return { action: "schedule_agent" }; - } - const title = extractTodoTitle(text); - if (!title) { - return { action: "create_todo", needsClarification: ["todo_title"] }; - } - return { action: "create_todo", title }; - } - return { action: "none" }; -} -function isScheduleIntent(intent) { - return intent?.action && intent.action !== "none"; -} -function shouldUseScheduleAssistant(text) { - const compact = normalizeText2(text); - if (!compact) return false; - if (isScheduleIntent(parseScheduleIntent(text))) return true; - const scheduleKeywords = /(提醒|待办|代办|带办|代拜|日程|行程|安排|计划|闹钟)/; - const timeKeywords = /(今天|今晚|明天|后天|早上|上午|中午|下午|晚上|\d{1,2}[点::])/; - return scheduleKeywords.test(compact) && timeKeywords.test(compact); -} - -// chat-skills.mjs -var PUBLISH_SKILL_NAME2 = "static-page-publish"; -var WEB_INTENT_PATTERNS = [ - /(?:今天|今日|最新|热点|热搜|新闻|头条|头条新闻|最新消息|发生了什么|最近发生)/u, - /(?:latest|breaking)\s+news/i, - /what(?:'s| is)?\s+happening/i -]; -var WEB_NEWS_INTENT_PATTERNS = [ - /(?:今天|今日|最新|热点|热搜|新闻|头条|头条新闻|热榜|热议|舆情|突发)/u, - /(?:today|latest|breaking|trending)\s+(?:news|stories|headlines|topics)/i, - /(?:what(?:'s| is)?\s+the\s+latest|what(?:'s| is)?\s+trending)/i -]; -var PAGE_GENERATION_INTENT_PATTERNS = [ - /(?:生成|做|制作|创建|设计|写|出)(?:一个|一份|个)?(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u, - /(?:帮我|给我).*(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u, - /(?:publish|create|generate|make|build|design).*(?:html|web\s?page|landing\s?page|h5)/i -]; -var CHAT_SKILL_DEFINITIONS = [ - { - id: "web-search", - label: "\u67E5\u8D44\u6599", - icon: "web", - skillName: "web", - requiresSkill: "web", - prefillOnly: true, - promptKey: "web" - }, - { - id: "code-search", - label: "\u641C\u4EE3\u7801", - icon: "search", - skillName: "search", - requiresSkill: "search", - prefillOnly: true, - promptKey: "search" - }, - { - id: "form-collect", - label: "\u8868\u5355\u6536\u96C6", - icon: "form", - skillName: "form-builder", - requiresSkill: "form-builder", - prefillOnly: true, - promptKey: "form-builder" - }, - { - id: "table-view", - label: "\u6570\u636E\u8868\u683C", - icon: "table", - skillName: "table-viewer", - requiresSkill: "table-viewer", - prefillOnly: true, - promptKey: "table-viewer" - }, - { - id: "product-campaign-page", - label: "\u5546\u54C1\u9875", - icon: "page", - skillName: "product-campaign-page", - requiresSkill: "product-campaign-page", - promptKey: "product-campaign-page" - }, - { - id: "summarize", - label: "\u603B\u7ED3\u5185\u5BB9", - icon: "summary", - prefillOnly: true, - promptKey: "summarize" - }, - { - id: "analyze", - label: "\u6DF1\u5EA6\u5206\u6790", - icon: "analyze", - prefillOnly: true, - promptKey: "analyze" - }, - { - id: "generate-page", - label: "\u751F\u6210\u9875\u9762", - icon: "page", - skillName: PUBLISH_SKILL_NAME2, - requiresPublish: true, - promptKey: "generate-page" - } -]; -function buildChatSkillPrompt(promptKey, skillName) { - switch (promptKey) { - case "web": - return `\u8BF7\u4F7F\u7528 ${skillName ?? "web"} \u6280\u80FD\uFF1A\u5E2E\u6211\u641C\u7D22\u5E76\u67E5\u9605\u76F8\u5173\u8D44\u6599\uFF08\u4F18\u5148\u5B98\u65B9\u6587\u6863\uFF09\uFF0C\u5E76\u7ED9\u51FA\u4E2D\u6587\u6458\u8981\u4E0E\u6765\u6E90\u94FE\u63A5\u3002\u6211\u7684\u95EE\u9898\u662F\uFF1A`; - case "search": - return `\u8BF7\u4F7F\u7528 ${skillName ?? "search"} \u6280\u80FD\uFF1A\u5E2E\u6211\u5728\u5DE5\u4F5C\u533A\u4E2D\u67E5\u627E\u4EE3\u7801\u6216\u6587\u4EF6\u3002\u6211\u8981\u627E\u7684\u662F\uFF1A`; - case "form-builder": - return `\u8BF7\u4F7F\u7528 ${skillName ?? "form-builder"} \u6280\u80FD\uFF1A\u8BF7\u7528\u4EA4\u4E92\u5F0F\u8868\u5355\u6536\u96C6\u4EE5\u4E0B\u573A\u666F\u6240\u9700\u7684\u7ED3\u6784\u5316\u5B57\u6BB5\uFF08\u5B57\u6BB5\u4E0D\u8D85\u8FC7 8 \u4E2A\uFF09\uFF1A`; - case "table-viewer": - return `\u8BF7\u4F7F\u7528 ${skillName ?? "table-viewer"} \u6280\u80FD\uFF1A\u8BF7\u628A\u4EE5\u4E0B\u6570\u636E\u6574\u7406\u6210\u53EF\u6392\u5E8F\u3001\u53EF\u7B5B\u9009\u7684\u4EA4\u4E92\u5F0F\u8868\u683C\uFF1A`; - case "product-campaign-page": - return `\u8BF7\u4F7F\u7528 ${skillName ?? "product-campaign-page"} \u6280\u80FD\uFF1A\u5E2E\u6211\u5236\u4F5C\u4E00\u4E2A\u5546\u54C1\u5BA3\u4F20 / \u6D3B\u52A8\u9875\u3002\u8BF7\u5148\u8BE2\u95EE\u5546\u54C1\u94FE\u63A5\u3001\u9875\u9762\u4E3B\u9898\u3001\u662F\u5426\u6709\u5546\u54C1\u56FE\u7247\u6216\u54C1\u724C\u7D20\u6750\uFF1B\u4FE1\u606F\u8DB3\u591F\u540E\uFF0C\u751F\u6210\u5305\u542B\u8D2D\u4E70\u8DF3\u8F6C\u6309\u94AE\u7684\u9875\u9762\u65B9\u6848\u3002\u5982\u679C\u6211\u786E\u8BA4\u8981\u53D1\u5E03\u6210 H5 \u9875\u9762\uFF0C\u8BF7\u7EE7\u7EED\u4F7F\u7528 static-page-publish \u6280\u80FD\u751F\u6210\u53EF\u8BBF\u95EE\u94FE\u63A5\u3002`; - case "summarize": - return "\u8BF7\u603B\u7ED3\u4EE5\u4E0B\u5185\u5BB9\uFF0C\u63D0\u70BC\u6838\u5FC3\u7ED3\u8BBA\u3001\u91CD\u70B9\u4FE1\u606F\u548C\u53EF\u6267\u884C\u5EFA\u8BAE\uFF08\u6761\u7406\u6E05\u6670\u3001\u4E2D\u6587\u8F93\u51FA\uFF09\uFF1A"; - case "analyze": - return "\u8BF7\u6DF1\u5165\u5206\u6790\u4EE5\u4E0B\u5185\u5BB9\uFF0C\u7ED9\u51FA\u7ED3\u6784\u5316\u89E3\u8BFB\uFF1A\u80CC\u666F\u3001\u5173\u952E\u53D1\u73B0\u3001\u98CE\u9669\u6216\u673A\u4F1A\u3001\u4EE5\u53CA\u5EFA\u8BAE\u4E0B\u4E00\u6B65\uFF1A"; - case "generate-page": - return `\u8BF7\u4F7F\u7528 ${skillName ?? PUBLISH_SKILL_NAME2} \u6280\u80FD\uFF1A\u5728\u6211\u7684\u4E13\u5C5E MindSpace \u53D1\u5E03\u76EE\u5F55\u751F\u6210\u9759\u6001 HTML \u9875\u9762\uFF0C\u5E76\u7ED9\u51FA\u53EF\u516C\u7F51\u8BBF\u95EE\u7684\u5B8C\u6574\u94FE\u63A5\u3002\u9ED8\u8BA4\u53EA\u751F\u6210 HTML\uFF1A\u5FC5\u987B\u5148 load_skill\uFF0C\u518D\u7528 write_file/edit_file \u5199\u5165 public/\u9875\u9762.html\uFF1B\u5B8C\u6210\u540E\u76F4\u63A5\u8FD4\u56DE Markdown \u53EF\u70B9\u51FB\u516C\u7F51\u94FE\u63A5 \`[\u9875\u9762\u6807\u9898](URL)\`\u3002\u7981\u6B62\u53EA\u7ED9\u672C\u5730\u8DEF\u5F84\uFF08\u5982 hello/index.html\uFF09\uFF0C\u7981\u6B62\u8BE2\u95EE\u662F\u5426\u8FD8\u8981\u53D1\u5E03\uFF0C\u9664\u975E\u5199\u6587\u4EF6\u5DE5\u5177\u5B9E\u9645\u5931\u8D25\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42 Word / docx \u4E0B\u8F7D\u65F6\uFF0C\u624D\u5148 \`load_skill\` \u2192 \`docx-generate\`\uFF0C\u751F\u6210 \`public/*.docx\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u518D\u5199 HTML \u5E76\u7528\u540C\u76EE\u5F55\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u957F\u56FE\u4E0B\u8F7D\u65F6\uFF0C\u624D\u5148 \`load_skill\` \u2192 \`long-image-download\`\uFF0C\u8C03\u7528 \`generate_long_image\` \u751F\u6210 \`public/*.long.png\` \u5E76\u786E\u8BA4\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u518D\u7ED9\u957F\u56FE\u9884\u89C8\u94FE\u63A5\u548C \`?download=long-image\` \u4E0B\u8F7D\u94FE\u63A5\u3002\u4E0D\u8981\u5728\u6CA1\u6709\u660E\u786E\u9700\u6C42\u65F6\u5F3A\u5236\u751F\u6210\u4F34\u751F\u6587\u4EF6\u3002`; - default: - return ""; - } -} -function buildWebNewsSkillPrompt(skillName) { - return `\u8BF7\u4F7F\u7528 ${skillName ?? "web"} \u6280\u80FD\uFF1A\u5148\u641C\u7D22\u4ECA\u5929/\u6700\u65B0\u76F8\u5173\u7684\u65B0\u95FB\u4E0E\u70ED\u70B9\uFF0C\u4F18\u5148\u4E00\u624B\u6765\u6E90\u548C\u6743\u5A01\u5A92\u4F53\uFF0C\u6574\u7406 3-5 \u6761\u6700\u76F8\u5173\u7ED3\u679C\uFF0C\u6309\u65F6\u95F4\u6216\u70ED\u5EA6\u6392\u5E8F\uFF1B\u7136\u540E\u7ED9\u51FA\u4E2D\u6587\u6458\u8981\u3001\u5173\u952E\u4FE1\u606F\u3001\u4E8B\u4EF6\u80CC\u666F\u548C\u6765\u6E90\u94FE\u63A5\u3002\u6211\u7684\u95EE\u9898\u662F\uFF1A`; -} -function buildAutoChatSkillPrefix(text, grantedSkills = []) { - const trimmed = String(text ?? "").trim(); - if (!trimmed) return ""; - if (grantedSkills.includes(PUBLISH_SKILL_NAME2) && PAGE_GENERATION_INTENT_PATTERNS.some((pattern) => pattern.test(trimmed))) { - return buildChatSkillPrompt("generate-page", PUBLISH_SKILL_NAME2); - } - if (!grantedSkills.includes("web")) return ""; - if (WEB_NEWS_INTENT_PATTERNS.some((pattern) => pattern.test(trimmed))) { - return buildWebNewsSkillPrompt("web"); - } - if (!WEB_INTENT_PATTERNS.some((pattern) => pattern.test(trimmed))) return ""; - return buildChatSkillPrompt("web", "web"); -} -function stripKnownChatSkillPrompt(text) { - let next = String(text ?? "").trimStart(); - const candidates = []; - for (const definition of CHAT_SKILL_DEFINITIONS) { - if (!definition.promptKey) continue; - candidates.push(buildChatSkillPrompt(definition.promptKey, definition.skillName)); - } - candidates.push(buildWebNewsSkillPrompt("web")); - candidates.sort((a, b) => b.length - a.length); - for (const prompt of candidates) { - if (prompt && next.startsWith(prompt)) { - next = next.slice(prompt.length); - break; - } - } - return next.trim(); -} - -// wechat-media.mjs -import crypto29 from "node:crypto"; -import fs30 from "node:fs"; -import path32 from "node:path"; -import { fileURLToPath as fileURLToPath5 } from "node:url"; -import { fetch as undiciFetch6 } from "undici"; -var __dirname4 = path32.dirname(fileURLToPath5(import.meta.url)); -var DEFAULT_WECHAT_MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/media/get"; -var DEFAULT_MAX_IMAGE_BYTES = 10 * 1024 * 1024; -var ALLOWED_IMAGE_MIME_TYPES = /* @__PURE__ */ new Map([ - ["image/jpeg", "jpg"], - ["image/png", "png"], - ["image/webp", "webp"], - ["image/gif", "gif"] -]); -function resolveImageExtension(contentType = "", fallbackUrl = "") { - const normalized = String(contentType ?? "").split(";")[0].trim().toLowerCase(); - if (ALLOWED_IMAGE_MIME_TYPES.has(normalized)) { - return { - mimeType: normalized, - extension: ALLOWED_IMAGE_MIME_TYPES.get(normalized) - }; - } - const ext = path32.extname(String(fallbackUrl ?? "").split("?")[0]).replace(/^\./, "").toLowerCase(); - if (ext === "jpg" || ext === "jpeg") return { mimeType: "image/jpeg", extension: "jpg" }; - if (ext === "png") return { mimeType: "image/png", extension: "png" }; - if (ext === "webp") return { mimeType: "image/webp", extension: "webp" }; - if (ext === "gif") return { mimeType: "image/gif", extension: "gif" }; - return null; -} -function ensureImageWithinLimit(buffer, maxBytes) { - if (!Buffer.isBuffer(buffer) || buffer.length === 0) { - throw new Error("\u5FAE\u4FE1\u56FE\u7247\u5185\u5BB9\u4E3A\u7A7A"); - } - if (buffer.length > maxBytes) { - throw new Error(`\u56FE\u7247\u8D85\u8FC7\u5927\u5C0F\u9650\u5236\uFF08${maxBytes} bytes\uFF09`); - } -} -async function downloadTemporaryMedia(accessToken, mediaId, { wechatFetch = undiciFetch6 } = {}) { - if (!accessToken) throw new Error("\u7F3A\u5C11\u5FAE\u4FE1 access_token"); - if (!mediaId) throw new Error("\u7F3A\u5C11\u5FAE\u4FE1 mediaId"); - const url = new URL(DEFAULT_WECHAT_MEDIA_URL); - url.searchParams.set("access_token", accessToken); - url.searchParams.set("media_id", mediaId); - const response = await wechatFetch(url.toString(), { - method: "GET", - headers: { Accept: "*/*" } - }); - if (!response.ok) { - const text = await response.text().catch(() => ""); - throw new Error(text || `\u5FAE\u4FE1\u4E34\u65F6\u7D20\u6750\u4E0B\u8F7D\u5931\u8D25 (${response.status})`); - } - const contentType = response.headers.get("content-type") || ""; - const buffer = Buffer.from(await response.arrayBuffer()); - return { buffer, contentType }; -} -function buildWechatImagePublicUrl({ - publicBaseUrl, - publishKey, - filename, - publicBasePath = `${PUBLIC_ZONE_DIR}/wechat-mp` -}) { - if (!publicBaseUrl) throw new Error("\u7F3A\u5C11\u56FE\u7247\u516C\u7F51\u57FA\u7840\u5730\u5740"); - if (!publishKey) throw new Error("\u7F3A\u5C11\u7528\u6237 publish key"); - if (!filename) throw new Error("\u7F3A\u5C11\u56FE\u7247\u6587\u4EF6\u540D"); - return buildPublicUrl(publicBaseUrl, publishKey, `${publicBasePath}/${filename}`); -} -async function persistWechatImage({ - userId, - appId, - openid, - msgId, - mediaId, - picUrl, - publicBaseUrl, - maxImageBytes = DEFAULT_MAX_IMAGE_BYTES -}, { - wechatFetch = undiciFetch6, - accessToken, - h5Root = __dirname4 -} = {}) { - if (!userId) throw new Error("\u7F3A\u5C11 userId"); - const publishDir = path32.join(h5Root, PUBLISH_ROOT_DIR, String(userId), PUBLIC_ZONE_DIR, "wechat-mp"); - fs30.mkdirSync(publishDir, { recursive: true }); - let source = "wechat_media"; - let buffer; - let contentType = ""; - if (mediaId && accessToken) { - try { - const downloaded = await downloadTemporaryMedia(accessToken, mediaId, { wechatFetch }); - buffer = downloaded.buffer; - contentType = downloaded.contentType; - } catch (error) { - if (!picUrl) throw error; - } - } - if (!buffer && picUrl) { - source = "wechat_pic_url"; - const response = await wechatFetch(picUrl, { - method: "GET", - headers: { Accept: "image/*,*/*;q=0.8" } - }); - if (!response.ok) { - const text = await response.text().catch(() => ""); - throw new Error(text || `\u5FAE\u4FE1\u56FE\u7247\u4E0B\u8F7D\u5931\u8D25 (${response.status})`); - } - contentType = response.headers.get("content-type") || contentType; - buffer = Buffer.from(await response.arrayBuffer()); - } - ensureImageWithinLimit(buffer, maxImageBytes); - const resolved = resolveImageExtension(contentType, picUrl); - if (!resolved) { - throw new Error(`\u6682\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B\uFF1A${contentType || "unknown"}`); - } - const timestamp = Date.now(); - const hash = crypto29.createHash("sha1").update(buffer).digest("hex").slice(0, 12); - const fileBase = [appId || "wx", openid || "openid", msgId || timestamp, mediaId || hash].filter(Boolean).join("-").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 120); - const filename = `${fileBase}.${resolved.extension}`; - const absolutePath = path32.join(publishDir, filename); - fs30.writeFileSync(absolutePath, buffer); - return { - absolutePath, - bytes: buffer.length, - contentType: resolved.mimeType, - filename, - publicUrl: buildWechatImagePublicUrl({ - publicBaseUrl, - publishKey: String(userId), - filename - }), - source - }; -} - -// wechat/ack/ack-intent.mjs -var INTENT_RULES = [ - { task: "translate", pattern: /翻译|译成|translate/i }, - { task: "summary", pattern: /总结|概括|摘要|提炼|归纳/ }, - { task: "rewrite", pattern: /改写|润色|优化文|帮我写|写一[篇封份个]/ }, - { task: "poster", pattern: /海报|宣传图|banner/i }, - { task: "ppt", pattern: /ppt|PPT|幻灯片/ }, - { task: "mindmap", pattern: /思维导图|脑图/ }, - { task: "code", pattern: /代码|写个函数|写个方法|debug|报错|bug/i }, - { task: "search", pattern: /查一下|搜索|帮我找|查找/ } -]; -function resolveIntent(msgType, text) { - if (msgType === "image") return { task: "image_analysis" }; - if (msgType === "voice") return { task: "voice_analysis" }; - if (msgType === "location") return { task: "location" }; - if (msgType === "link") return { task: "link" }; - const t = String(text ?? "").trim(); - if (!t) return { task: "unknown" }; - if (shouldUseScheduleAssistant(t)) return { task: "schedule" }; - for (const rule of INTENT_RULES) { - if (rule.pattern.test(t)) return { task: rule.task }; - } - return { task: "unknown" }; -} - -// wechat/ack/ack-templates.mjs -var TEMPLATES = { - default: [ - "\u6536\u5230\uFF0C\u6211\u9A6C\u4E0A\u5904\u7406\u3002", - "\u6536\u5230\uFF0C\u6211\u8FD9\u5C31\u5F00\u59CB\u3002", - "\u597D\u7684\uFF0C\u6211\u5148\u5904\u7406\u4E00\u4E0B\u3002", - "\u660E\u767D\uFF0C\u6211\u5F00\u59CB\u4E86\u3002", - "\u6536\u5230\uFF0C\u5F88\u5FEB\u7ED9\u4F60\u7ED3\u679C\u3002" - ], - image: [ - "\u56FE\u7247\u6536\u5230\uFF0C\u6211\u5148\u770B\u770B\u3002", - "\u6536\u5230\u56FE\u7247\uFF0C\u6211\u5206\u6790\u4E00\u4E0B\u3002", - "\u8BA9\u6211\u770B\u770B\u8FD9\u5F20\u56FE\u7247\u3002", - "\u56FE\u7247\u5DF2\u6536\u5230\uFF0C\u6211\u6765\u5904\u7406\u3002" - ], - voice: [ - "\u6536\u5230\u8BED\u97F3\uFF0C\u6211\u5148\u542C\u4E00\u4E0B\u3002", - "\u542C\u5230\u5566\uFF0C\u6211\u9A6C\u4E0A\u5904\u7406\u3002", - "\u8BED\u97F3\u6536\u5230\uFF0C\u6211\u5148\u8F6C\u6587\u5B57\u3002" - ], - location: ["\u6536\u5230\u4F4D\u7F6E\uFF0C\u6211\u6765\u770B\u770B\u3002"], - link: ["\u6536\u5230\u94FE\u63A5\uFF0C\u6211\u53BB\u8BFB\u4E00\u4E0B\u3002"], - intent: { - translate: ["\u6536\u5230\uFF0C\u6211\u5F00\u59CB\u7FFB\u8BD1\u3002", "\u597D\u7684\uFF0C\u6211\u6765\u7FFB\u8BD1\u4E00\u4E0B\u3002"], - summary: ["\u6536\u5230\uFF0C\u6211\u5F00\u59CB\u6574\u7406\u91CD\u70B9\u3002", "\u597D\u7684\uFF0C\u6211\u6765\u63D0\u70BC\u4E00\u4E0B\u3002"], - rewrite: ["\u6536\u5230\uFF0C\u6211\u5F00\u59CB\u5199\u3002", "\u597D\u7684\uFF0C\u6211\u6765\u6539\u4E00\u4E0B\u3002"], - poster: ["\u6536\u5230\uFF0C\u6211\u6765\u8BBE\u8BA1\u6D77\u62A5\u3002"], - ppt: ["\u6536\u5230\uFF0C\u6211\u5F00\u59CB\u5236\u4F5C PPT\u3002"], - mindmap: ["\u6536\u5230\uFF0C\u6211\u6765\u6574\u7406\u601D\u7EF4\u5BFC\u56FE\u3002"], - code: ["\u6536\u5230\uFF0C\u6211\u6765\u770B\u770B\u4EE3\u7801\u3002", "\u597D\u7684\uFF0C\u6211\u5F00\u59CB\u5904\u7406\u3002"], - search: ["\u6536\u5230\uFF0C\u6211\u53BB\u67E5\u4E00\u4E0B\u3002", "\u597D\u7684\uFF0C\u6211\u6765\u641C\u7D22\u4E00\u4E0B\u3002"], - schedule: ["\u6536\u5230\uFF0C\u6211\u6765\u5E2E\u4F60\u8BBE\u7F6E\u3002"] - } -}; - -// wechat/ack/ack-builders.mjs -function pickRandom(arr, randomEnabled) { - if (!randomEnabled || arr.length === 1) return arr[0]; - return arr[Math.floor(Math.random() * arr.length)]; -} -function withNickname(text, nickname, nicknameEnabled) { - if (!nicknameEnabled || !nickname) return text; - return `${nickname}\uFF0C${text}`; -} -function buildText(pool2, ctx) { - const text = pickRandom(pool2, ctx.randomEnabled); - return withNickname(text, ctx.nickname, ctx.nicknameEnabled); -} -var ImageBuilder = { - priority: 80, - support: (ctx) => ctx.msgType === "image", - build: (ctx) => buildText(TEMPLATES.image, ctx) -}; -var VoiceBuilder = { - priority: 80, - support: (ctx) => ctx.msgType === "voice", - build: (ctx) => buildText(TEMPLATES.voice, ctx) -}; -var LocationBuilder = { - priority: 80, - support: (ctx) => ctx.msgType === "location", - build: (ctx) => buildText(TEMPLATES.location, ctx) -}; -var LinkBuilder = { - priority: 80, - support: (ctx) => ctx.msgType === "link", - build: (ctx) => buildText(TEMPLATES.link, ctx) -}; -var IntentBuilder = { - priority: 50, - support: (ctx) => { - const task = ctx.intent?.task; - return task && task !== "unknown" && TEMPLATES.intent[task]; - }, - build: (ctx) => buildText(TEMPLATES.intent[ctx.intent.task], ctx) -}; -var DefaultBuilder = { - priority: 0, - support: () => true, - build: (ctx) => buildText(TEMPLATES.default, ctx) -}; -var BUILDERS = [ImageBuilder, VoiceBuilder, LocationBuilder, LinkBuilder, IntentBuilder, DefaultBuilder].sort((a, b) => b.priority - a.priority); -function selectBuilder(ctx) { - return BUILDERS.find((b) => b.support(ctx)) ?? DefaultBuilder; -} - -// wechat/ack/ack-provider.mjs -function readConfig(config) { - return { - randomEnabled: config?.ackRandomEnabled !== false, - nicknameEnabled: config?.ackNicknameEnabled !== false - }; -} -function buildAckText({ intent, nickname, config, fallbackText }) { - try { - const cfg = readConfig(config); - const ctx = { - msgType: String(intent?.msgType ?? "text"), - nickname: String(nickname ?? ""), - intent: resolveIntent(intent?.msgType, intent?.agentText), - randomEnabled: cfg.randomEnabled, - nicknameEnabled: cfg.nicknameEnabled - }; - return selectBuilder(ctx).build(ctx); - } catch { - return fallbackText ?? "\u5DF2\u6536\u5230\uFF0C\u6B63\u5728\u5904\u7406\uFF0C\u5B8C\u6210\u540E\u53D1\u5230\u8FD9\u91CC\u3002"; - } -} - -// wechat-mp.mjs -var DEFAULT_WECHAT_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/stable_token"; -var DEFAULT_WECHAT_CUSTOMER_SERVICE_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send"; -var DEFAULT_WECHAT_JSAPI_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"; -var DEFAULT_ASR_TARGET = process.env.H5_ASR_TARGET ?? "https://asr.tkmind.cn"; -var DEFAULT_ACK_TEXT = "\u5DF2\u6536\u5230\uFF0C\u6B63\u5728\u5904\u7406\uFF0C\u5B8C\u6210\u540E\u53D1\u5230\u8FD9\u91CC\u3002"; -var DEFAULT_PROGRESS_TEXT = "\u8FD8\u5728\u5904\u7406\uFF0C\u8BF7\u7A0D\u7B49\u7247\u523B\u3002"; -var DEFAULT_STATUS_TEXT = "\u6211\u5728\u8FD9\u8FB9\u3002\u4E0A\u4E00\u6761\u5982\u679C\u8FD8\u6CA1\u5B8C\u6210\uFF0C\u6211\u4F1A\u7EE7\u7EED\u628A\u7ED3\u679C\u53D1\u7ED9\u4F60\uFF1B\u4F60\u4E5F\u53EF\u4EE5\u76F4\u63A5\u8865\u4E00\u53E5\u8981\u6C42\u3002"; -var DEFAULT_UNSUPPORTED_TEXT = "\u5F53\u524D\u5148\u652F\u6301\u6587\u5B57\u6D88\u606F\uFF0C\u4F60\u53EF\u4EE5\u76F4\u63A5\u53D1\u6587\u5B57\u7ED9\u6211\u3002"; -var DEFAULT_UNBOUND_TEXT = "\u5148\u70B9\u8FD9\u91CC\u5B8C\u6210\u7ED1\u5B9A\uFF0C\u518D\u7EE7\u7EED\u548C\u4E13\u5C5E Agent \u5BF9\u8BDD\uFF1A"; -var DEFAULT_PROGRESS_DELAY_MS = 8e3; -var DEFAULT_SESSION_IDLE_ROTATE_MS = 30 * 60 * 1e3; -var PUBLIC_HTML_LINK_PATTERN2 = /https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/public\/([^\s<>"')\]]+\.html)/gi; -var SESSION_WORKSPACE_TOOL_NAMES = /* @__PURE__ */ new Set([ - "write", - "edit", - "tree", - "read_file", - "write_file", - "edit_file", - "create_dir", - "list_dir" -]); -function escapeRegExp(value) { - return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} -function parseXmlField(xml, field) { - const cdataMatch = xml.match(new RegExp(`<${field}><\\/${field}>`)); - if (cdataMatch) return cdataMatch[1]; - const plainMatch = xml.match(new RegExp(`<${field}>([\\s\\S]*?)<\\/${field}>`)); - return plainMatch ? plainMatch[1].trim() : ""; -} -function parseWechatMessage(xml) { - return { - toUserName: parseXmlField(xml, "ToUserName"), - fromUserName: parseXmlField(xml, "FromUserName"), - createTime: Number(parseXmlField(xml, "CreateTime") || 0), - msgType: parseXmlField(xml, "MsgType").toLowerCase(), - content: parseXmlField(xml, "Content"), - msgId: parseXmlField(xml, "MsgId"), - mediaId: parseXmlField(xml, "MediaId"), - format: parseXmlField(xml, "Format"), - recognition: parseXmlField(xml, "Recognition"), - picUrl: parseXmlField(xml, "PicUrl"), - locationX: parseXmlField(xml, "Location_X"), - locationY: parseXmlField(xml, "Location_Y"), - scale: parseXmlField(xml, "Scale"), - label: parseXmlField(xml, "Label"), - title: parseXmlField(xml, "Title"), - description: parseXmlField(xml, "Description"), - url: parseXmlField(xml, "Url"), - thumbMediaId: parseXmlField(xml, "ThumbMediaId"), - event: parseXmlField(xml, "Event").toLowerCase(), - eventKey: parseXmlField(xml, "EventKey"), - latitude: parseXmlField(xml, "Latitude"), - longitude: parseXmlField(xml, "Longitude"), - precision: parseXmlField(xml, "Precision"), - encrypt: parseXmlField(xml, "Encrypt"), - rawXml: xml - }; -} -function readJsonResponse2(response) { - return response.text().then((text) => { - if (!response.ok) { - throw new Error(text || `upstream ${response.status}`); - } - return text ? JSON.parse(text) : null; - }); -} -async function readJsonBody2(response) { - const text = await response.text(); - if (!text) return { payload: null, text: "" }; - try { - return { payload: JSON.parse(text), text }; - } catch { - return { payload: null, text }; - } -} -function sanitizeAsrMessage(message) { - if (!message || typeof message !== "string") return "\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"; - const trimmed = message.trim(); - if (!trimmed) return "\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"; - if (/Failed to load audio|ffmpeg|Invalid data found when processing input|moov atom not found/i.test(trimmed)) { - return "\u97F3\u9891\u683C\u5F0F\u65E0\u6CD5\u8BC6\u522B\uFF0C\u8BF7\u91CD\u65B0\u5F55\u5236"; - } - if (/timeout|timed out/i.test(trimmed)) return "\u8BC6\u522B\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5"; - if (trimmed.length > 160) return `${trimmed.slice(0, 160)}\u2026`; - return trimmed; -} -function guessVoiceMimeType(format = "", contentType = "") { - const normalizedContentType = String(contentType ?? "").split(";")[0].trim().toLowerCase(); - if (normalizedContentType) return normalizedContentType; - const normalizedFormat = String(format ?? "").trim().toLowerCase(); - if (normalizedFormat === "amr") return "audio/amr"; - if (normalizedFormat === "wav") return "audio/wav"; - if (normalizedFormat === "mp3") return "audio/mpeg"; - if (normalizedFormat === "m4a") return "audio/mp4"; - if (normalizedFormat === "ogg") return "audio/ogg"; - return "application/octet-stream"; -} -function deriveWechatEndpointFromUrl(sourceUrl, pathname) { - if (!sourceUrl) return ""; - try { - const url = new URL(sourceUrl); - url.pathname = pathname; - url.search = ""; - return url.toString(); - } catch { - return ""; - } -} -function createUserMessage2(text, metadata = {}) { - return { - id: crypto30.randomUUID(), - role: "user", - created: Math.floor(Date.now() / 1e3), - content: [{ type: "text", text }], - metadata: { - userVisible: true, - agentVisible: true, - ...metadata - } - }; -} -function messageVisibleText2(message) { - if (!message?.content) return ""; - return message.content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join(""); -} -function pushMessage2(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]; -} -async function executeSessionReply(apiFetch2, sessionId, requestId, prompt, metadata = {}) { - 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 new Error(text || "\u65E0\u6CD5\u5EFA\u7ACB\u516C\u4F17\u53F7\u6D88\u606F\u4E8B\u4EF6\u6D41"); - } - const replyResponse = await apiFetch2(`/sessions/${sessionId}/reply`, { - method: "POST", - body: JSON.stringify({ - request_id: requestId, - user_message: createUserMessage2(prompt, metadata) - }) - }); - if (!replyResponse.ok) { - const text = await replyResponse.text().catch(() => ""); - throw new Error(text || "Agent reply \u8BF7\u6C42\u5931\u8D25"); - } - replyResponse.body?.cancel().catch?.(() => { - }); - const reader = eventsResponse.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - let messages = []; - let hasScopedAssistantUpdate = false; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { 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 new Error("\u5F53\u524D\u56DE\u590D\u9700\u8981\u4EBA\u5DE5\u786E\u8BA4\uFF0C\u516C\u4F17\u53F7\u901A\u9053\u6682\u4E0D\u652F\u6301"); - } - if (event.message.role === "assistant") hasScopedAssistantUpdate = true; - messages = pushMessage2(messages, event.message); - } else if (event.type === "UpdateConversation") { - if (hasScopedAssistantUpdate) { - messages = (event.conversation ?? []).filter((item) => item.metadata?.userVisible); - } - } else if (event.type === "Error") { - throw new Error(event.error || "\u4EFB\u52A1\u6267\u884C\u5931\u8D25"); - } else if (event.type === "Finish") { - const assistant = [...messages].reverse().find((item) => item.role === "assistant"); - if (!hasScopedAssistantUpdate || !assistant) { - throw new Error("\u672C\u8F6E\u672A\u6536\u5230\u53EF\u53D1\u9001\u7684\u65B0\u56DE\u590D\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"); - } - return { - text: messageVisibleText2(assistant), - tokenState: event.token_state ?? null, - messages - }; - } - } - } - throw new Error("\u516C\u4F17\u53F7\u6D88\u606F\u4E8B\u4EF6\u6D41\u63D0\u524D\u7ED3\u675F"); -} -var WECHAT_CUSTOMER_TEXT_MAX_BYTES = 2048; -function splitWechatText(text, maxBytes = WECHAT_CUSTOMER_TEXT_MAX_BYTES) { - const normalized = String(text ?? "").trim(); - if (!normalized) return ["\u6211\u5148\u6536\u5230\u4E86\uFF0C\u4F46\u8FD9\u6B21\u6CA1\u6709\u751F\u6210\u53EF\u53D1\u9001\u7684\u6587\u672C\u7ED3\u679C\u3002"]; - const chunks = []; - let offset = 0; - while (offset < normalized.length) { - let byteCount = 0; - let end = offset; - while (end < normalized.length) { - const charBytes = Buffer.byteLength(normalized[end], "utf8"); - if (byteCount + charBytes > maxBytes) break; - byteCount += charBytes; - end += 1; - } - if (end === offset) break; - chunks.push(normalized.slice(offset, end)); - offset = end; - } - return chunks.length > 0 ? chunks : ["\u6211\u5148\u6536\u5230\u4E86\uFF0C\u4F46\u8FD9\u6B21\u6CA1\u6709\u751F\u6210\u53EF\u53D1\u9001\u7684\u6587\u672C\u7ED3\u679C\u3002"]; -} -function flattenSessionTools(extensions = []) { - return new Set( - extensions.flatMap( - (extension) => Array.isArray(extension?.available_tools) ? extension.available_tools : [] - ) - ); -} -function needsWorkspaceTools(sessionPolicy) { - return developerToolsFromPolicy(sessionPolicy).some((tool) => SESSION_WORKSPACE_TOOL_NAMES.has(tool)); -} -async function readSessionExtensions(fetchForSession, sessionId) { - const payload = await readJsonResponse2(await fetchForSession(sessionId, `/sessions/${sessionId}/extensions`)); - return payload?.extensions ?? []; -} -async function sessionHasRequiredTools(fetchForSession, sessionId, sessionPolicy) { - if (!needsWorkspaceTools(sessionPolicy)) return true; - const desired = developerToolsFromPolicy(sessionPolicy).filter((tool) => SESSION_WORKSPACE_TOOL_NAMES.has(tool)); - if (desired.length === 0) return true; - const available = flattenSessionTools(await readSessionExtensions(fetchForSession, sessionId)); - return desired.some((tool) => available.has(tool)); -} -function extractHtmlWriteTargets(messages = []) { - const targets = /* @__PURE__ */ new Set(); - for (const message of messages) { - for (const item of message?.content ?? []) { - if (item?.type !== "toolRequest") continue; - const toolCall = item.toolCall?.value; - const name = toolCall?.name; - const args = toolCall?.arguments ?? {}; - const action = String(args.action ?? "").toLowerCase(); - const writeLikeDeveloper = name === "developer" && action === "write"; - const normalizedName = String(name ?? "").trim(); - const writeLikeSandbox = (normalizedName === "write_file" || normalizedName === "edit_file" || normalizedName.endsWith("__write_file") || normalizedName.endsWith("__edit_file")) && typeof args.path === "string"; - if (!writeLikeDeveloper && !writeLikeSandbox) continue; - const candidate = String(args.path ?? "").trim(); - if (candidate.toLowerCase().endsWith(".html")) targets.add(candidate); - } - } - return [...targets]; -} -function looksLikeHtmlGenerationIntent(text) { - const normalized = String(text ?? "").trim().toLowerCase(); - if (!normalized) return false; - return /(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/i.test(normalized); -} -function looksLikeDocxDownloadIntent(text) { - const normalized = String(text ?? "").trim(); - if (!normalized) return false; - return /(?:(?:word|docx|\.docx|\.doc|文档).*(?:下载|链接|导出|给我)|(?:下载|导出|提供|给我).*(?:word|docx|\.docx|\.doc|文档))/iu.test(normalized); -} -function usedStaticPagePublishSkill(messages = []) { - return messages.some( - (message) => message?.content?.some((item) => { - if (item?.type !== "toolRequest") return false; - const toolCall = item.toolCall?.value; - const name = String(toolCall?.name ?? "").trim(); - const args = toolCall?.arguments ?? {}; - const skillName = String(args.name ?? "").trim(); - return name === "load_skill" && skillName === "static-page-publish"; - }) - ); -} -function isBareCompletionText(text) { - const normalized = String(text ?? "").trim(); - return /^(?:已完成|完成了|完成)$/u.test(normalized); -} -function hasAnyToolRequest(messages = []) { - return messages.some( - (message) => message?.content?.some((item) => item?.type === "toolRequest") - ); -} -function isSuspiciousBareCompletionReply(reply, intent) { - if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false; - if (!isBareCompletionText(reply?.text)) return false; - if (extractHtmlWriteTargets(reply?.messages ?? []).length > 0) return false; - return !hasAnyToolRequest(reply?.messages ?? []); -} -function looksLikePublishSuccessClaim(text) { - const normalized = String(text ?? "").trim(); - if (!normalized) return false; - return /(?:页面|网页|html).*(?:已创建|已生成|已发布|创建完毕|生成完成|发布成功)|(?:成功发布|已经发布|已创建完毕).*(?:页面|网页|html)/iu.test(normalized); -} -async function hasAnyValidPublishedHtmlLink(text, linkExists, { confirmedArtifacts = [] } = {}) { - const value = String(text ?? ""); - for (const match of value.matchAll(PUBLIC_HTML_LINK_PATTERN2)) { - try { - if (await linkExists(match[0])) return true; - } catch { - } - const filename = String(match[2] ?? "").split("/").pop()?.trim(); - if (filename && confirmedArtifacts.some((artifact) => { - const artifactName = path33.posix.basename(String(artifact.relativePath ?? "").replace(/\\/g, "/")); - return artifactName === filename && artifactFileExists(artifact); - })) { - return true; - } - } - return false; -} -async function isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists = defaultPublicHtmlLinkExists } = {}) { - if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false; - const text = String(reply?.text ?? "").trim(); - if (!looksLikePublishSuccessClaim(text)) return false; - if (extractHtmlWriteTargets(reply?.messages ?? []).length > 0) return false; - return !await hasAnyValidPublishedHtmlLink(text, linkExists); -} -function isMissingRequiredPublishSkill(reply, intent) { - if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false; - const wroteHtml = extractHtmlWriteTargets(reply?.messages ?? []).length > 0; - if (!wroteHtml) return false; - return !usedStaticPagePublishSkill(reply?.messages ?? []); -} -function buildPublicHtmlUrl(workingDir, relativePath, publicBaseUrl) { - const owner = path33.basename(path33.resolve(workingDir)); - const normalized = relativePath.split(path33.sep).map(encodeURIComponent).join("/"); - return buildPublicUrl(publicBaseUrl, owner, normalized); -} -function ensurePublicHtmlArtifact(htmlPath, workingDir) { - const workspaceRoot = path33.resolve(workingDir); - const source = path33.isAbsolute(String(htmlPath ?? "")) ? path33.resolve(String(htmlPath)) : path33.resolve(workspaceRoot, String(htmlPath ?? "")); - if (source !== workspaceRoot && !source.startsWith(`${workspaceRoot}${path33.sep}`)) return null; - if (!fs31.existsSync(source) || !fs31.statSync(source).isFile()) return null; - const publicRoot = path33.join(workspaceRoot, "public"); - let publishedPath = source; - if (source !== publicRoot && !source.startsWith(`${publicRoot}${path33.sep}`)) { - fs31.mkdirSync(publicRoot, { recursive: true }); - publishedPath = path33.join(publicRoot, path33.basename(source)); - if (publishedPath !== source) fs31.copyFileSync(source, publishedPath); - } - const relativePath = path33.relative(workspaceRoot, publishedPath); - if (!relativePath || relativePath.startsWith("..")) return null; - return { - localPath: publishedPath, - relativePath - }; -} -function collectPublishedHtmlArtifacts(reply, { workingDir, publicBaseUrl }) { - const artifacts = []; - const htmlTargets = extractHtmlWriteTargets(reply?.messages ?? []); - for (const target of htmlTargets) { - const artifact = ensurePublicHtmlArtifact(target, workingDir); - if (!artifact) continue; - artifacts.push({ - ...artifact, - url: buildPublicHtmlUrl(workingDir, artifact.relativePath, publicBaseUrl) - }); - } - return artifacts; -} -function extractRequestedHtmlTarget(text) { - const value = String(text ?? ""); - if (!value) return ""; - const explicitPublicMatch = value.match(/(?:^|[\s`'"])(public\/[a-z0-9._-]+\.html)\b/i); - if (explicitPublicMatch?.[1]) return explicitPublicMatch[1]; - const bareMatch = value.match(/(?:^|[\s`'"])([a-z0-9._-]+\.html)\b/i); - if (bareMatch?.[1]) return `public/${bareMatch[1]}`; - return ""; -} -function collectExpectedHtmlArtifacts(intent, { workingDir, publicBaseUrl }) { - const target = extractRequestedHtmlTarget(intent?.agentText ?? intent?.content ?? ""); - if (!target) return []; - const artifact = ensurePublicHtmlArtifact(target, workingDir); - if (!artifact) return []; - return [{ - ...artifact, - url: buildPublicHtmlUrl(workingDir, artifact.relativePath, publicBaseUrl) - }]; -} -function hasAnyUrl(text) { - return /https?:\/\/\S+/i.test(String(text ?? "")); -} -function hasAnyPublicHtmlLink(text) { - return [...String(text ?? "").matchAll(PUBLIC_HTML_LINK_PATTERN2)].length > 0; -} -function collectPublicHtmlLinkFilenames(text) { - const filenames = /* @__PURE__ */ new Set(); - for (const match of String(text ?? "").matchAll(PUBLIC_HTML_LINK_PATTERN2)) { - const relativePath = String(match[2] ?? "").trim(); - const filename = path33.posix.basename(relativePath); - if (filename) filenames.add(filename); - } - return filenames; -} -function collectRecentPublishedHtmlArtifacts(intent, { workingDir, publicBaseUrl, replyText = "", sinceMs = 0, limit = 20 } = {}) { - const publicRoot = path33.join(path33.resolve(workingDir), "public"); - if (!fs31.existsSync(publicRoot) || !fs31.statSync(publicRoot).isDirectory()) return []; - const expectedTarget = extractRequestedHtmlTarget(intent?.agentText ?? intent?.content ?? ""); - const expectedRelativePath = expectedTarget ? expectedTarget.replace(/^\/+/, "").replace(/\\/g, "/") : ""; - const linkedFilenames = collectPublicHtmlLinkFilenames(replyText); - const artifacts = []; - const stack = [publicRoot]; - while (stack.length > 0 && artifacts.length < limit) { - const current = stack.pop(); - let entries = []; - try { - entries = fs31.readdirSync(current, { withFileTypes: true }); - } catch { - continue; - } - for (const entry of entries) { - const absolutePath = path33.join(current, entry.name); - if (entry.isDirectory()) { - stack.push(absolutePath); - continue; - } - if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".html")) continue; - let stat; - try { - stat = fs31.statSync(absolutePath); - } catch { - continue; - } - if (sinceMs > 0 && Number(stat.mtimeMs ?? 0) + 1 < sinceMs) continue; - const relativePath = path33.relative(path33.resolve(workingDir), absolutePath); - if (!relativePath || relativePath.startsWith("..")) continue; - const normalizedRelativePath = relativePath.replace(/\\/g, "/"); - const filename = path33.posix.basename(normalizedRelativePath); - if (linkedFilenames.size > 0 && filename && !linkedFilenames.has(filename)) continue; - artifacts.push({ - localPath: absolutePath, - relativePath: normalizedRelativePath, - url: buildPublicHtmlUrl(workingDir, normalizedRelativePath, publicBaseUrl), - mtimeMs: Number(stat.mtimeMs ?? 0), - matchesExpected: expectedRelativePath ? normalizedRelativePath === expectedRelativePath : false - }); - if (artifacts.length >= limit) break; - } - } - return artifacts.sort((left, right) => { - if (left.matchesExpected !== right.matchesExpected) return left.matchesExpected ? -1 : 1; - return right.mtimeMs - left.mtimeMs; - }).map(({ mtimeMs, matchesExpected, ...artifact }) => artifact); -} -function rewritePublishedHtmlLinks(text, artifacts = []) { - const value = String(text ?? ""); - if (!value || artifacts.length === 0) return value; - let next = value; - for (const artifact of artifacts) { - const canonicalUrl = String(artifact?.url ?? "").trim(); - const filename = String(artifact?.relativePath ?? "").replace(/\\/g, "/").split("/").pop(); - if (!canonicalUrl || !filename) continue; - const wrongPublicLinkPattern = new RegExp( - `https?:\\/\\/[^\\s)]+\\/(?:MindSpace\\/[^\\s/]+\\/)?public\\/${escapeRegExp(filename)}\\b`, - "g" - ); - const wrongTkmindHtmlLinkPattern = new RegExp( - `https?:\\/\\/(?:[^\\s./]+\\.)*tkmind\\.cn\\/[^ -\\s)]*${escapeRegExp(filename)}\\b`, - "gi" - ); - next = next.replace(wrongPublicLinkPattern, canonicalUrl); - next = next.replace(wrongTkmindHtmlLinkPattern, canonicalUrl); - next = next.replace(PUBLIC_HTML_LINK_PATTERN2, (match) => { - if (match === canonicalUrl) return match; - const matchedFilename = String(match).split("/").pop(); - return matchedFilename === filename ? canonicalUrl : match; - }); - } - return next; -} -async function maybeAttachPublishedHtmlLink(reply, { workingDir, publicBaseUrl, artifacts: providedArtifacts = null }) { - const artifacts = Array.isArray(providedArtifacts) ? providedArtifacts : collectPublishedHtmlArtifacts(reply, { workingDir, publicBaseUrl }); - const baseText = rewritePublishedHtmlLinks(String(reply?.text ?? "").trim(), artifacts).trim(); - for (const artifact of artifacts) { - const url = artifact.url; - if (baseText.includes(url)) return baseText; - return baseText ? `${baseText} - -\u67E5\u770B\u9875\u9762\uFF1A -${url}` : `\u67E5\u770B\u9875\u9762\uFF1A -${url}`; - } - return baseText; -} -function stripInternalWechatUsername(text) { - return String(text ?? "").replace(/^\s*wx_[a-z0-9_]{4,64}\s*[,,、::]\s*/i, ""); -} -function replaceLeadingInternalWechatUsername(text, user) { - const addressName = normalizeWechatName(user?.nickname) || normalizeWechatName(user?.displayName); - if (!addressName) return stripInternalWechatUsername(text); - return String(text ?? "").replace( - /(^|[\s,,、::])wx_[a-z0-9_]{4,64}\s*([,,、::])\s*/gi, - (_match, prefix = "", punctuation = "\uFF0C") => `${prefix}${addressName}${punctuation}` - ); -} -function convertMarkdownLinks(text) { - return String(text ?? "").replace(/\[([^\]\n]{1,160})\]\((https?:\/\/[^\s)]+)\)/g, (_match, label, url) => { - const cleanLabel = String(label ?? "").trim(); - const cleanUrl = String(url ?? "").trim(); - return cleanLabel ? `${cleanLabel} -${cleanUrl}` : cleanUrl; - }); -} -function splitMarkdownTableRow(line) { - const trimmed = String(line ?? "").trim(); - if (!trimmed.includes("|")) return null; - return trimmed.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim()); -} -function isMarkdownTableSeparator(line) { - const cells = splitMarkdownTableRow(line); - return Boolean(cells?.length) && cells.every((cell) => /^:?-{3,}:?$/.test(cell)); -} -function convertMarkdownTables(text) { - const lines = String(text ?? "").split("\n"); - const output = []; - for (let index = 0; index < lines.length; index += 1) { - const headers = splitMarkdownTableRow(lines[index]); - if (!headers || !isMarkdownTableSeparator(lines[index + 1])) { - output.push(lines[index]); - continue; - } - const readableRows = []; - index += 2; - while (index < lines.length) { - const cells = splitMarkdownTableRow(lines[index]); - if (!cells) break; - const parts = cells.map((cell, cellIndex) => { - const header = headers[cellIndex] || `\u5217${cellIndex + 1}`; - return cell ? `${header}\uFF1A${cell}` : ""; - }).filter(Boolean); - if (parts.length) readableRows.push(`- ${parts.join("\uFF1B")}`); - index += 1; - } - index -= 1; - output.push(...readableRows); - } - return output.join("\n"); -} -function stripMarkdownEmphasis(text) { - return String(text ?? "").replace(/\*\*([^*\n]+)\*\*/g, "$1").replace(/__([^_\n]+)__/g, "$1").replace(/`([^`\n]+)`/g, "$1").replace(/\*/g, "").replace(/__/g, ""); -} -function formatWechatOutboundText(text, user = null) { - return replaceLeadingInternalWechatUsername( - stripMarkdownEmphasis(convertMarkdownTables(convertMarkdownLinks(text))).replace(/^\s*#{1,6}\s+/gm, "").replace(/^\s*---+\s*$/gm, "").replace(/\n{3,}/g, "\n\n"), - user - ); -} -function defaultPublicHtmlLinkExists(urlText) { - let url; - try { - url = new URL(urlText); - } catch { - return true; - } - const parts = url.pathname.split("/").filter(Boolean).map((part) => { - try { - return decodeURIComponent(part); - } catch { - return part; - } - }); - if (parts.length < 4 || parts[0] !== PUBLISH_ROOT_DIR || parts[2] !== "public") return true; - const owner = parts[1]; - const rest = parts.slice(3); - if (!owner || rest.some((part) => !part || part === "." || part === "..")) return false; - const root = path33.resolve(process.cwd(), PUBLISH_ROOT_DIR, owner, "public"); - const target = path33.resolve(root, ...rest); - if (target !== root && !target.startsWith(`${root}${path33.sep}`)) return false; - return fs31.existsSync(target) && fs31.statSync(target).isFile(); -} -function createPublicHtmlLinkExists(workingDir) { - const workspaceRoot = path33.resolve(String(workingDir ?? "")); - const workspaceKey = path33.basename(workspaceRoot); - return (urlText) => { - let url; - try { - url = new URL(urlText); - } catch { - return true; - } - const parts = url.pathname.split("/").filter(Boolean).map((part) => { - try { - return decodeURIComponent(part); - } catch { - return part; - } - }); - if (parts.length < 4 || parts[0] !== PUBLISH_ROOT_DIR || parts[2] !== "public") return true; - const owner = parts[1]; - const rest = parts.slice(3); - if (!owner || rest.some((part) => !part || part === "." || part === "..")) return false; - if (owner === workspaceKey) { - const root = path33.join(workspaceRoot, "public"); - const target = path33.resolve(root, ...rest); - if (target !== root && !target.startsWith(`${root}${path33.sep}`)) return false; - return fs31.existsSync(target) && fs31.statSync(target).isFile(); - } - return defaultPublicHtmlLinkExists(urlText); - }; -} -function resolveLinkExistsForWorkingDir(workingDir, linkExists = defaultPublicHtmlLinkExists) { - if (linkExists !== defaultPublicHtmlLinkExists) return linkExists; - return createPublicHtmlLinkExists(workingDir); -} -async function guardMissingPublicHtmlLinks(text, { linkExists = defaultPublicHtmlLinkExists } = {}) { - const value = String(text ?? ""); - const replacements = []; - for (const match of value.matchAll(PUBLIC_HTML_LINK_PATTERN2)) { - const url = match[0]; - let exists = true; - try { - exists = await linkExists(url); - } catch { - exists = false; - } - if (!exists) { - const filename = match[2].split("/").pop() ?? "\u9875\u9762"; - replacements.push([ - url, - `\uFF08\u9875\u9762\u751F\u6210\u672A\u5B8C\u6210\uFF0C\u5DF2\u963B\u6B62\u53D1\u9001\u5931\u6548\u94FE\u63A5\uFF1A${filename}\u3002\u8BF7\u91CD\u65B0\u751F\u6210\u9875\u9762\u540E\u518D\u6253\u5F00\u3002\uFF09` - ]); - } - } - return replacements.reduce((next, [from, to]) => next.replaceAll(from, to), value); -} -function downgradePrematurePublishClaims(text) { - const value = String(text ?? ""); - if (!value.includes("\u9875\u9762\u751F\u6210\u672A\u5B8C\u6210")) return value; - return value.replace(/(^|\n)([^\n]*页面都已经成功发布了[^\n]*)(?=\n|$)/g, "$1\u9875\u9762\u6682\u672A\u751F\u6210\u5B8C\u6210\uFF0C\u8FD9\u6B21\u6211\u5148\u4E0D\u53D1\u9001\u5931\u6548\u94FE\u63A5\u3002").replace(/(^|\n)([^\n]*主题页面已发布[^\n]*)(?=\n|$)/g, "$1\u{1F334} \u590F\u65E5\u4E3B\u9898\u9875\u9762\u751F\u6210\u672A\u5B8C\u6210").replace(/页面已创建完毕/g, "\u9875\u9762\u751F\u6210\u672A\u5B8C\u6210").replace(/页面已创建/g, "\u9875\u9762\u751F\u6210\u672A\u5B8C\u6210").replace(/发布成功/g, "\u751F\u6210\u672A\u5B8C\u6210").replace(/已发布成功/g, "\u751F\u6210\u672A\u5B8C\u6210"); -} -function buildHtmlPublishFailureText() { - return [ - "\u8FD9\u6B21\u9875\u9762\u6CA1\u6709\u6309 H5 \u91CC\u7684\u9875\u9762\u6280\u80FD\u771F\u6B63\u751F\u6210\u6210\u529F\uFF0C\u6240\u4EE5\u6211\u5148\u4E0D\u53D1\u4E0D\u4E00\u81F4\u7684\u7B80\u7248\u9875\u3002", - "\u8BF7\u76F4\u63A5\u91CD\u53D1\u4E00\u6B21\u4F60\u7684\u9875\u9762\u9700\u6C42\uFF0C\u6211\u4F1A\u6309\u548C H5 \u76F8\u540C\u7684 `static-page-publish` \u6280\u80FD\u94FE\u8DEF\u91CD\u505A\u3002" - ].join("\n"); -} -function artifactFileExists(artifact) { - const localPath = String(artifact?.localPath ?? "").trim(); - if (!localPath) return false; - try { - return fs31.existsSync(localPath) && fs31.statSync(localPath).isFile(); - } catch { - return false; - } -} -function uniqueArtifactsByUrl(artifacts = []) { - const byUrl = /* @__PURE__ */ new Map(); - for (const artifact of artifacts) { - const url = String(artifact?.url ?? "").trim(); - if (url) byUrl.set(url, artifact); - } - return [...byUrl.values()]; -} -function allExistingHtmlArtifacts({ publishedArtifacts = [], expectedArtifacts = [], recentArtifacts = [] } = {}) { - return uniqueArtifactsByUrl([ - ...publishedArtifacts, - ...expectedArtifacts, - ...recentArtifacts - ]).filter((artifact) => artifactFileExists(artifact)); -} -function buildVerifiedHtmlArtifacts({ - publishedArtifacts = [], - expectedArtifacts = [], - recentArtifacts = [], - replyText = "" -} = {}) { - const existing = allExistingHtmlArtifacts({ publishedArtifacts, expectedArtifacts, recentArtifacts }); - const linkedFilenames = collectPublicHtmlLinkFilenames(replyText); - if (linkedFilenames.size === 0) return existing; - const matched = existing.filter((artifact) => { - const filename = path33.posix.basename(String(artifact.relativePath ?? "").replace(/\\/g, "/")); - return filename && linkedFilenames.has(filename); - }); - return matched; -} -function resolveHtmlPublishArtifacts({ - reply, - intent, - workingDir, - publicBaseUrl, - requestStartedAt -}) { - materializeMissingPublicHtmlWrites({ - messages: reply?.messages ?? [], - publishDir: workingDir - }); - const publishedArtifacts = collectPublishedHtmlArtifacts(reply, { - workingDir, - publicBaseUrl - }); - const expectedArtifacts = collectExpectedHtmlArtifacts(intent, { - workingDir, - publicBaseUrl - }); - const recentArtifacts = collectRecentPublishedHtmlArtifacts(intent, { - workingDir, - publicBaseUrl, - replyText: reply?.text, - sinceMs: requestStartedAt - }); - const confirmedArtifacts = allExistingHtmlArtifacts({ - publishedArtifacts, - expectedArtifacts, - recentArtifacts - }); - const verifiedArtifacts = buildVerifiedHtmlArtifacts({ - publishedArtifacts, - expectedArtifacts, - recentArtifacts, - replyText: reply?.text - }); - return { - publishedArtifacts, - expectedArtifacts, - recentArtifacts, - confirmedArtifacts, - verifiedArtifacts - }; -} -function shouldRetryHtmlGenerationReply({ - reply, - intent, - confirmedArtifacts = [], - hasValidLinkInReply = false -}) { - if (!looksLikeHtmlGenerationIntent(intent?.agentText)) return false; - const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text); - if (replyHasPublicLinks) { - if (!hasValidLinkInReply) return true; - if (isMissingRequiredPublishSkill(reply, intent)) return true; - return false; - } - if (confirmedArtifacts.length > 0) return false; - if (isMissingRequiredPublishSkill(reply, intent) || isSuspiciousBareCompletionReply(reply, intent)) { - return true; - } - if (!usedStaticPagePublishSkill(reply?.messages ?? [])) return true; - return !hasAnyUrl(reply?.text); -} -function isQuestionStatusProbe(text) { - return /^[??]+$/.test(String(text ?? "").trim()); -} -function isSimpleGreeting(text) { - const normalized = String(text ?? "").trim().replace(/[!!。.\s]+$/g, "").toLowerCase(); - return /^(你好|您好|在吗|在不在|嗨|hi|hello|hey)$/.test(normalized); -} -function isConnectivityTest(text) { - const normalized = String(text ?? "").trim().replace(/[!!。.\s]+$/g, ""); - return /^(测试\s*\d*|test\s*\d*)$/i.test(normalized); -} -function isTopicResetIntent(text) { - const normalized = String(text ?? "").trim(); - return /^(换(个)?话题|新问题|忽略之前|不管之前|重新开始|reset)$/i.test(normalized) || /忽略.*之前|不要管.*之前|别管.*之前/.test(normalized); -} -function normalizeNumber(value) { - if (value === "" || value === null || value === void 0) return null; - const num = Number(value); - return Number.isFinite(num) ? num : null; -} -function normalizeWechatInboundIntent(inbound) { - const msgType = String(inbound?.msgType ?? "").toLowerCase(); - const base = { - appId: "", - openid: String(inbound?.fromUserName ?? "").trim(), - msgId: String(inbound?.msgId ?? "").trim(), - msgType, - displayText: "", - agentText: "", - raw: { ...inbound } - }; - if (msgType === "text") { - const content = String(inbound?.content ?? "").trim(); - return { - ...base, - displayText: content, - agentText: content - }; - } - if (msgType === "voice") { - const recognition = String(inbound?.recognition ?? "").trim(); - return { - ...base, - displayText: recognition ? `\u8BED\u97F3\uFF1A${recognition}` : "\u8BED\u97F3\u6D88\u606F", - agentText: recognition, - media: { - mediaId: inbound?.mediaId || "", - format: inbound?.format || "" - } - }; - } - if (msgType === "image") { - return { - ...base, - displayText: "\u6536\u5230\u56FE\u7247\uFF0C\u8BF7\u63CF\u8FF0\u4F60\u60F3\u8BA9\u6211\u600E\u4E48\u5904\u7406", - agentText: "", - media: { - mediaId: inbound?.mediaId || "", - picUrl: inbound?.picUrl || "" - } - }; - } - if (msgType === "location") { - const latitude = normalizeNumber(inbound?.locationX); - const longitude = normalizeNumber(inbound?.locationY); - const scale = normalizeNumber(inbound?.scale); - const label = String(inbound?.label ?? "").trim(); - const fragments = [ - label ? `\u7528\u6237\u53D1\u9001\u4E86\u4F4D\u7F6E\uFF1A${label}` : "\u7528\u6237\u53D1\u9001\u4E86\u4F4D\u7F6E", - latitude !== null ? `\u7EAC\u5EA6\uFF1A${latitude}` : "", - longitude !== null ? `\u7ECF\u5EA6\uFF1A${longitude}` : "", - scale !== null ? `\u7F29\u653E\uFF1A${scale}` : "" - ].filter(Boolean); - return { - ...base, - displayText: fragments.join("\uFF1B"), - agentText: fragments.join("\uFF1B"), - location: { - latitude, - longitude, - scale, - label - } - }; - } - if (msgType === "link") { - const title = String(inbound?.title ?? "").trim(); - const description = String(inbound?.description ?? "").trim(); - const url = String(inbound?.url ?? "").trim(); - const fragments = [ - "\u7528\u6237\u5206\u4EAB\u4E86\u94FE\u63A5", - title ? `\u6807\u9898\uFF1A${title}` : "", - description ? `\u63CF\u8FF0\uFF1A${description}` : "", - url ? `URL\uFF1A${url}` : "" - ].filter(Boolean); - return { - ...base, - displayText: fragments.join("\uFF1B"), - agentText: fragments.join("\uFF1B"), - link: { title, description, url } - }; - } - if (msgType === "video" || msgType === "shortvideo") { - return { - ...base, - displayText: msgType === "shortvideo" ? "\u6536\u5230\u5C0F\u89C6\u9891" : "\u6536\u5230\u89C6\u9891", - agentText: "", - media: { - mediaId: inbound?.mediaId || "", - thumbMediaId: inbound?.thumbMediaId || "" - } - }; - } - if (msgType === "event") { - return { - ...base, - displayText: inbound?.event ? `\u4E8B\u4EF6\uFF1A${inbound.event}` : "\u4E8B\u4EF6\u6D88\u606F", - agentText: "", - location: inbound?.event === "location" ? { - latitude: normalizeNumber(inbound?.latitude), - longitude: normalizeNumber(inbound?.longitude), - precision: normalizeNumber(inbound?.precision) - } : void 0 - }; - } - return base; -} -function buildWechatAgentPrompt(intent, { grantedSkills = [] } = {}) { - const msgType = String(intent?.msgType ?? "text"); - const autoSkillPrefix = msgType === "text" || msgType === "voice" ? buildAutoChatSkillPrefix(intent?.agentText ?? intent?.content, grantedSkills) : ""; - const docxDownloadHint = looksLikeHtmlGenerationIntent(intent?.agentText ?? intent?.content) && looksLikeDocxDownloadIntent(intent?.agentText ?? intent?.content) ? [ - "\u3010Word \u4E0B\u8F7D\u8981\u6C42\u3011\u7528\u6237\u660E\u786E\u8981\u6C42\u9875\u9762\u91CC\u53EF\u4E0B\u8F7D Word / docx\u3002", - "\u5F00\u59CB\u524D\u5FC5\u987B\u5148\u8C03\u7528 `load_skill` \u2192 `docx-generate`\uFF0C\u5E76\u6309\u6280\u80FD\u8BF4\u660E\u751F\u6210\u76EE\u6807 `public/*.docx`\u3002", - "\u751F\u6210\u540E\u5FC5\u987B\u786E\u8BA4\u76EE\u6807 `.docx` \u5DF2\u5B58\u5728\uFF0C\u518D\u8C03\u7528 `load_skill` \u2192 `static-page-publish` \u5199 `public/*.html`\u3002", - "HTML \u4E2D\u53EA\u80FD\u7528\u540C\u76EE\u5F55\u76F8\u5BF9\u8DEF\u5F84\u94FE\u63A5\u8BE5\u6587\u6863\uFF1B\u7981\u6B62\u53EA\u5199\u4E0B\u8F7D\u6309\u94AE\u5374\u6CA1\u6709\u5148\u628A `.docx` \u843D\u76D8\u3002", - "" - ].join("\n") : ""; - const pagePublishHint = looksLikeHtmlGenerationIntent(intent?.agentText ?? intent?.content) ? [ - "\u3010\u9875\u9762\u53D1\u5E03\u6280\u80FD\u8981\u6C42\u3011\u8FD9\u6761\u6D88\u606F\u662F\u5728\u751F\u6210\u53EF\u8BBF\u95EE HTML \u9875\u9762\u3002", - "\u5F00\u59CB\u524D\u5FC5\u987B\u5148\u8C03\u7528 `load_skill` \u2192 `static-page-publish`\uFF0C\u4E0D\u80FD\u7701\u7565\uFF0C\u4E5F\u4E0D\u80FD\u5199\u5B8C\u9875\u9762\u540E\u518D\u8865\u8C03\u3002", - "\u968F\u540E\u5FC5\u987B\u7528 sandbox-fs \u7684 `write_file` / `edit_file` \u5199\u5165 `public/*.html`\u3002", - "\u7981\u6B62\u7528 shell / cat / heredoc / echo / cp \u5199\u5165 HTML\uFF1Ashell \u5728\u5BB9\u5668\u5185\u6267\u884C\uFF0C\u6587\u4EF6\u4E0D\u4F1A\u51FA\u73B0\u5728\u516C\u7F51 MindSpace \u8DEF\u5F84\uFF0C\u7528\u6237\u4F1A\u6536\u5230\u300C\u6587\u4EF6\u4E0D\u5B58\u5728\u300D\u3002", - "\u5728\u56DE\u590D\u7528\u6237\u524D\uFF0C\u786E\u8BA4 `public/` \u4E0B\u76EE\u6807 HTML \u5DF2\u901A\u8FC7 write_file \u843D\u76D8\uFF1B\u6CA1\u6709\u843D\u76D8\u5C31\u4E0D\u8981\u53D1\u9001\u94FE\u63A5\u6216\u8BF4\u300C\u5DF2\u53D1\u5E03\u300D\u3002", - "\u6700\u7EC8\u53EA\u7ED9\u7528\u6237\u4E00\u4E2A\u6B63\u5F0F\u57DF\u540D\u7684\u552F\u4E00\u6B63\u786E\u94FE\u63A5\uFF1B\u4E0D\u8981\u8F93\u51FA\u9519\u8BEF\u57DF\u540D\u3001\u5907\u7528\u94FE\u63A5\u6216\u8BA9\u7528\u6237\u624B\u52A8\u4FDD\u5B58\u6587\u4EF6\u3002", - "" - ].join("\n") : ""; - const scheduleTimezone = process.env.H5_DEFAULT_TIMEZONE || "Asia/Shanghai"; - const currentTimeHint = buildCurrentTimeAgentPrefix({ timezone: scheduleTimezone }); - const scheduleAssistantHint = shouldUseScheduleAssistant(intent?.agentText ?? intent?.content) ? [ - "\u3010\u65E5\u7A0B\u6280\u80FD\u8981\u6C42\u3011\u8FD9\u6761\u6D88\u606F\u6D89\u53CA\u5F85\u529E\u3001\u63D0\u9192\u6216\u65E5\u7A0B\u3002", - "\u5F00\u59CB\u524D\u5148\u52A0\u8F7D `schedule-assistant` skill\uFF0C\u5E76\u4E25\u683C\u6309 skill \u91CC\u7684\u8FB9\u754C\u6267\u884C\u3002", - "\u5199\u5165\u5DE5\u5177\u65F6\u4F18\u5148\u4F7F\u7528 startLocal / endLocal / remindLocal\uFF08YYYY-MM-DD HH:mm\uFF09\uFF0C\u4E0D\u8981\u81EA\u884C\u4F30\u7B97 Unix \u6BEB\u79D2\u3002", - "\u53EA\u6709\u5728 `schedule_create_item` / `schedule_create_reminder` \u7B49\u5DE5\u5177\u6210\u529F\u8FD4\u56DE\u540E\uFF0C\u624D\u80FD\u544A\u8BC9\u7528\u6237\u201C\u5DF2\u7ECF\u8BBE\u7F6E\u597D\u4E86\u201D\u3002", - intent?.msgId ? `\u8C03\u7528 schedule_create_item \u65F6\u5FC5\u987B\u4F20\u5165 sourceMessageId: ${intent.msgId}` : "", - "" - ].filter(Boolean).join("\n") : ""; - if (msgType === "voice") { - return [ - docxDownloadHint, - currentTimeHint, - scheduleAssistantHint, - "\u3010\u5FAE\u4FE1\u670D\u52A1\u53F7\u8BED\u97F3\u6D88\u606F\u3011\u7528\u6237\u901A\u8FC7\u8BED\u97F3\u8F93\u5165\uFF0C\u4EE5\u4E0B\u662F\u5FAE\u4FE1\u8BC6\u522B\u7ED3\u679C\u3002", - "", - `\u7528\u6237\u8BED\u97F3\u8BC6\u522B\u6587\u672C\uFF1A${autoSkillPrefix}${String(intent?.agentText ?? "").trim()}` - ].filter(Boolean).join("\n"); - } - if (msgType === "image") { - return [ - docxDownloadHint, - currentTimeHint, - scheduleAssistantHint, - "\u3010\u5FAE\u4FE1\u670D\u52A1\u53F7\u56FE\u7247\u6D88\u606F\u3011\u7528\u6237\u53D1\u9001\u4E86\u56FE\u7247\u3002", - "\u5982\u7528\u6237\u6CA1\u6709\u660E\u786E\u8981\u6C42\uFF0C\u8BF7\u5148\u6839\u636E\u56FE\u7247\u5185\u5BB9\u7ED9\u51FA\u7B80\u77ED\u7406\u89E3\uFF0C\u5E76\u8BE2\u95EE\u4E0B\u4E00\u6B65\u3002", - "", - String(intent?.agentText ?? "").trim() - ].filter(Boolean).join("\n"); - } - if (msgType === "location") { - const location = intent?.location ?? {}; - return [ - currentTimeHint, - scheduleAssistantHint, - "\u3010\u5FAE\u4FE1\u670D\u52A1\u53F7\u4F4D\u7F6E\u6D88\u606F\u3011\u7528\u6237\u53D1\u9001\u4E86\u5F53\u524D\u4F4D\u7F6E\u3002", - location.label ? `\u5730\u5740\uFF1A${location.label}` : "", - location.latitude !== null && location.latitude !== void 0 ? `\u7EAC\u5EA6\uFF1A${location.latitude}` : "", - location.longitude !== null && location.longitude !== void 0 ? `\u7ECF\u5EA6\uFF1A${location.longitude}` : "", - "\u8BF7\u7ED3\u5408\u4F4D\u7F6E\u56DE\u7B54\u7528\u6237\u53EF\u80FD\u7684\u8DEF\u7EBF\u3001\u9644\u8FD1\u3001\u884C\u7A0B\u6216\u63D0\u9192\u9700\u6C42\uFF1B\u5982\u679C\u610F\u56FE\u4E0D\u660E\u786E\uFF0C\u5148\u7B80\u77ED\u8BE2\u95EE\u3002" - ].filter(Boolean).join("\n"); - } - if (msgType === "link") { - const link = intent?.link ?? {}; - return [ - currentTimeHint, - scheduleAssistantHint, - "\u3010\u5FAE\u4FE1\u670D\u52A1\u53F7\u94FE\u63A5\u6D88\u606F\u3011\u7528\u6237\u5206\u4EAB\u4E86\u94FE\u63A5\u3002", - link.title ? `\u6807\u9898\uFF1A${link.title}` : "", - link.description ? `\u63CF\u8FF0\uFF1A${link.description}` : "", - link.url ? `URL\uFF1A${link.url}` : "" - ].filter(Boolean).join("\n"); - } - const content = String(intent?.agentText ?? intent?.content ?? "").trim(); - const lines = [currentTimeHint]; - if (docxDownloadHint) lines.push(docxDownloadHint); - if (pagePublishHint) lines.push(pagePublishHint); - if (scheduleAssistantHint) lines.push(scheduleAssistantHint); - lines.push( - "\u3010\u5FAE\u4FE1\u670D\u52A1\u53F7\u65B0\u6D88\u606F\u3011\u8BF7\u53EA\u56DE\u7B54\u4E0B\u9762\u8FD9\u6761\u7528\u6237\u6D88\u606F\uFF0C\u4E0D\u8981\u4E3B\u52A8\u5EF6\u7EED\u65E0\u5173\u7684\u5386\u53F2\u8BDD\u9898\u3002", - "\u82E5\u7528\u6237\u53EA\u662F\u5728\u6D4B\u8BD5\u8FDE\u901A\u6027\uFF0C\u8BF7\u4E00\u53E5\u8BDD\u786E\u8BA4\u6536\u5230\u5373\u53EF\uFF0C\u4E0D\u8981\u5C55\u5F00\u65E7\u4EFB\u52A1\u3002", - "", - `\u7528\u6237\u6D88\u606F\uFF1A${autoSkillPrefix}${content}` - ); - return lines.join("\n"); -} -function looksLikeScheduleConfirmation(text) { - const compact = String(text ?? "").replace(/\s+/g, ""); - if (!compact) return false; - if (/(没有|没能|未能|无法|不能|失败|需要你|请补充|请告诉)/.test(compact)) return false; - return /(已经|已|现在).{0,12}(设置|安排|记录|加上|添加|创建).{0,12}(待办|提醒|日程|安排|闹钟)|设置好了|已经设置好了|已经加上/.test( - compact - ); -} -function buildConnectivityTestReply(user) { - const name = resolveWechatAddressName(user); - return name ? `${name}\uFF0C\u516C\u4F17\u53F7\u6D88\u606F\u901A\u9053\u6B63\u5E38\uFF0C\u6211\u6536\u5230\u4F60\u7684\u6D4B\u8BD5\u4E86\u3002` : "\u516C\u4F17\u53F7\u6D88\u606F\u901A\u9053\u6B63\u5E38\uFF0C\u6211\u6536\u5230\u4F60\u7684\u6D4B\u8BD5\u4E86\u3002"; -} -function normalizeWechatName(value) { - const name = String(value ?? "").trim(); - if (!name || /^wx_[a-z0-9_]{4,64}$/i.test(name)) return ""; - return name; -} -function resolveWechatAddressName(user) { - return normalizeWechatName(user?.nickname) || normalizeWechatName(user?.displayName) || ""; -} -function buildGreetingText(user) { - const name = resolveWechatAddressName(user); - return name ? `\u4F60\u597D\uFF0C${name}\uFF01\u6211\u5728\u5462\uFF0C\u6709\u4EC0\u4E48\u9700\u8981\uFF1F` : "\u4F60\u597D\uFF01\u6211\u5728\u5462\uFF0C\u6709\u4EC0\u4E48\u9700\u8981\uFF1F"; -} -function buildStatusText(user, fallbackText) { - const name = resolveWechatAddressName(user); - if (!name) return fallbackText; - return `\u6211\u5728\u8FD9\u8FB9\uFF0C${name}\u3002\u4E0A\u4E00\u6761\u5982\u679C\u8FD8\u6CA1\u5B8C\u6210\uFF0C\u6211\u4F1A\u7EE7\u7EED\u628A\u7ED3\u679C\u53D1\u7ED9\u4F60\uFF1B\u4F60\u4E5F\u53EF\u4EE5\u76F4\u63A5\u8865\u4E00\u53E5\u8981\u6C42\u3002`; -} -function successResponse(task = null) { - return { - ok: true, - status: 200, - contentType: "text/plain; charset=utf-8", - body: "success", - ...task ? { task } : {} - }; -} -function loadWechatMpConfig(env = process.env) { - const appId = env.H5_WECHAT_MP_APP_ID?.trim() ?? env.H5_WECHAT_APP_ID?.trim() ?? ""; - const appSecret = env.H5_WECHAT_MP_APP_SECRET?.trim() ?? env.H5_WECHAT_APP_SECRET?.trim() ?? ""; - const token = env.H5_WECHAT_MP_TOKEN?.trim() ?? ""; - const publicBaseUrl = env.H5_PUBLIC_BASE_URL?.trim()?.replace(/\/$/, "") ?? ""; - const enabledFlag = env.H5_WECHAT_MP_ENABLED === "1"; - const bindPath = env.H5_WECHAT_MP_BIND_PATH?.trim() || "/auth/wechat/authorize?intent=login"; - return { - enabled: enabledFlag && Boolean(appId && appSecret && token && publicBaseUrl), - appId, - appSecret, - token, - publicBaseUrl, - bindPath, - ackText: env.H5_WECHAT_MP_ACK_TEXT?.trim() || DEFAULT_ACK_TEXT, - ackRandomEnabled: env.H5_WECHAT_MP_ACK_RANDOM !== "0", - ackNicknameEnabled: env.H5_WECHAT_MP_ACK_NICKNAME !== "0", - progressText: env.H5_WECHAT_MP_PROGRESS_TEXT?.trim() || DEFAULT_PROGRESS_TEXT, - progressDelayMs: Math.max( - 0, - Number(env.H5_WECHAT_MP_PROGRESS_DELAY_MS ?? DEFAULT_PROGRESS_DELAY_MS) - ), - sessionIdleRotateMs: Math.max( - 0, - Number(env.H5_WECHAT_MP_SESSION_IDLE_ROTATE_MS ?? DEFAULT_SESSION_IDLE_ROTATE_MS) - ), - statusText: env.H5_WECHAT_MP_STATUS_TEXT?.trim() || DEFAULT_STATUS_TEXT, - unsupportedText: env.H5_WECHAT_MP_UNSUPPORTED_TEXT?.trim() || DEFAULT_UNSUPPORTED_TEXT, - unboundTextPrefix: env.H5_WECHAT_MP_UNBOUND_TEXT_PREFIX?.trim() || DEFAULT_UNBOUND_TEXT, - tokenUrl: env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_WECHAT_TOKEN_URL, - customerServiceUrl: env.H5_WECHAT_MP_CUSTOMER_SERVICE_URL?.trim() || DEFAULT_WECHAT_CUSTOMER_SERVICE_URL, - jsapiTicketUrl: env.H5_WECHAT_MP_JSAPI_TICKET_URL?.trim() || deriveWechatEndpointFromUrl(env.H5_WECHAT_MP_TOKEN_URL?.trim(), "/cgi-bin/ticket/getticket") || DEFAULT_WECHAT_JSAPI_TICKET_URL, - mediaPublicBaseUrl: env.H5_WECHAT_MP_MEDIA_PUBLIC_BASE_URL?.trim()?.replace(/\/$/, "") || publicBaseUrl, - maxImageBytes: Math.max(1, Number(env.H5_WECHAT_MP_MAX_IMAGE_BYTES ?? 10 * 1024 * 1024)), - acceptVoice: env.H5_WECHAT_MP_ACCEPT_VOICE !== "0", - acceptImage: env.H5_WECHAT_MP_ACCEPT_IMAGE !== "0", - acceptLocation: env.H5_WECHAT_MP_ACCEPT_LOCATION !== "0", - acceptLink: env.H5_WECHAT_MP_ACCEPT_LINK !== "0", - encodingAesKey: env.H5_WECHAT_MP_ENCODING_AES_KEY?.trim() ?? "" - }; -} -function sha1Hex(parts) { - return crypto30.createHash("sha1").update([...parts].sort().join("")).digest("hex"); -} -function verifyWechatMpSignature({ token, timestamp, nonce, signature }) { - return sha1Hex([token, timestamp, nonce]) === String(signature ?? ""); -} -function verifyWechatMpMsgSignature({ token, timestamp, nonce, echoStr, msgSignature }) { - return sha1Hex([token, timestamp, nonce, echoStr]) === String(msgSignature ?? ""); -} -function decodeWechatMpAesKey(encodingAesKey) { - const key = Buffer.from(`${String(encodingAesKey ?? "").trim()}=`, "base64"); - if (key.length !== 32) { - throw new Error("invalid encoding aes key"); - } - return key; -} -function decryptWechatMpPayload({ encodingAesKey, appId, encrypted }) { - const key = decodeWechatMpAesKey(encodingAesKey); - const iv = key.subarray(0, 16); - const decipher = crypto30.createDecipheriv("aes-256-cbc", key, iv); - decipher.setAutoPadding(false); - const cipherBuf = Buffer.from(String(encrypted ?? ""), "base64"); - let decoded = Buffer.concat([decipher.update(cipherBuf), decipher.final()]); - const pad = decoded.at(-1); - if (!Number.isInteger(pad) || pad < 1 || pad > 32) { - throw new Error("invalid padding"); - } - decoded = decoded.subarray(0, decoded.length - pad); - const content = decoded.subarray(16); - const msgLen = content.readUInt32BE(0); - const msg = content.subarray(4, 4 + msgLen).toString("utf8"); - const receivedAppId = content.subarray(4 + msgLen).toString("utf8"); - if (receivedAppId !== String(appId ?? "")) { - throw new Error("appid mismatch"); - } - return msg; -} -function verifyWechatMpUrlChallenge(query = {}, config = {}) { - const timestamp = String(query.timestamp ?? ""); - const nonce = String(query.nonce ?? ""); - const echostr = String(query.echostr ?? ""); - const encryptType = String(query.encrypt_type ?? "").toLowerCase(); - if (encryptType === "aes") { - const msgSignature = String(query.msg_signature ?? ""); - if (!config?.token || !config?.encodingAesKey || !config?.appId) { - return { ok: false, status: 503, body: "wechat mp aes mode not configured" }; - } - if (!verifyWechatMpMsgSignature({ - token: config.token, - timestamp, - nonce, - echoStr: echostr, - msgSignature - })) { - return { ok: false, status: 403, body: "invalid signature" }; - } - try { - const plain = decryptWechatMpPayload({ - encodingAesKey: config.encodingAesKey, - appId: config.appId, - encrypted: echostr - }); - return { ok: true, status: 200, body: plain }; - } catch { - return { ok: false, status: 403, body: "invalid echostr" }; - } - } - if (!verifyWechatMpSignature({ - token: config?.token, - timestamp, - nonce, - signature: String(query.signature ?? "") - })) { - return { ok: false, status: 403, body: "invalid signature" }; - } - return { ok: true, status: 200, body: echostr }; -} -function buildWechatTextReply({ - toUserName, - fromUserName, - content, - createTime = Math.floor(Date.now() / 1e3) -}) { - return [ - "", - ``, - ``, - `${Number(createTime) || Math.floor(Date.now() / 1e3)}`, - "", - ``, - "" - ].join(""); -} -function createWechatMpService({ - config, - userAuth: userAuth2, - apiFetch: apiFetch2, - startAgentSession = null, - sessionApiFetch = null, - scheduleService: scheduleService2 = null, - applySessionLlmProvider = null, - refreshSessionSnapshot = null, - wechatFetch = undiciFetch7, - linkExists = defaultPublicHtmlLinkExists, - logger = console -}) { - if (!config?.enabled) { - return { - enabled: false - }; - } - config = { - ...config, - tokenUrl: config.tokenUrl || DEFAULT_WECHAT_TOKEN_URL, - customerServiceUrl: config.customerServiceUrl || DEFAULT_WECHAT_CUSTOMER_SERVICE_URL, - jsapiTicketUrl: config.jsapiTicketUrl || DEFAULT_WECHAT_JSAPI_TICKET_URL, - progressText: config.progressText ?? DEFAULT_PROGRESS_TEXT, - progressDelayMs: Math.max(0, Number(config.progressDelayMs ?? DEFAULT_PROGRESS_DELAY_MS)), - statusText: config.statusText || DEFAULT_STATUS_TEXT, - mediaPublicBaseUrl: config.mediaPublicBaseUrl || config.publicBaseUrl, - maxImageBytes: Math.max(1, Number(config.maxImageBytes ?? 10 * 1024 * 1024)), - acceptVoice: config.acceptVoice !== false, - acceptImage: config.acceptImage !== false, - acceptLocation: config.acceptLocation !== false, - acceptLink: config.acceptLink !== false, - asrTarget: config.asrTarget || DEFAULT_ASR_TARGET - }; - let accessTokenCache = { - token: null, - expiresAt: 0 - }; - let jsapiTicketCache = { - ticket: null, - expiresAt: 0 - }; - const fetchForSession = (sessionId, pathname, init) => sessionApiFetch ? sessionApiFetch(sessionId, pathname, init) : apiFetch2(pathname, init); - const buildBindUrl = () => { - if (/^https?:\/\//i.test(config.bindPath)) return config.bindPath; - const path35 = config.bindPath.startsWith("/") ? config.bindPath : `/${config.bindPath}`; - return `${config.publicBaseUrl}${path35}`; - }; - const verifyRequest = (query = {}) => verifyWechatMpSignature({ - token: config.token, - timestamp: String(query.timestamp ?? ""), - nonce: String(query.nonce ?? ""), - signature: String(query.signature ?? "") - }); - const verifyUrlChallenge = (query = {}) => verifyWechatMpUrlChallenge(query, config); - const getStableAccessToken = async () => { - if (accessTokenCache.token && accessTokenCache.expiresAt > Date.now() + 6e4) { - return accessTokenCache.token; - } - const payload = await readJsonResponse2( - await wechatFetch(config.tokenUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - grant_type: "client_credential", - appid: config.appId, - secret: config.appSecret, - force_refresh: false - }) - }) - ); - if (!payload?.access_token) { - throw new Error(payload?.errmsg || "\u83B7\u53D6\u5FAE\u4FE1 access_token \u5931\u8D25"); - } - const expiresIn = Number(payload.expires_in ?? 7200); - accessTokenCache = { - token: payload.access_token, - expiresAt: Date.now() + expiresIn * 1e3 - }; - return accessTokenCache.token; - }; - const getJsapiTicket = async () => { - if (jsapiTicketCache.ticket && jsapiTicketCache.expiresAt > Date.now() + 6e4) { - return jsapiTicketCache.ticket; - } - const accessToken = await getStableAccessToken(); - const url = new URL(config.jsapiTicketUrl); - url.searchParams.set("access_token", accessToken); - url.searchParams.set("type", "jsapi"); - const payload = await readJsonResponse2( - await wechatFetch(url.toString(), { - method: "GET", - headers: { Accept: "application/json" } - }) - ); - if (Number(payload?.errcode ?? 0) !== 0 || !payload?.ticket) { - throw new Error(payload?.errmsg || `\u83B7\u53D6\u5FAE\u4FE1 jsapi_ticket \u5931\u8D25 (${payload?.errcode})`); - } - const expiresIn = Number(payload.expires_in ?? 7200); - jsapiTicketCache = { - ticket: payload.ticket, - expiresAt: Date.now() + expiresIn * 1e3 - }; - return jsapiTicketCache.ticket; - }; - const createJsSdkSignature = async (pageUrl) => { - const normalizedUrl = String(pageUrl ?? "").split("#")[0]; - if (!/^https?:\/\//i.test(normalizedUrl)) { - throw new Error("\u7B7E\u540D URL \u5FC5\u987B\u662F\u5B8C\u6574 http(s) \u5730\u5740"); - } - const ticket = await getJsapiTicket(); - const nonceStr = crypto30.randomBytes(12).toString("hex"); - const timestamp = Math.floor(Date.now() / 1e3); - const signature = crypto30.createHash("sha1").update( - [ - `jsapi_ticket=${ticket}`, - `noncestr=${nonceStr}`, - `timestamp=${timestamp}`, - `url=${normalizedUrl}` - ].join("&") - ).digest("hex"); - return { - appId: config.appId, - timestamp, - nonceStr, - signature, - url: normalizedUrl, - jsApiList: [ - "startRecord", - "stopRecord", - "onVoiceRecordEnd", - "translateVoice", - "updateAppMessageShareData", - "updateTimelineShareData", - "onMenuShareAppMessage", - "onMenuShareTimeline" - ] - }; - }; - const sendCustomerServiceText = async (openid, content, user = null, { verifiedHtmlUrls = [], linkExistsForRequest = linkExists } = {}) => { - const formatted = formatWechatOutboundText(content, user); - const verifiedUrlSet = new Set( - verifiedHtmlUrls.map((url) => String(url ?? "").trim()).filter(Boolean) - ); - const guarded = downgradePrematurePublishClaims( - await guardMissingPublicHtmlLinks(formatted, { - linkExists: async (url) => { - const normalized = String(url ?? "").trim(); - if (verifiedUrlSet.has(normalized) && !linkExistsForRequest(normalized)) { - verifiedUrlSet.delete(normalized); - } - return linkExistsForRequest(url); - } - }) - ); - const chunks = splitWechatText(guarded); - const accessToken = await getStableAccessToken(); - for (const chunk of chunks) { - const payload = await readJsonResponse2( - await wechatFetch( - `${config.customerServiceUrl}?access_token=${encodeURIComponent(accessToken)}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - touser: openid, - msgtype: "text", - text: { - content: chunk - } - }) - } - ) - ); - if (Number(payload?.errcode ?? 0) !== 0) { - const errcode = Number(payload?.errcode ?? 0); - const errmsg = String(payload?.errmsg ?? "").trim() || "unknown_error"; - throw new Error(`\u5FAE\u4FE1\u5BA2\u670D\u6D88\u606F\u53D1\u9001\u5931\u8D25 errcode=${errcode} errmsg=${errmsg}`); - } - } - }; - const sendTextToUser = async (userId, content) => { - const openid = await userAuth2.getWechatOpenidForUser(userId, config.appId); - if (!openid) { - throw new Error("\u7528\u6237\u5C1A\u672A\u7ED1\u5B9A\u670D\u52A1\u53F7\uFF0C\u65E0\u6CD5\u63A8\u9001\u63D0\u9192"); - } - await sendCustomerServiceText(openid, content); - }; - const ensureSessionProvider = async (sessionId) => { - if (!applySessionLlmProvider || !sessionId) return; - const applied = await applySessionLlmProvider(sessionId); - if (applied && applied.ok === false) { - throw new Error(applied.message || "\u4E13\u5C5E Agent Provider \u672A\u914D\u7F6E"); - } - }; - const transcribeWechatVoiceMedia = async (mediaId, format) => { - if (!mediaId) return ""; - const accessToken = await getStableAccessToken(); - const downloaded = await downloadTemporaryMedia(accessToken, mediaId, { wechatFetch }); - if (!downloaded.buffer?.length) return ""; - const extension = String(format ?? "").trim().toLowerCase() || "amr"; - const form = new FormData(); - form.append( - "file", - new Blob([downloaded.buffer], { - type: guessVoiceMimeType(format, downloaded.contentType) - }), - `wechat-voice.${extension}` - ); - const response = await wechatFetch(`${config.asrTarget.replace(/\/$/, "")}/asr/oneshot`, { - method: "POST", - body: form - }); - const { payload, text } = await readJsonBody2(response); - if (!response.ok) { - throw new Error(sanitizeAsrMessage(payload?.message ?? text ?? `ASR HTTP ${response.status}`)); - } - if (Number(payload?.code ?? 200) !== 200) { - throw new Error(sanitizeAsrMessage(payload?.message ?? text ?? "\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5")); - } - return String(payload?.data?.text ?? "").trim(); - }; - const ensureWechatAgentSession = async ({ - userId, - openid, - forceNew = false, - userContext = null - }) => { - if (forceNew) { - await userAuth2.clearWechatAgentRoute(config.appId, openid); - } - const workingDir = await userAuth2.resolveWorkingDir(userId); - const sessionPolicy = await userAuth2.getAgentSessionPolicy(userId); - const publishLayout = await userAuth2.getUserPublishLayout(userId); - const addressName = resolveWechatAddressName(userContext); - const existingRoute = await userAuth2.getWechatAgentRoute(config.appId, openid); - if (existingRoute?.agentSessionId) { - await reconcileAgentSession( - (pathname, init) => fetchForSession(existingRoute.agentSessionId, pathname, init), - existingRoute.agentSessionId, - { - workingDir, - sessionPolicy, - sandboxConstraints: publishLayout?.constraints ?? null, - userContext: publishLayout ? { - userId, - displayName: addressName || publishLayout.displayName, - username: addressName || null, - slug: null - } : null, - tolerateInvalidWorkingDir: true - } - ); - const routeHasTools = await sessionHasRequiredTools( - fetchForSession, - existingRoute.agentSessionId, - sessionPolicy - ).catch(() => false); - if (routeHasTools) { - return existingRoute.agentSessionId; - } - await userAuth2.clearWechatAgentRoute(config.appId, openid); - } else if (existingRoute?.status === "disabled") { - await userAuth2.clearWechatAgentRoute(config.appId, openid); - } - const gate = await userAuth2.canUseChat(userId); - if (!gate.ok) { - throw new Error(gate.message || "\u5F53\u524D\u7528\u6237\u65E0\u6CD5\u4F7F\u7528\u804A\u5929\u80FD\u529B"); - } - const started = startAgentSession ? await startAgentSession({ userId, workingDir, sessionPolicy }) : await readJsonResponse2( - await apiFetch2("/agent/start", { - method: "POST", - body: JSON.stringify({ - working_dir: workingDir, - enable_context_memory: sessionPolicy.enableContextMemory, - ...sessionPolicy.extensionOverrides ? { extension_overrides: sessionPolicy.extensionOverrides } : {} - }) - }) - ); - const sessionId = started?.id; - if (!sessionId) { - throw new Error("\u516C\u4F17\u53F7\u4E13\u5C5E Agent \u4F1A\u8BDD\u521B\u5EFA\u5931\u8D25"); - } - if (!startAgentSession && typeof userAuth2.registerAgentSession === "function") { - await userAuth2.registerAgentSession(userId, sessionId); - } - await reconcileAgentSession( - (pathname, init) => fetchForSession(sessionId, pathname, init), - sessionId, - { - workingDir, - sessionPolicy, - sandboxConstraints: publishLayout?.constraints ?? null, - userContext: publishLayout ? { - userId, - displayName: addressName || publishLayout.displayName, - username: addressName || null, - slug: null - } : null, - tolerateInvalidWorkingDir: true - } - ); - await userAuth2.upsertWechatAgentRoute({ - userId, - appId: config.appId, - openid, - agentSessionId: sessionId - }); - return sessionId; - }; - const refreshWechatSessionSnapshot = async (sessionId, userId) => { - if (!sessionId || !userId || typeof refreshSessionSnapshot !== "function") return; - try { - await refreshSessionSnapshot(sessionId, userId); - } catch (err) { - logger.warn?.("WeChat MP snapshot refresh failed:", err); - } - }; - const rememberWechatUserContext = async (sessionId, user) => { - const addressName = resolveWechatAddressName(user); - if (!addressName) return; - try { - await readJsonResponse2( - await fetchForSession(sessionId, "/agent/harness_remember", { - method: "POST", - body: JSON.stringify({ - sessionId, - title: "\u5FAE\u4FE1\u670D\u52A1\u53F7\u7528\u6237", - content: [ - `\u5F53\u524D\u670D\u52A1\u53F7\u7528\u6237\u540D\u79F0\uFF1A${addressName}`, - "\u8FD9\u662F\u901A\u8FC7\u5FAE\u4FE1 openid \u5728 H5 \u7ED1\u5B9A\u5E93\u4E2D\u67E5\u8BE2\u5230\u7684\u7528\u6237\u540D\u79F0\u3002", - "\u56DE\u590D\u65F6\u53EF\u4EE5\u81EA\u7136\u4F7F\u7528\u8BE5\u540D\u79F0\u79F0\u547C\u7528\u6237\uFF0C\u4E0D\u8981\u4F7F\u7528 openid \u6216 wx_ \u5F00\u5934\u7684\u5185\u90E8\u7528\u6237\u540D\u3002" - ].join("\n") - }) - }) - ); - await readJsonResponse2( - await fetchForSession(sessionId, "/agent/harness_bootstrap", { - method: "POST", - body: JSON.stringify({ sessionId, force: true }) - }) - ); - } catch (err) { - logger.warn?.("WeChat MP user context remember failed:", err); - } - }; - const buildIntentMetadata = (intent) => ({ - source: "wechat_mp", - msgType: intent.msgType, - originalMsgId: intent.msgId || null, - displayText: intent.displayText || "", - mediaPublicUrl: intent.media?.publicUrl || null, - recognition: intent.msgType === "voice" ? intent.agentText || null : null, - location: intent.location || null, - link: intent.link || null - }); - const persistIntentDetail = async ({ intent, userId = null, rawXmlHash = "" }) => { - if (typeof userAuth2.insertWechatMpMessageDetail !== "function") return; - await userAuth2.insertWechatMpMessageDetail({ - appId: config.appId, - openid: intent.openid, - msgId: intent.msgId || null, - userId, - msgType: intent.msgType, - displayText: intent.displayText || "", - agentText: intent.agentText || "", - mediaId: intent.media?.mediaId || null, - mediaUrl: intent.media?.picUrl || null, - mediaPublicUrl: intent.media?.publicUrl || null, - mediaFormat: intent.media?.format || null, - locationLat: intent.location?.latitude ?? null, - locationLng: intent.location?.longitude ?? null, - locationLabel: intent.location?.label || null, - linkUrl: intent.link?.url || null, - linkTitle: intent.link?.title || null, - rawXmlHash, - rawJson: intent.raw - }); - }; - const resolveGrantedSkills = async (userId) => { - try { - const row = await userAuth2.getUserById(userId); - if (!row) return []; - const caps = await userAuth2.resolveUserCapabilities(row); - return Array.isArray(caps?.grantedSkills) ? caps.grantedSkills : []; - } catch (err) { - logger.warn?.("WeChat MP granted skills lookup failed:", err); - return []; - } - }; - const runIntentMessage = async ({ inbound, intent, user }) => { - const resetCandidate = intent.msgType === "text" || intent.msgType === "voice" ? intent.agentText : ""; - const forceNew = isTopicResetIntent(resetCandidate); - let sessionId = await ensureWechatAgentSession({ - userId: user.userId, - openid: inbound.fromUserName, - userContext: user, - forceNew - }); - const publishLayout = await userAuth2.getUserPublishLayout(user.userId); - await ensureSessionProvider(sessionId); - await rememberWechatUserContext(sessionId, user); - let finished = false; - let progressTimer = null; - if (config.progressDelayMs > 0 && config.progressText) { - progressTimer = setTimeout(() => { - if (finished) return; - sendCustomerServiceText(inbound.fromUserName, config.progressText, user).catch((err) => { - logger.warn?.("WeChat MP progress reply failed:", err); - }); - }, config.progressDelayMs); - progressTimer.unref?.(); - } - const requestId = crypto30.randomUUID(); - const guardScheduleReply = async (replyText) => { - if (!scheduleService2 || !looksLikeScheduleConfirmation(replyText)) return replyText; - const sourceMessageId = String(intent.msgId ?? "").trim(); - if (!sourceMessageId || typeof scheduleService2.listItemsBySourceMessage !== "function") { - return replyText; - } - const items = await scheduleService2.listItemsBySourceMessage({ - userId: user.userId, - sourceMessageId, - limit: 5 - }).catch((err) => { - logger.warn?.("Schedule confirmation guard failed:", err); - return []; - }); - if (items.length > 0) return replyText; - return [ - "\u6211\u521A\u624D\u6CA1\u6709\u786E\u8BA4\u5230\u5F85\u529E/\u63D0\u9192\u5DF2\u7ECF\u5199\u5165\u7CFB\u7EDF\uFF0C\u6240\u4EE5\u8FD9\u6B21\u4E0D\u80FD\u7B97\u8BBE\u7F6E\u6210\u529F\u3002", - "\u8BF7\u518D\u53D1\u4E00\u6B21\u5B8C\u6574\u5B89\u6392\uFF0C\u6BD4\u5982\u201C\u660E\u5929\u65E9\u4E0A 6 \u70B9\u8DD1\u6B65\uFF0C5 \u70B9\u534A\u63D0\u9192\u6211\u201D\u3002\u6211\u4F1A\u5728\u5DE5\u5177\u5199\u5165\u6210\u529F\u540E\u518D\u786E\u8BA4\u3002" - ].join("\n"); - }; - try { - const requestStartedAt = Date.now(); - const reply = await executeSessionReply( - (pathname, init) => fetchForSession(sessionId, pathname, init), - sessionId, - requestId, - buildWechatAgentPrompt(intent), - buildIntentMetadata(intent) - ); - const workingDir = publishLayout?.publishDir ?? await userAuth2.resolveWorkingDir(user.userId); - const linkExistsForRequest = resolveLinkExistsForWorkingDir(workingDir, linkExists); - const { - publishedArtifacts, - verifiedArtifacts, - expectedArtifacts, - recentArtifacts, - confirmedArtifacts - } = resolveHtmlPublishArtifacts({ - reply, - intent, - workingDir, - publicBaseUrl: config.publicBaseUrl, - requestStartedAt - }); - const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, { - confirmedArtifacts - }); - if (shouldRetryHtmlGenerationReply({ - reply, - intent, - confirmedArtifacts, - hasValidLinkInReply - }) || expectedArtifacts.length === 0 && recentArtifacts.length === 0 && await isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists: linkExistsForRequest })) { - throw new Error("stale_session_poisoned_completion"); - } - if (reply.tokenState) { - await userAuth2.billSessionUsage(user.userId, sessionId, reply.tokenState, requestId); - } - if (looksLikeHtmlGenerationIntent(intent?.agentText) && verifiedArtifacts.length === 0 && confirmedArtifacts.length === 0) { - throw new Error(buildHtmlPublishFailureText()); - } - const finalizedReply = await maybeAttachPublishedHtmlLink(reply, { - workingDir, - publicBaseUrl: config.publicBaseUrl, - artifacts: verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts - }); - await refreshWechatSessionSnapshot(sessionId, user.userId); - await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, { - verifiedHtmlUrls: (verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts).map( - (artifact) => artifact.url - ), - linkExistsForRequest - }); - return { sessionId }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - const mayBeStaleSession = /403|404|not found|无权访问|session|stale_session_poisoned_completion/i.test(message) && sessionId; - if (mayBeStaleSession) { - sessionId = await ensureWechatAgentSession({ - userId: user.userId, - openid: inbound.fromUserName, - forceNew: true, - userContext: user - }); - await ensureSessionProvider(sessionId); - await rememberWechatUserContext(sessionId, user); - const retryId = crypto30.randomUUID(); - const retryStartedAt = Date.now(); - const reply = await executeSessionReply( - (pathname, init) => fetchForSession(sessionId, pathname, init), - sessionId, - retryId, - buildWechatAgentPrompt(intent), - buildIntentMetadata(intent) - ); - const workingDir = publishLayout?.publishDir ?? await userAuth2.resolveWorkingDir(user.userId); - const linkExistsForRequest = resolveLinkExistsForWorkingDir(workingDir, linkExists); - const { - publishedArtifacts, - verifiedArtifacts, - expectedArtifacts, - recentArtifacts, - confirmedArtifacts - } = resolveHtmlPublishArtifacts({ - reply, - intent, - workingDir, - publicBaseUrl: config.publicBaseUrl, - requestStartedAt: retryStartedAt - }); - const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, { - confirmedArtifacts - }); - if (shouldRetryHtmlGenerationReply({ - reply, - intent, - confirmedArtifacts, - hasValidLinkInReply - }) || expectedArtifacts.length === 0 && recentArtifacts.length === 0 && await isSuspiciousHtmlPublishClaimReply(reply, intent, { linkExists: linkExistsForRequest })) { - throw new Error(buildHtmlPublishFailureText()); - } - if (reply.tokenState) { - await userAuth2.billSessionUsage(user.userId, sessionId, reply.tokenState, retryId); - } - if (looksLikeHtmlGenerationIntent(intent?.agentText) && verifiedArtifacts.length === 0 && confirmedArtifacts.length === 0) { - throw new Error(buildHtmlPublishFailureText()); - } - const finalizedReply = await maybeAttachPublishedHtmlLink(reply, { - workingDir, - publicBaseUrl: config.publicBaseUrl, - artifacts: verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts - }); - await refreshWechatSessionSnapshot(sessionId, user.userId); - await sendCustomerServiceText(inbound.fromUserName, await guardScheduleReply(finalizedReply), user, { - verifiedHtmlUrls: (verifiedArtifacts.length > 0 ? verifiedArtifacts : confirmedArtifacts).map( - (artifact) => artifact.url - ), - linkExistsForRequest - }); - return { sessionId }; - } - throw err; - } finally { - finished = true; - if (progressTimer) clearTimeout(progressTimer); - } - }; - const handleScheduleIntent = async ({ intent, user }) => { - if (!scheduleService2) return null; - const scheduleIntent = parseScheduleIntent(intent.agentText); - if (!isScheduleIntent(scheduleIntent)) return null; - if (scheduleIntent.action === "create_todo") { - if (scheduleIntent.needsClarification?.includes("todo_title")) { - return "\u53EF\u4EE5\u3002\u4F60\u60F3\u8BA9\u6211\u8BB0\u54EA\u4E00\u6761\u5F85\u529E\uFF1F\u4F8B\u5982\u201C\u5E2E\u6211\u8BB0\u4E00\u4E0B \u8DDF\u6BB5\u5403\u996D\u201D\u3002"; - } - const item = await scheduleService2.createItem({ - userId: user.userId, - kind: "task", - title: scheduleIntent.title, - timezone: process.env.H5_DEFAULT_TIMEZONE || "Asia/Shanghai", - sourceChannel: "wechat", - sourceMessageId: intent.msgId || null, - sourceText: intent.agentText, - metadata: { - source: "wechat_mp" - } - }); - return `\u5DF2\u8BB0\u5F55\u5230\u5F85\u529E\u5217\u8868\uFF1A${item.title}\uFF0C\u672A\u8BBE\u7F6E\u63D0\u9192\u3002`; - } - if (scheduleIntent.action === "create_daily_todo_digest") { - if (scheduleIntent.needsClarification?.includes("digest_time")) { - return "\u53EF\u4EE5\u3002\u4F60\u60F3\u6BCF\u5929\u51E0\u70B9\u6536\u5230\u5F53\u5929\u5F85\u529E\u8BB0\u5F55\uFF1F\u6BD4\u5982\u201C\u6BCF\u5929\u65E9\u4E0A 7 \u70B9\u53D1\u7ED9\u6211\u201D\u3002"; - } - const subscription = await scheduleService2.createDailyTodoDigest({ - userId: user.userId, - hour: scheduleIntent.hour, - minute: scheduleIntent.minute, - timezone: process.env.H5_DEFAULT_TIMEZONE || "Asia/Shanghai", - channel: "wechat", - sourceChannel: "wechat", - sourceMessageId: intent.msgId || null, - sourceText: intent.agentText - }); - const minuteText = subscription.minute === 0 ? "" : `${String(subscription.minute).padStart(2, "0")}\u5206`; - return `\u5DF2\u8BBE\u7F6E\uFF1A\u6211\u4F1A\u6BCF\u5929\u65E9\u4E0A ${subscription.hour}\u70B9${minuteText} \u901A\u8FC7\u670D\u52A1\u53F7\u628A\u5F53\u5929\u5F85\u529E\u8BB0\u5F55\u53D1\u7ED9\u4F60\u3002`; - } - if (scheduleIntent.action === "create_balance_alert") { - if (scheduleIntent.needsClarification?.includes("threshold")) { - return "\u53EF\u4EE5\u3002\u4F60\u60F3\u5728\u4F59\u989D\u4F4E\u4E8E\u591A\u5C11\u65F6\u63D0\u9192\u6211\uFF1F\u4F8B\u5982\u201C\u4F59\u989D\u4F4E\u4E8E 20 \u5143\u63D0\u9192\u6211\u201D\u3002"; - } - const subscription = await scheduleService2.createBalanceLowAlert({ - userId: user.userId, - thresholdCents: scheduleIntent.thresholdCents, - channel: "wechat", - sourceChannel: "wechat", - sourceMessageId: intent.msgId || null, - sourceText: intent.agentText - }); - return `\u5DF2\u8BBE\u7F6E\uFF1A\u5F53\u4F59\u989D\u4F4E\u4E8E ${(subscription.thresholdCents / 100).toFixed(2)} \u5143\u65F6\uFF0C\u6211\u4F1A\u901A\u8FC7\u670D\u52A1\u53F7\u63D0\u9192\u4F60\u3002`; - } - if (scheduleIntent.action === "query_schedule") { - const text = await scheduleService2.buildTodoDigestText({ - userId: user.userId, - timezone: process.env.H5_DEFAULT_TIMEZONE || "Asia/Shanghai" - }); - return text; - } - return null; - }; - const handleInboundMessage = async (bodyText, query = {}) => { - if (!verifyRequest(query)) { - return { ok: false, status: 403, body: "invalid signature" }; - } - const inbound = parseWechatMessage(String(bodyText ?? "")); - const rawXmlHash = crypto30.createHash("sha1").update(inbound.rawXml || "").digest("hex"); - const intent = normalizeWechatInboundIntent(inbound); - intent.appId = config.appId; - if (!inbound.fromUserName || !inbound.toUserName || !inbound.msgType) { - return { ok: false, status: 400, body: "invalid xml" }; - } - if (String(query.encrypt_type ?? "").toLowerCase() === "aes" || inbound.encrypt) { - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: "\u5F53\u524D\u516C\u4F17\u53F7\u56DE\u8C03\u9700\u4F7F\u7528\u660E\u6587\u6A21\u5F0F\u63A5\u5165\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5207\u6362\u540E\u518D\u8BD5\u3002" - }) - }; - } - if (inbound.msgType === "event" && inbound.event === "location") { - await persistIntentDetail({ intent, rawXmlHash }); - return successResponse(); - } - if (inbound.msgType === "event" && inbound.event === "subscribe") { - await persistIntentDetail({ intent, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: `${config.unboundTextPrefix} -${buildBindUrl()}` - }) - }; - } - if (inbound.msgType === "event") { - await persistIntentDetail({ intent, rawXmlHash }); - return successResponse(); - } - const supportedByConfig = intent.msgType === "voice" && config.acceptVoice || intent.msgType === "image" && config.acceptImage || intent.msgType === "location" && config.acceptLocation || intent.msgType === "link" && config.acceptLink || intent.msgType === "text" || intent.msgType === "video" || intent.msgType === "shortvideo"; - if (!supportedByConfig) { - await persistIntentDetail({ intent, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: config.unsupportedText - }) - }; - } - const boundUser = await userAuth2.findWechatUserByOpenid(config.appId, inbound.fromUserName); - if (!boundUser) { - await persistIntentDetail({ intent, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: `${config.unboundTextPrefix} -${buildBindUrl()}` - }) - }; - } - if (boundUser.status === "disabled") { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: "\u5F53\u524D\u8D26\u53F7\u4E0D\u53EF\u7528\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5904\u7406\u3002" - }) - }; - } - if (intent.msgType === "voice" && !intent.agentText.trim() && intent.media?.mediaId) { - try { - const fallbackText = await transcribeWechatVoiceMedia(intent.media.mediaId, intent.media.format); - if (fallbackText) { - intent.agentText = fallbackText; - intent.displayText = `\u8BED\u97F3\uFF1A${fallbackText}`; - } - } catch (err) { - logger.warn?.("WeChat MP voice ASR fallback failed:", err); - } - } - if (intent.msgType === "text" && !intent.agentText.trim()) { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: config.unsupportedText - }) - }; - } - if (intent.msgType === "voice" && !intent.agentText.trim()) { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: "\u672A\u8BC6\u522B\u5230\u8BED\u97F3\u6587\u5B57\uFF0C\u8BF7\u518D\u8BF4\u4E00\u6B21\u6216\u8F93\u5165\u6587\u5B57\u3002" - }) - }; - } - if (intent.msgType === "image") { - try { - const accessToken = await getStableAccessToken(); - const persisted = await persistWechatImage( - { - userId: boundUser.userId, - appId: config.appId, - openid: inbound.fromUserName, - msgId: inbound.msgId, - mediaId: inbound.mediaId, - picUrl: inbound.picUrl, - publicBaseUrl: config.mediaPublicBaseUrl, - maxImageBytes: config.maxImageBytes - }, - { - wechatFetch, - accessToken - } - ); - intent.media = { - ...intent.media, - mediaId: inbound.mediaId || intent.media?.mediaId || "", - picUrl: inbound.picUrl || intent.media?.picUrl || "", - publicUrl: persisted.publicUrl, - format: persisted.contentType, - source: persisted.source - }; - intent.agentText = `[\u56FE\u72471]: ${persisted.publicUrl}`; - } catch (error) { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: "\u56FE\u7247\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u76F4\u63A5\u53D1\u9001\u6587\u5B57\u8BF4\u660E\u3002" - }) - }; - } - } - if (intent.msgType === "location" && (intent.location?.latitude == null || intent.location?.longitude == null)) { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: "\u8FD9\u6B21\u4F4D\u7F6E\u5B57\u6BB5\u4E0D\u5B8C\u6574\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u4F4D\u7F6E\u6216\u76F4\u63A5\u8F93\u5165\u5730\u70B9\u3002" - }) - }; - } - if (intent.msgType === "link" && !intent.link?.url) { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: "\u8FD9\u6B21\u6CA1\u6709\u62FF\u5230\u5B8C\u6574\u94FE\u63A5\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u4E00\u6B21\u3002" - }) - }; - } - if (intent.msgType === "video" || intent.msgType === "shortvideo") { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: "\u5DF2\u6536\u5230\u89C6\u9891\uFF0C\u5F53\u524D\u5148\u652F\u6301\u6587\u672C\u3001\u8BED\u97F3\u3001\u56FE\u7247\u3001\u5B9A\u4F4D\u548C\u94FE\u63A5\u3002" - }) - }; - } - if (intent.msgType === "text" && isQuestionStatusProbe(intent.agentText)) { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: buildStatusText(boundUser, config.statusText) - }) - }; - } - if (intent.msgType === "text" && isSimpleGreeting(intent.agentText)) { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: buildGreetingText(boundUser) - }) - }; - } - if ((intent.msgType === "text" || intent.msgType === "voice") && isConnectivityTest(intent.agentText)) { - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: buildConnectivityTestReply(boundUser) - }) - }; - } - if (inbound.msgId && typeof userAuth2.recordWechatMpMessage === "function") { - const recorded = await userAuth2.recordWechatMpMessage({ - appId: config.appId, - openid: inbound.fromUserName, - msgId: inbound.msgId - }); - if (!recorded?.inserted) { - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: buildStatusText(boundUser, config.statusText) - }) - }; - } - } - await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash }); - const scheduleReply = intent.msgType === "text" || intent.msgType === "voice" ? await handleScheduleIntent({ intent, user: boundUser }) : null; - if (scheduleReply) { - if (inbound.msgId && typeof userAuth2.finishWechatMpMessage === "function") { - await userAuth2.finishWechatMpMessage({ - appId: config.appId, - openid: inbound.fromUserName, - msgId: inbound.msgId, - status: "done", - agentSessionId: null - }); - } - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: scheduleReply - }) - }; - } - const task = runIntentMessage({ - inbound, - intent, - user: boundUser - }).then(async ({ sessionId } = {}) => { - if (inbound.msgId && typeof userAuth2.finishWechatMpMessage === "function") { - await userAuth2.finishWechatMpMessage({ - appId: config.appId, - openid: inbound.fromUserName, - msgId: inbound.msgId, - status: "done", - agentSessionId: sessionId - }); - } - }).catch(async (err) => { - if (inbound.msgId && typeof userAuth2.finishWechatMpMessage === "function") { - await userAuth2.finishWechatMpMessage({ - appId: config.appId, - openid: inbound.fromUserName, - msgId: inbound.msgId, - status: "failed" - }).catch(() => { - }); - } - logger.error?.("WeChat MP background reply failed:", err); - return sendCustomerServiceText( - inbound.fromUserName, - `\u8FD9\u6B21\u8F6C\u53D1\u5230\u4E13\u5C5E Agent \u5931\u8D25\u4E86\uFF1A${err instanceof Error ? err.message : String(err)}`, - boundUser - ).catch(() => { - }); - }); - return { - ok: true, - status: 200, - contentType: "application/xml; charset=utf-8", - body: buildWechatTextReply({ - toUserName: inbound.fromUserName, - fromUserName: inbound.toUserName, - content: buildAckText({ - intent, - nickname: resolveWechatAddressName(boundUser), - config, - fallbackText: config.ackText - }) - }), - task - }; - }; - const getRouteStatusForUser = async (userId) => { - const openid = await userAuth2.getWechatOpenidForUser(userId, config.appId); - if (!openid) { - return { - enabled: true, - bound: false, - appId: config.appId, - openid: null, - agentSessionId: null, - routeStatus: null, - updatedAt: null - }; - } - const route = await userAuth2.getWechatAgentRoute(config.appId, openid); - return { - enabled: true, - bound: true, - appId: config.appId, - openid, - agentSessionId: route?.agentSessionId ?? null, - routeStatus: route?.status ?? null, - updatedAt: route?.updatedAt ?? null - }; - }; - const recreateRouteForUser = async (userId) => { - const openid = await userAuth2.getWechatOpenidForUser(userId, config.appId); - if (!openid) { - return { - ok: false, - message: "\u5F53\u524D\u8D26\u53F7\u5C1A\u672A\u7ED1\u5B9A\u8BE5\u670D\u52A1\u53F7" - }; - } - const sessionId = await ensureWechatAgentSession({ - userId, - openid, - forceNew: true - }); - const route = await userAuth2.getWechatAgentRoute(config.appId, openid); - return { - ok: true, - route: { - enabled: true, - bound: true, - appId: config.appId, - openid, - agentSessionId: sessionId, - routeStatus: route?.status ?? "active", - updatedAt: route?.updatedAt ?? null - } - }; - }; - return { - enabled: true, - verifyRequest, - verifyUrlChallenge, - handleInboundMessage, - getRouteStatusForUser, - recreateRouteForUser, - createJsSdkSignature, - sendTextToUser - }; -} - -// wechat-share.mjs -function validateWechatShareSignatureUrl(pageUrl, { publicBaseUrl = "", requestHost = "" } = {}) { - const normalizedUrl = String(pageUrl ?? "").split("#")[0].trim(); - if (!normalizedUrl) { - throw Object.assign(new Error("\u7F3A\u5C11 url"), { code: "missing_url" }); - } - const target = new URL(normalizedUrl); - if (!/^https?:$/.test(target.protocol)) { - throw Object.assign(new Error("url \u5FC5\u987B\u662F http(s) \u5730\u5740"), { code: "invalid_url" }); - } - const publicHost = publicBaseUrl ? new URL(publicBaseUrl).host : null; - const allowedRequestHost = String(requestHost ?? "").split(",")[0].trim(); - if (publicHost && target.host !== publicHost && target.host !== allowedRequestHost) { - throw Object.assign(new Error("url \u4E0D\u5C5E\u4E8E\u5F53\u524D H5 \u57DF\u540D"), { code: "host_not_allowed" }); - } - return target.toString(); -} - -// schedule-service.mjs -import crypto31 from "node:crypto"; - -// schedule-time.mjs -var DEFAULT_TIMEZONE2 = "Asia/Shanghai"; -function pad2(value) { - return String(value).padStart(2, "0"); -} -function normalizeTimezone(timezone) { - return String(timezone || process.env.H5_DEFAULT_TIMEZONE || DEFAULT_TIMEZONE2).trim() || DEFAULT_TIMEZONE2; -} -function getLocalParts(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE2) { - 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_TIMEZONE2) { - const parts = getLocalParts(epochMs, timezone); - return `${parts.month}\u6708${parts.day}\u65E5`; -} -function startOfLocalDay(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE2) { - 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_TIMEZONE2) { - 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_TIMEZONE2) { - 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_TIMEZONE2, 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_TIMEZONE2) { - const parts = getLocalParts(epochMs, timezone); - return `${pad2(parts.hour)}:${pad2(parts.minute)}`; -} - -// schedule-service.mjs -var DEFAULT_TIMEZONE3 = "Asia/Shanghai"; -function nowMs2() { - 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_TIMEZONE3, - 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_TIMEZONE3, - 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_TIMEZONE3, - 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 parseJsonColumn3(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: parseJsonColumn3(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 = nowMs2(), timezone = DEFAULT_TIMEZONE3 } = {}) { - 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(pool2, options = {}) { - const defaultTimezone = normalizeTimezone(options.defaultTimezone || process.env.H5_DEFAULT_TIMEZONE); - const clock = options.clock || { now: nowMs2 }; - 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 = crypto31.randomUUID(); - const ts = clock.now(); - await pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 = crypto31.randomUUID(); - const ts = clock.now(); - await pool2.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 pool2.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 pool2.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 = crypto31.randomUUID(); - const ts = clock.now(); - await pool2.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 pool2.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 = crypto31.randomUUID(); - await pool2.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 pool2.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 pool2.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 pool2.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 pool2.query( - `SELECT * FROM h5_schedule_reminders WHERE id = ? LIMIT 1`, - [id] - ); - return rowToReminder(rows[0]); - }; - const markReminderSent = async (reminder, { now = clock.now() } = {}) => { - await pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.query( - `SELECT * FROM h5_balance_alert_subscriptions WHERE id = ? LIMIT 1`, - [id] - ); - return rowToBalanceAlert(rows[0]); - }; - const markBalanceAlertSent = async (subscription, { now = clock.now() } = {}) => { - await pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - crypto31.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 pool2.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 = crypto31.randomUUID(); - await pool2.query( - `INSERT INTO h5_user_notifications - (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at) - 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 pool2.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 pool2.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 pool2.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 pool2.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 pool2.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 - }; -} - -// user-feedback.mjs -import crypto32 from "node:crypto"; -var FEEDBACK_TYPES = /* @__PURE__ */ new Set(["bug", "feature", "other"]); -var FEEDBACK_MAX_IMAGES = 5; -var FEEDBACK_MAX_TITLE = 120; -var FEEDBACK_MAX_DESCRIPTION = 5e3; -var FEEDBACK_MAX_CONTACT = 120; -var FEEDBACK_MAX_IMAGE_BYTES = Math.floor(1.5 * 1024 * 1024); -function feedbackError(message, code = "invalid_input") { - const err = new Error(message); - err.code = code; - return err; -} -function normalizeType(value) { - const type = String(value ?? "").trim().toLowerCase(); - if (!FEEDBACK_TYPES.has(type)) { - throw feedbackError("\u8BF7\u9009\u62E9\u53CD\u9988\u7C7B\u578B"); - } - return type; -} -function normalizeImages(images) { - if (!Array.isArray(images)) return []; - if (images.length > FEEDBACK_MAX_IMAGES) { - throw feedbackError(`\u6700\u591A\u4E0A\u4F20 ${FEEDBACK_MAX_IMAGES} \u5F20\u56FE\u7247`); - } - return images.map((item, index) => { - const mimeType = String(item?.mimeType ?? item?.mime_type ?? "").trim().toLowerCase(); - const dataBase64 = String(item?.dataBase64 ?? item?.data_base64 ?? "").trim(); - const filename = String(item?.filename ?? item?.name ?? `screenshot-${index + 1}.jpg`).trim(); - if (!mimeType.startsWith("image/")) { - throw feedbackError("\u4EC5\u652F\u6301\u56FE\u7247\u9644\u4EF6"); - } - if (!dataBase64) { - throw feedbackError("\u56FE\u7247\u6570\u636E\u65E0\u6548"); - } - let buffer; - try { - buffer = Buffer.from(dataBase64, "base64"); - } catch { - throw feedbackError("\u56FE\u7247\u6570\u636E\u65E0\u6548"); - } - if (!buffer.length) { - throw feedbackError("\u56FE\u7247\u6570\u636E\u65E0\u6548"); - } - if (buffer.length > FEEDBACK_MAX_IMAGE_BYTES) { - throw feedbackError("\u5355\u5F20\u56FE\u7247\u4E0D\u80FD\u8D85\u8FC7 1.5MB"); - } - return { - filename: filename.slice(0, 120), - mimeType, - sizeBytes: buffer.length, - dataBase64 - }; - }); -} -function normalizeContext(context) { - if (!context || typeof context !== "object") return {}; - const pageUrl = String(context.pageUrl ?? context.page_url ?? "").trim().slice(0, 500); - const pagePath = String(context.pagePath ?? context.page_path ?? "").trim().slice(0, 300); - const userAgent = String(context.userAgent ?? context.user_agent ?? "").trim().slice(0, 500); - const viewport = String(context.viewport ?? "").trim().slice(0, 64); - const sessionId = String(context.sessionId ?? context.session_id ?? "").trim().slice(0, 128); - const appVersion = String(context.appVersion ?? context.app_version ?? "").trim().slice(0, 64); - const normalized = {}; - if (pageUrl) normalized.pageUrl = pageUrl; - if (pagePath) normalized.pagePath = pagePath; - if (userAgent) normalized.userAgent = userAgent; - if (viewport) normalized.viewport = viewport; - if (sessionId) normalized.sessionId = sessionId; - if (appVersion) normalized.appVersion = appVersion; - return normalized; -} -function rowToFeedback(row, { includeImages = true, includeContact = true } = {}) { - if (!row) return null; - let images = []; - let context = {}; - try { - images = row.images_json ? JSON.parse(row.images_json) : []; - } catch { - images = []; - } - try { - context = row.context_json ? JSON.parse(row.context_json) : {}; - } catch { - context = {}; - } - const item = { - id: row.id, - userId: row.user_id, - type: row.type, - title: row.title, - description: row.description, - status: row.status, - context, - createdAt: Number(row.created_at), - updatedAt: Number(row.updated_at), - submitterDisplayName: row.submitter_display_name ?? null, - imageCount: images.length - }; - if (includeContact) { - item.contact = row.contact ?? null; - } - if (includeImages) { - item.images = images; - } - return item; -} -function createFeedbackService(pool2) { - return { - async submit(userId, input = {}) { - const type = normalizeType(input.type); - const title = String(input.title ?? "").trim(); - const description = String(input.description ?? "").trim(); - const contact = String(input.contact ?? "").trim().slice(0, FEEDBACK_MAX_CONTACT); - if (!title) throw feedbackError("\u8BF7\u586B\u5199\u6807\u9898"); - if (title.length > FEEDBACK_MAX_TITLE) throw feedbackError(`\u6807\u9898\u4E0D\u80FD\u8D85\u8FC7 ${FEEDBACK_MAX_TITLE} \u5B57`); - if (!description) throw feedbackError("\u8BF7\u586B\u5199\u8BE6\u7EC6\u63CF\u8FF0"); - if (description.length > FEEDBACK_MAX_DESCRIPTION) { - throw feedbackError(`\u63CF\u8FF0\u4E0D\u80FD\u8D85\u8FC7 ${FEEDBACK_MAX_DESCRIPTION} \u5B57`); - } - const images = normalizeImages(input.images); - const context = normalizeContext(input.context); - const now = Date.now(); - const id = crypto32.randomUUID(); - await pool2.query( - `INSERT INTO h5_user_feedback - (id, user_id, type, title, description, contact, status, images_json, context_json, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?)`, - [ - id, - userId, - type, - title, - description, - contact || null, - JSON.stringify(images), - JSON.stringify(context), - now, - now - ] - ); - return { - id, - type, - title, - status: "pending", - createdAt: now - }; - }, - async listForUser(userId, { limit = 20 } = {}) { - const safeLimit = Math.min(Math.max(Number(limit) || 20, 1), 50); - const [rows] = await pool2.query( - `SELECT f.id, f.user_id, f.type, f.title, f.description, f.contact, f.status, - f.images_json, f.context_json, f.created_at, f.updated_at, - u.display_name AS submitter_display_name - FROM h5_user_feedback f - JOIN h5_users u ON u.id = f.user_id - WHERE f.user_id = ? - ORDER BY f.created_at DESC - LIMIT ?`, - [userId, safeLimit] - ); - return rows.map((row) => rowToFeedback(row)); - }, - async listAll({ page = 1, limit = 10 } = {}) { - const safeLimit = Math.min(Math.max(Number(limit) || 10, 1), 10); - const safePage = Math.max(Number(page) || 1, 1); - const offset = (safePage - 1) * safeLimit; - const [[countRow]] = await pool2.query(`SELECT COUNT(*) AS total FROM h5_user_feedback`); - const total = Number(countRow?.total ?? 0); - const totalPages = total === 0 ? 0 : Math.ceil(total / safeLimit); - const [rows] = await pool2.query( - `SELECT f.id, f.user_id, f.type, f.title, f.description, f.status, - f.images_json, f.context_json, f.created_at, f.updated_at, - u.display_name AS submitter_display_name - FROM h5_user_feedback f - JOIN h5_users u ON u.id = f.user_id - ORDER BY f.created_at DESC - LIMIT ? OFFSET ?`, - [safeLimit, offset] - ); - return { - items: rows.map( - (row) => rowToFeedback(row, { includeImages: false, includeContact: false }) - ), - page: safePage, - pageSize: safeLimit, - total, - totalPages - }; - }, - async getById(feedbackId, viewerUserId) { - const id = String(feedbackId ?? "").trim(); - if (!id) throw feedbackError("\u53CD\u9988\u4E0D\u5B58\u5728", "feedback_not_found"); - const [rows] = await pool2.query( - `SELECT f.id, f.user_id, f.type, f.title, f.description, f.contact, f.status, - f.images_json, f.context_json, f.created_at, f.updated_at, - u.display_name AS submitter_display_name - FROM h5_user_feedback f - JOIN h5_users u ON u.id = f.user_id - WHERE f.id = ? - LIMIT 1`, - [id] - ); - const row = rows[0]; - if (!row) throw feedbackError("\u53CD\u9988\u4E0D\u5B58\u5728", "feedback_not_found"); - const isOwner = viewerUserId && row.user_id === viewerUserId; - return rowToFeedback(row, { includeImages: true, includeContact: isOwner }); - } - }; -} - -// schedule-reminder-worker.mjs -function startScheduleReminderWorker({ - scheduleService: scheduleService2, - sendWechatTextToUser, - notificationDispatcher: notificationDispatcher2, - logger = console, - intervalMs = Number(process.env.H5_REMINDER_SCAN_INTERVAL_MS ?? 3e4), - maxAttempts = Number(process.env.H5_REMINDER_MAX_ATTEMPTS ?? 5), - runOnStart = true -} = {}) { - const sendScheduleNotification = notificationDispatcher2?.sendScheduleNotification?.bind(notificationDispatcher2) ?? (typeof sendWechatTextToUser === "function" ? async ({ userId, text }) => sendWechatTextToUser(userId, text) : null); - if (!scheduleService2 || typeof sendScheduleNotification !== "function") { - return { stop() { - } }; - } - let stopped = false; - let running = false; - const runOnce = async () => { - if (running || stopped) return; - running = true; - try { - const dueReminders = await scheduleService2.listDueReminders({ limit: 50 }); - for (const candidate of dueReminders) { - const reminder = await scheduleService2.lockReminder(candidate.id); - if (!reminder) continue; - try { - const text = await scheduleService2.buildReminderText(reminder); - if (!text) { - await scheduleService2.markReminderCancelled(reminder, "\u4E8B\u9879\u5DF2\u5931\u6548\u6216\u4E0D\u5B58\u5728"); - continue; - } - await scheduleService2.createUserNotification?.({ - userId: reminder.userId, - channel: "web", - notificationType: "schedule_reminder", - title: "\u5F85\u529E\u63D0\u9192", - body: text, - data: { - reminderId: reminder.id, - itemId: reminder.itemId - } - }); - if (reminder.channel === "wechat") { - await sendScheduleNotification({ userId: reminder.userId, text }); - } - await scheduleService2.logDelivery({ - reminderId: reminder.id, - userId: reminder.userId, - channel: reminder.channel, - status: "success" - }); - await scheduleService2.markReminderSent(reminder); - } catch (err) { - logger.warn?.("Schedule reminder delivery failed:", err); - await scheduleService2.logDelivery({ - reminderId: reminder.id, - userId: reminder.userId, - channel: reminder.channel, - status: "failed", - errorMessage: err instanceof Error ? err.message : String(err) - }).catch(() => { - }); - await scheduleService2.markReminderFailed(reminder, err, { maxAttempts }); - } - } - const due = await scheduleService2.listDueDigestSubscriptions({ limit: 50 }); - for (const candidate of due) { - const subscription = await scheduleService2.lockDigestSubscription(candidate.id); - if (!subscription) continue; - try { - const text = await scheduleService2.buildTodoDigestText({ - userId: subscription.userId, - timezone: subscription.timezone - }); - await scheduleService2.createUserNotification?.({ - userId: subscription.userId, - channel: "web", - notificationType: "todo_digest", - title: "\u4ECA\u65E5\u5F85\u529E\u6458\u8981", - body: text, - data: { - subscriptionId: subscription.id, - timezone: subscription.timezone - } - }); - await sendScheduleNotification({ userId: subscription.userId, text }); - await scheduleService2.logDelivery({ - subscriptionId: subscription.id, - userId: subscription.userId, - channel: subscription.channel, - status: "success" - }); - await scheduleService2.markDigestSent(subscription); - } catch (err) { - logger.warn?.("Schedule digest delivery failed:", err); - await scheduleService2.logDelivery({ - subscriptionId: subscription.id, - userId: subscription.userId, - channel: subscription.channel, - status: "failed", - errorMessage: err instanceof Error ? err.message : String(err) - }).catch(() => { - }); - await scheduleService2.markDigestFailed(subscription, err, { maxAttempts }); - } - } - const dueBalance = await scheduleService2.listDueBalanceAlerts({ limit: 50 }); - for (const candidate of dueBalance) { - const subscription = await scheduleService2.lockBalanceAlert(candidate.id); - if (!subscription) continue; - try { - const user = await scheduleService2.getUserWalletSnapshot?.(subscription.userId); - const balanceCents = Number(user?.balanceCents ?? user?.balance_cents ?? 0); - if (balanceCents > subscription.thresholdCents) { - await scheduleService2.markBalanceAlertSent({ - ...subscription, - lastNotifiedBalanceCents: balanceCents - }); - continue; - } - const yuan = (subscription.thresholdCents / 100).toFixed(2); - const text = `\u4F59\u989D\u4E0D\u8DB3\u63D0\u9192 - -\u4F60\u7684\u8D26\u6237\u4F59\u989D\u5DF2\u4F4E\u4E8E ${yuan} \u5143\uFF0C\u8BF7\u53CA\u65F6\u5145\u503C\u3002`; - await scheduleService2.createUserNotification?.({ - userId: subscription.userId, - channel: "web", - notificationType: "balance_low", - title: "\u4F59\u989D\u4E0D\u8DB3\u63D0\u9192", - body: text, - data: { - thresholdCents: subscription.thresholdCents, - balanceCents - } - }); - await sendScheduleNotification({ userId: subscription.userId, text }); - await scheduleService2.logDelivery({ - subscriptionId: subscription.id, - userId: subscription.userId, - channel: subscription.channel, - status: "success" - }); - await scheduleService2.markBalanceAlertSent({ - ...subscription, - lastNotifiedBalanceCents: balanceCents - }); - } catch (err) { - logger.warn?.("Balance alert delivery failed:", err); - await scheduleService2.logDelivery({ - subscriptionId: subscription.id, - userId: subscription.userId, - channel: subscription.channel, - status: "failed", - errorMessage: err instanceof Error ? err.message : String(err) - }).catch(() => { - }); - await scheduleService2.markBalanceAlertFailed(subscription, err, { maxAttempts }); - } - } - } catch (err) { - logger.warn?.("Schedule reminder worker failed:", err); - } finally { - running = false; - } - }; - const timer = setInterval(runOnce, Math.max(1e3, Number(intervalMs) || 3e4)); - timer.unref?.(); - if (runOnStart) void runOnce(); - return { - runOnce, - stop() { - stopped = true; - clearInterval(timer); - } - }; -} - -// conversation-display.mjs -var TASK_ROUTING_HINT_RE = /^【TKMind 路由提示】[\s\S]*?\n\n/; -var MINDSPACE_CONTEXT_RE = /^\[MindSpace 上下文\][\s\S]*?\n\n/; -var USER_IDENTITY_BLOCK_RE = /^\[用户身份\][\s\S]*?\n\n/; -function stripUserIdentityPrefix(text) { - return String(text ?? "").replace(USER_IDENTITY_BLOCK_RE, "").trimStart(); -} -function stripTaskRoutingHint(text) { - return String(text ?? "").replace(TASK_ROUTING_HINT_RE, "").trimStart(); -} -function stripMindSpaceContextPrefix(text) { - let next = String(text ?? ""); - while (MINDSPACE_CONTEXT_RE.test(next)) { - next = next.replace(MINDSPACE_CONTEXT_RE, "").trimStart(); - } - return next; -} -function deriveUserFacingText(text) { - let next = String(text ?? ""); - next = stripUserIdentityPrefix(next); - next = stripTaskRoutingHint(next); - next = stripMindSpaceContextPrefix(next); - next = stripKnownChatSkillPrompt(next); - return next.trim(); -} - -// session-snapshot.mjs -var DERIVED_TITLE_MAX_LEN = 40; -function isGeneratedSessionName(name) { - return /^20\d{6}(?:[_-]\d+)?$/.test(String(name ?? "").trim()); -} -function isDefaultSessionName(name) { - const trimmed = String(name ?? "").trim(); - return !trimmed || trimmed === "New Chat" || trimmed === "\u65B0\u5BF9\u8BDD" || isGeneratedSessionName(trimmed); -} -function messageSnippet(message) { - const fromContent = (message?.content ?? []).filter((item) => item?.type === "text" && typeof item.text === "string").map((item) => item.text).join("").trim(); - if (message?.role === "user") { - return deriveUserFacingText(fromContent); - } - if (fromContent) return fromContent; - const displayText = message?.metadata?.displayText; - return typeof displayText === "string" ? displayText.trim() : ""; -} -function deriveTitleFromMessages(messages) { - if (!Array.isArray(messages)) return ""; - const firstUser = messages.find((m) => m?.role === "user" && messageSnippet(m)); - const text = (firstUser ? messageSnippet(firstUser) : "").replace(/\s+/g, " ").trim(); - if (!text) return ""; - return text.length > DERIVED_TITLE_MAX_LEN ? `${text.slice(0, DERIVED_TITLE_MAX_LEN)}\u2026` : text; -} -function resolveDisplayTitle(session, messages) { - if (session?.user_set_name) { - const explicit = String(session.name ?? "").trim(); - if (explicit) return explicit; - } - const recipeTitle = String(session?.recipe?.title ?? "").trim(); - if (recipeTitle) return recipeTitle; - const sessionName = String(session?.name ?? "").trim(); - if (sessionName && !isDefaultSessionName(sessionName)) return sessionName; - return deriveTitleFromMessages(messages); -} -function createSessionSnapshotService(pool2, options = {}) { - const conversationMemoryService2 = options.conversationMemoryService ?? null; - function isEnabled() { - return process.env.SESSION_SNAPSHOT_CACHE_ENABLED !== "0"; - } - async function persistDisplayTitle(sessionId, title) { - const normalized = String(title ?? "").trim(); - if (!normalized || !pool2) return; - try { - await pool2.query( - `UPDATE h5_session_snapshots - SET display_title = ? - WHERE agent_session_id = ? - AND (display_title IS NULL OR display_title = '')`, - [normalized, sessionId] - ); - } catch (err) { - console.warn("[snapshot] persistDisplayTitle failed:", err instanceof Error ? err.message : err); - } - } - async function save(sessionId, userId, session, messages) { - if (!isEnabled() || !pool2) return; - try { - const now = Date.now(); - const sessionMeta = { - name: session.name ?? "", - display_title: resolveDisplayTitle(session, messages), - working_dir: session.working_dir ?? "", - created_at_str: session.created_at ?? "", - updated_at_str: session.updated_at ?? "", - user_set_name: session.user_set_name ? 1 : 0, - recipe_json: session.recipe != null ? JSON.stringify(session.recipe) : null - }; - await pool2.query( - `INSERT INTO h5_session_snapshots - (agent_session_id, user_id, name, display_title, working_dir, - created_at_str, updated_at_str, user_set_name, recipe_json, - synced_msg_count, source_updated_at, - messages_json, synced_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - display_title = VALUES(display_title), - working_dir = VALUES(working_dir), - created_at_str = VALUES(created_at_str), - updated_at_str = VALUES(updated_at_str), - user_set_name = VALUES(user_set_name), - recipe_json = VALUES(recipe_json), - synced_msg_count = VALUES(synced_msg_count), - source_updated_at = VALUES(source_updated_at), - messages_json = VALUES(messages_json), - synced_at = VALUES(synced_at)`, - [ - sessionId, - userId, - sessionMeta.name, - sessionMeta.display_title, - sessionMeta.working_dir, - sessionMeta.created_at_str, - sessionMeta.updated_at_str, - sessionMeta.user_set_name, - sessionMeta.recipe_json, - session.message_count ?? 0, - session.updated_at ?? "", - JSON.stringify(messages), - now - ] - ); - if (conversationMemoryService2?.isEnabled?.()) { - void conversationMemoryService2.saveAndAnalyze(sessionId, userId, messages).catch((err) => { - console.warn( - "[snapshot] conversation memory sync failed:", - err instanceof Error ? err.message : err - ); - }); - } - } catch (err) { - console.warn("[snapshot] save failed:", err instanceof Error ? err.message : err); - } - } - async function get(sessionId) { - if (!isEnabled() || !pool2) return null; - try { - const [rows] = await pool2.query( - `SELECT agent_session_id, user_id, name, display_title, working_dir, - created_at_str, updated_at_str, user_set_name, recipe_json, - synced_msg_count, source_updated_at, messages_json, synced_at - FROM h5_session_snapshots - WHERE agent_session_id = ? - LIMIT 1`, - [sessionId] - ); - if (!rows.length) return null; - const row = rows[0]; - const parsedMessages = JSON.parse(row.messages_json); - const storedDisplayTitle = String(row.display_title ?? "").trim(); - const rawName = String(row.name ?? "").trim(); - const fallbackDisplayTitle = storedDisplayTitle || resolveDisplayTitle( - { - name: row.name, - recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null, - user_set_name: row.user_set_name === 1 - }, - parsedMessages - ); - if (!storedDisplayTitle && fallbackDisplayTitle) { - void persistDisplayTitle(sessionId, fallbackDisplayTitle); - } - return { - session: { - id: sessionId, - name: fallbackDisplayTitle || rawName, - working_dir: row.working_dir, - message_count: row.synced_msg_count, - created_at: row.created_at_str || void 0, - updated_at: row.updated_at_str || void 0, - user_set_name: row.user_set_name === 1, - recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null, - conversation: null - }, - messages: parsedMessages, - meta: { - synced_msg_count: row.synced_msg_count, - source_updated_at: row.source_updated_at, - synced_at: row.synced_at - } - }; - } catch (err) { - console.warn("[snapshot] get failed:", err instanceof Error ? err.message : err); - return null; - } - } - async function remove(sessionId) { - if (!pool2) return; - try { - await pool2.query( - `DELETE FROM h5_session_snapshots WHERE agent_session_id = ?`, - [sessionId] - ); - } catch (err) { - console.warn("[snapshot] remove failed:", err instanceof Error ? err.message : err); - } - } - async function refresh(sessionId, userId, apiFetchFn) { - if (!isEnabled() || !pool2) return; - try { - const res = await apiFetchFn(`/sessions/${encodeURIComponent(sessionId)}`, { - method: "GET" - }); - if (!res.ok) return; - const gooseSession = await res.json(); - const messages = (gooseSession.conversation ?? []).filter((m) => m.metadata?.userVisible); - await save(sessionId, userId, gooseSession, messages); - } catch (err) { - console.warn("[snapshot] refresh failed:", sessionId, err instanceof Error ? err.message : err); - } - } - async function getDerivedTitles(sessionIds) { - if (!isEnabled() || !pool2) return /* @__PURE__ */ new Map(); - const ids = [...new Set((sessionIds ?? []).filter(Boolean))]; - if (ids.length === 0) return /* @__PURE__ */ new Map(); - const titles = /* @__PURE__ */ new Map(); - try { - const placeholders = ids.map(() => "?").join(", "); - const [rows] = await pool2.query( - `SELECT agent_session_id, display_title, name, recipe_json, user_set_name, messages_json - FROM h5_session_snapshots - WHERE agent_session_id IN (${placeholders})`, - ids - ); - for (const row of rows) { - try { - const parsedMessages = JSON.parse(row.messages_json); - const title = String(row.display_title ?? "").trim() || resolveDisplayTitle( - { - name: row.name, - recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null, - user_set_name: row.user_set_name === 1 - }, - parsedMessages - ); - if (title) { - titles.set(row.agent_session_id, title); - if (!String(row.display_title ?? "").trim()) { - void persistDisplayTitle(row.agent_session_id, title); - } - } - } catch { - } - } - } catch (err) { - console.warn("[snapshot] getDerivedTitles failed:", err instanceof Error ? err.message : err); - } - return titles; - } - async function getHistorySummaries(sessionIds) { - if (!isEnabled() || !pool2) return /* @__PURE__ */ new Map(); - const ids = [...new Set((sessionIds ?? []).filter(Boolean))]; - if (ids.length === 0) return /* @__PURE__ */ new Map(); - const summaries = /* @__PURE__ */ new Map(); - try { - const placeholders = ids.map(() => "?").join(", "); - const [rows] = await pool2.query( - `SELECT agent_session_id, display_title, name, recipe_json, user_set_name, - synced_msg_count, messages_json - FROM h5_session_snapshots - WHERE agent_session_id IN (${placeholders})`, - ids - ); - for (const row of rows) { - try { - const parsedMessages = JSON.parse(row.messages_json); - const title = String(row.display_title ?? "").trim() || resolveDisplayTitle( - { - name: row.name, - recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null, - user_set_name: row.user_set_name === 1 - }, - parsedMessages - ); - const messageCount = Number(row.synced_msg_count ?? 0); - summaries.set(row.agent_session_id, { - title, - messageCount - }); - if (title && !String(row.display_title ?? "").trim()) { - void persistDisplayTitle(row.agent_session_id, title); - } - } catch { - } - } - } catch (err) { - console.warn("[snapshot] getHistorySummaries failed:", err instanceof Error ? err.message : err); - } - return summaries; - } - return { save, get, remove, refresh, getDerivedTitles, getHistorySummaries, isEnabled }; -} - -// conversation-memory.mjs -import crypto33 from "node:crypto"; -import { fetch as undiciFetch8 } from "undici"; -import { jsonrepair as jsonrepair3 } from "jsonrepair"; -var MAX_MESSAGE_TEXT = 12e3; -var MAX_ANALYSIS_MESSAGES = 8; -var MAX_MEMORY_TEXT = 1e3; -var LABELS = /* @__PURE__ */ new Set([ - "preference", - "habit", - "interest", - "goal", - "fact", - "experience", - "knowledge" -]); -function resolveEncryptionKey3(explicitKey) { - return explicitKey ?? process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY ?? "local-dev-secret"; -} -function stableId(...parts) { - return crypto33.createHash("sha256").update(parts.map((part) => String(part ?? "")).join("\0")).digest("hex"); -} -function parseJsonObject3(text) { - const source = String(text ?? "").trim(); - if (!source) return null; - try { - return JSON.parse(source); - } catch { - try { - return JSON.parse(jsonrepair3(source)); - } catch { - return null; - } - } -} -function extractJsonObject3(text) { - const direct = parseJsonObject3(text); - if (direct && typeof direct === "object") return direct; - const source = String(text ?? "").trim(); - const start = source.indexOf("{"); - const end = source.lastIndexOf("}"); - if (start < 0 || end <= start) return null; - return parseJsonObject3(source.slice(start, end + 1)); -} -function extractConversationMessageText(message) { - const content = Array.isArray(message?.content) ? message.content : []; - const chunks = []; - for (const item of content) { - if (item?.type === "text" && typeof item.text === "string") chunks.push(item.text); - if (item?.type === "image" && item.url) chunks.push(`[image] ${item.url}`); - } - const fromContent = chunks.join("\n").trim(); - if (fromContent) return fromContent.slice(0, MAX_MESSAGE_TEXT); - const displayText = message?.metadata?.displayText; - return typeof displayText === "string" ? displayText.trim().slice(0, MAX_MESSAGE_TEXT) : ""; -} -function normalizeMessage(sessionId, userId, message, index, now) { - const role = (String(message?.role ?? "").trim() || "unknown").toLowerCase().slice(0, 32); - const text = extractConversationMessageText(message); - if (!text) return null; - const rawMessageId = String(message?.id ?? message?.message_id ?? "").trim(); - const messageKey = (rawMessageId || `idx:${index}:${stableId(role, text).slice(0, 16)}`).slice(0, 128); - return { - id: stableId(userId, sessionId, messageKey), - userId, - sessionId, - messageKey, - sequenceNo: index, - role, - text, - rawJson: JSON.stringify(message), - createdAt: Number(message?.created_at ?? message?.createdAt ?? now) || now, - updatedAt: now - }; -} -function buildMemoryPrompt(messages) { - const transcript = messages.map((message, index) => { - const text = String(message.text ?? "").replace(/\s+/g, " ").trim().slice(0, 1500); - return `${index + 1}. [${message.role}] ${text}`; - }).join("\n"); - return [ - "\u4F60\u662F TKMind \u7684\u7528\u6237\u957F\u671F\u8BB0\u5FC6\u63D0\u53D6\u5668\u3002", - "\u8BF7\u4ECE\u4E0B\u9762\u7684\u7528\u6237\u5BF9\u8BDD\u4E2D\u63D0\u53D6\u53EF\u4EE5\u957F\u671F\u590D\u7528\u7684\u4E2A\u4EBA\u8BB0\u5FC6\u3002", - "\u53EA\u8BB0\u5F55\u7528\u6237\u660E\u786E\u8868\u8FBE\u6216\u5F3A\u8BC1\u636E\u652F\u6301\u7684\u4FE1\u606F\uFF0C\u4E0D\u8981\u731C\u6D4B\uFF0C\u4E0D\u8981\u8BB0\u5F55\u4E34\u65F6\u95F2\u804A\u3002", - "\u4E0D\u8981\u8BB0\u5F55\u5BC6\u7801\u3001\u5BC6\u94A5\u3001\u8EAB\u4EFD\u8BC1\u3001\u624B\u673A\u53F7\u3001\u94F6\u884C\u5361\u7B49\u654F\u611F\u4FE1\u606F\u3002", - "\u53EA\u8FD4\u56DE JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981 Markdown\uFF0C\u4E0D\u8981\u89E3\u91CA\u3002", - '\u683C\u5F0F\uFF1A{"memories":[{"label":"preference|habit|interest|goal|fact|experience|knowledge","text":"\u4E00\u53E5\u5B8C\u6574\u4E2D\u6587\u8BB0\u5FC6","confidence":0.0-1.0}]}', - '\u6700\u591A\u8FD4\u56DE 5 \u6761\u3002\u6CA1\u6709\u503C\u5F97\u8BB0\u5F55\u7684\u5185\u5BB9\u65F6\u8FD4\u56DE {"memories":[]}', - "", - transcript - ].join("\n"); -} -function normalizeMemoryItem(item, fallbackLabel = "fact") { - const text = String(item?.text ?? item?.memory ?? item?.content ?? "").trim(); - if (!text) return null; - const labelRaw = String(item?.label ?? item?.category ?? fallbackLabel).trim().toLowerCase(); - const label = LABELS.has(labelRaw) ? labelRaw : fallbackLabel; - const confidence = Math.max(0, Math.min(1, Number(item?.confidence ?? 0.6))); - return { - label, - text: text.slice(0, MAX_MEMORY_TEXT), - confidence: Number.isFinite(confidence) ? confidence : 0.6 - }; -} -function fallbackMemoriesFromMessages(messages) { - const memories = []; - const patterns = [ - { label: "preference", re: /(?:我喜欢|我偏好|我更喜欢|以后.*(?:用|叫|按)|不要再|别再)(.+)/ }, - { label: "interest", re: /(?:我对|我关注|我感兴趣|我想了解)(.+)/ }, - { label: "goal", re: /(?:我的目标是|我想要|我希望|我打算)(.+)/ }, - { label: "habit", re: /(?:我通常|我习惯|我一般)(.+)/ } - ]; - for (const message of messages) { - const text = String(message.text ?? "").replace(/\s+/g, " ").trim(); - for (const { label, re } of patterns) { - const match = text.match(re); - if (!match?.[0]) continue; - memories.push({ - label, - text: `\u7528\u6237${match[0].replace(/[。!?\s]+$/g, "")}`, - confidence: 0.45 - }); - if (memories.length >= 3) return memories; - } - } - return memories; -} -function createConversationMemoryService(pool2, options = {}) { - const now = options.now ?? (() => Date.now()); - const fetchImpl = options.fetch ?? undiciFetch8; - const encryptionKey = options.encryptionKey; - function isEnabled() { - return process.env.USER_CONVERSATION_MEMORY_ENABLED !== "0"; - } - function llmEnabled() { - return process.env.USER_CONVERSATION_MEMORY_LLM_ENABLED !== "0"; - } - async function saveConversationMessages(sessionId, userId, messages = []) { - if (!isEnabled() || !pool2 || !sessionId || !userId || !Array.isArray(messages)) return []; - const ts = now(); - const normalized = messages.map((message, index) => normalizeMessage(sessionId, userId, message, index, ts)).filter(Boolean); - if (!normalized.length) return []; - await pool2.query( - `INSERT INTO h5_conversation_messages - (id, user_id, agent_session_id, message_key, sequence_no, role, text, raw_json, - created_at, updated_at) - VALUES ? - ON DUPLICATE KEY UPDATE - sequence_no = VALUES(sequence_no), - role = VALUES(role), - text = VALUES(text), - raw_json = VALUES(raw_json), - updated_at = VALUES(updated_at)`, - [ - normalized.map((message) => [ - message.id, - message.userId, - message.sessionId, - message.messageKey, - message.sequenceNo, - message.role, - message.text, - message.rawJson, - message.createdAt, - message.updatedAt - ]) - ] - ); - return normalized; - } - async function loadUnanalyzedUserMessages(userId, limit = MAX_ANALYSIS_MESSAGES) { - const [rows] = await pool2.query( - `SELECT id, user_id, agent_session_id, message_key, sequence_no, role, text, created_at - FROM h5_conversation_messages - WHERE user_id = ? AND role = 'user' AND analyzed_at IS NULL - ORDER BY created_at ASC, sequence_no ASC - LIMIT ?`, - [userId, Math.max(1, limit)] - ); - return rows; - } - async function markAnalyzed(messageIds) { - if (!messageIds.length) return; - await pool2.query( - `UPDATE h5_conversation_messages SET analyzed_at = ? WHERE id IN (?)`, - [now(), messageIds] - ); - } - async function selectedProvider() { - const [rows] = await pool2.query( - `SELECT * FROM h5_llm_provider_keys WHERE is_selected = 1 AND status = 'active' LIMIT 1` - ); - return rows[0] ?? null; - } - async function extractWithLlm(messages) { - const row = await selectedProvider(); - if (!row) return null; - const apiUrl = normalizeApiUrl(row.api_url); - const url = resolveChatCompletionsUrl(apiUrl); - if (!url) return null; - let apiKey; - try { - apiKey = decryptSecret( - { - ciphertext: row.api_key_ciphertext, - iv: row.api_key_iv, - tag: row.api_key_tag - }, - resolveEncryptionKey3(encryptionKey) - ); - } catch { - return null; - } - const upstream = await fetchImpl(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}` - }, - body: JSON.stringify({ - model: row.default_model, - messages: [{ role: "user", content: buildMemoryPrompt(messages) }], - stream: false, - temperature: 0, - ...row.relay_provider ? { provider: row.relay_provider } : {} - }) - }); - const text = await upstream.text().catch(() => ""); - if (!upstream.ok) return null; - const payload = extractJsonObject3(text); - const content = payload?.choices?.[0]?.message?.content ?? payload?.content ?? text; - const parsed = extractJsonObject3(content); - const rawItems = Array.isArray(parsed?.memories) ? parsed.memories : []; - return rawItems.map((item) => normalizeMemoryItem(item)).filter(Boolean); - } - async function storeMemories(userId, sourceMessageIds, memories, rawJson = null) { - const rows = []; - const ts = now(); - const sourceSessionId = sourceMessageIds[0]?.agent_session_id ?? null; - const evidenceMessageId = sourceMessageIds[0]?.id ?? null; - for (const item of memories) { - const normalized = normalizeMemoryItem(item); - if (!normalized) continue; - const hash = stableId(userId, normalized.label, normalized.text); - rows.push([ - stableId("memory", hash), - userId, - normalized.label, - hash, - normalized.text, - evidenceMessageId, - sourceSessionId, - normalized.confidence, - rawJson ? JSON.stringify(rawJson) : null, - ts, - ts - ]); - } - if (!rows.length) return 0; - await pool2.query( - `INSERT INTO h5_user_memory_items - (id, user_id, label, memory_hash, memory_text, evidence_message_id, - source_session_id, confidence, raw_json, created_at, updated_at) - VALUES ? - ON DUPLICATE KEY UPDATE - confidence = GREATEST(confidence, VALUES(confidence)), - updated_at = VALUES(updated_at)`, - [rows] - ); - return rows.length; - } - async function analyzeUser(userId) { - if (!isEnabled() || !pool2 || !userId) return { ok: false, reason: "disabled" }; - const messages = await loadUnanalyzedUserMessages(userId); - if (!messages.length) return { ok: true, analyzed: 0, memories: 0 }; - let memories = null; - let extractionSucceeded = false; - if (llmEnabled()) { - try { - memories = await extractWithLlm(messages); - extractionSucceeded = Array.isArray(memories); - } catch (err) { - console.warn("[conversation-memory] LLM extraction failed:", err instanceof Error ? err.message : err); - } - } - if (!memories) memories = fallbackMemoriesFromMessages(messages); - const stored = await storeMemories(userId, messages, memories); - if (!llmEnabled() || extractionSucceeded || stored > 0) { - await markAnalyzed(messages.map((message) => message.id)); - } - return { ok: true, analyzed: messages.length, memories: stored }; - } - async function saveAndAnalyze(sessionId, userId, messages = []) { - const saved = await saveConversationMessages(sessionId, userId, messages); - if (!saved.length) return { saved: 0, analyzed: 0, memories: 0 }; - const result = await analyzeUser(userId); - return { - saved: saved.length, - analyzed: result.analyzed ?? 0, - memories: result.memories ?? 0 - }; - } - async function listMemories(userId, { limit = 50 } = {}) { - const [rows] = await pool2.query( - `SELECT id, label, memory_text, confidence, source_session_id, created_at, updated_at - FROM h5_user_memory_items - WHERE user_id = ? AND status = 'active' - ORDER BY updated_at DESC - LIMIT ?`, - [userId, Math.max(1, Math.min(200, Number(limit) || 50))] - ); - return rows.map((row) => ({ - id: row.id, - label: row.label, - text: row.memory_text, - confidence: Number(row.confidence ?? 0), - sourceSessionId: row.source_session_id ?? null, - createdAt: Number(row.created_at), - updatedAt: Number(row.updated_at) - })); - } - return { - isEnabled, - saveConversationMessages, - analyzeUser, - saveAndAnalyze, - listMemories - }; -} - -// experience-service.mjs -import crypto34 from "node:crypto"; -function createExperienceService(pool2, options = {}) { - const now = options.now ?? (() => Date.now()); - const halfLifeMs = Math.max( - 6e4, - Number(options.recencyHalfLifeMs ?? 30 * 24 * 60 * 60 * 1e3) - ); - function normalizeTags2(tags) { - if (!Array.isArray(tags)) return []; - return [...new Set(tags.map((t) => String(t).trim()).filter(Boolean))]; - } - function rowToExperience(row) { - return { - id: row.id, - scope: row.scope, - kind: row.kind, - title: row.title, - body: row.body, - tags: row.tags_json ? safeJsonArray(row.tags_json) : [], - 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) - }; - } - function safeJsonArray(value) { - if (Array.isArray(value)) return value; - try { - const parsed = JSON.parse(value); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } - } - function rankRows(rows, queryTerms, nowMs3) { - const terms = queryTerms; - return rows.map((row) => { - const haystack = `${row.title} -${row.body}`.toLowerCase(); - let keywordScore = 0; - for (const term of terms) { - if (haystack.includes(term)) keywordScore += 1; - } - const ageMs = Math.max(0, nowMs3 - Number(row.updated_at)); - const recency = Math.pow(0.5, ageMs / halfLifeMs); - const score = keywordScore * recency; - return { row, score }; - }).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score); - } - 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 record(input) { - const title = String(input?.title ?? "").trim(); - const body = String(input?.body ?? "").trim(); - if (!title) throw experienceError("\u7ECF\u9A8C\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A", "invalid_experience_input"); - if (!body) throw experienceError("\u7ECF\u9A8C\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A", "invalid_experience_input"); - const id = crypto34.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); - await pool2.query( - `INSERT INTO h5_experience - (id, scope, kind, title, body, tags_json, source_session_id, source_user_id, - use_count, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`, - [ - id, - scope, - kind, - title, - body, - JSON.stringify(tags), - input?.sourceSessionId ?? null, - input?.sourceUserId ?? null, - ts, - ts - ] - ); - return rowToExperience({ - id, - scope, - kind, - title, - body, - tags_json: 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 terms = tokenize(query); - if (terms.length === 0) return []; - const [rows] = await pool2.query( - `SELECT id, scope, kind, title, body, tags_json, source_session_id, - source_user_id, use_count, created_at, updated_at - FROM h5_experience - WHERE scope = ? - ORDER BY updated_at DESC - LIMIT 500`, - [scope] - ); - const ranked = rankRows(rows, terms, now()).slice(0, Math.max(1, limit)); - if (ranked.length > 0) { - const ids = ranked.map((entry) => entry.row.id); - const placeholders = ids.map(() => "?").join(","); - await pool2.query( - `UPDATE h5_experience SET use_count = use_count + 1 WHERE id IN (${placeholders})`, - ids - ).catch(() => { - }); - } - return ranked.map((entry) => rowToExperience(entry.row)); - } - async function reflect() { - return { ok: false, reason: "not_implemented" }; - } - return { record, search, reflect }; -} -function experienceError(message, code) { - return Object.assign(new Error(message), { code }); -} - -// asr-proxy.mjs -import express from "express"; -import { fetch as fetch4 } from "undici"; -var ASR_TARGET = process.env.H5_ASR_TARGET ?? "https://asr.tkmind.cn"; -var ASR_MAX_BYTES = Number(process.env.H5_ASR_MAX_BYTES ?? 5 * 1024 * 1024); -var ASR_TIMEOUT_MS = Number(process.env.H5_ASR_TIMEOUT_MS ?? 45e3); -function sanitizeAsrMessage2(message) { - if (!message || typeof message !== "string") return "\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"; - const trimmed = message.trim(); - if (!trimmed) return "\u8BC6\u522B\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"; - if (/Failed to load audio|ffmpeg|Invalid data found when processing input|moov atom not found/i.test(trimmed)) { - return "\u97F3\u9891\u683C\u5F0F\u65E0\u6CD5\u8BC6\u522B\uFF0C\u8BF7\u91CD\u65B0\u5F55\u5236"; - } - if (/timeout|timed out/i.test(trimmed)) return "\u8BC6\u522B\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5"; - if (trimmed.length > 160) return `${trimmed.slice(0, 160)}\u2026`; - return trimmed; -} -function attachAsrRoutes(api2, deps) { - const multipartRaw = express.raw({ - type: (req) => (req.headers["content-type"] ?? "").includes("multipart/form-data"), - limit: ASR_MAX_BYTES - }); - api2.post("/asr/oneshot", multipartRaw, async (req, res) => { - const contentType = req.headers["content-type"] ?? ""; - if (!contentType.includes("multipart/form-data")) { - return deps.sendError(res, req, 400, "invalid_request", "Content-Type \u5FC5\u987B\u4E3A multipart/form-data"); - } - if (!req.body?.length) { - return deps.sendError(res, req, 400, "invalid_request", "\u97F3\u9891\u5185\u5BB9\u4E3A\u7A7A"); - } - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), ASR_TIMEOUT_MS); - try { - const upstream = await fetch4(`${ASR_TARGET}/asr/oneshot`, { - method: "POST", - headers: { "content-type": contentType }, - body: req.body, - signal: controller.signal - }); - const text = await upstream.text(); - let payload = null; - try { - payload = JSON.parse(text); - } catch { - payload = null; - } - if (!upstream.ok) { - const message = sanitizeAsrMessage2(payload?.message ?? text ?? "ASR \u8BF7\u6C42\u5931\u8D25"); - console.warn("[asr] upstream HTTP error", upstream.status, message); - return deps.sendError(res, req, upstream.status, "asr_failed", message); - } - if (payload?.code !== 200) { - const message = sanitizeAsrMessage2(payload?.message ?? "\u8BC6\u522B\u5931\u8D25"); - console.warn("[asr] upstream business error", payload?.code, message); - return deps.sendError(res, req, 502, "asr_failed", message); - } - return deps.sendData(res, req, { text: payload?.data?.text ?? "" }); - } catch (err) { - const message = err instanceof Error && err.name === "AbortError" ? "\u8BC6\u522B\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5" : sanitizeAsrMessage2(err instanceof Error ? err.message : "ASR \u8BF7\u6C42\u5931\u8D25"); - console.warn("[asr] proxy failed", message); - return deps.sendError(res, req, 502, "asr_failed", message); - } finally { - clearTimeout(timer); - } - }); -} - -// server.mjs -var __dirname5 = path34.dirname(fileURLToPath6(import.meta.url)); -function loadEnvFile(filePath) { - if (!fs32.existsSync(filePath)) return; - for (const line of fs32.readFileSync(filePath, "utf8").split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - 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; - } -} -loadEnvFile(path34.join(__dirname5, "../../.env.local")); -loadEnvFile(path34.join(__dirname5, ".env")); -function parseApiTargets() { - const csvTargets = (process.env.TKMIND_API_TARGETS ?? "").split(",").map((value) => value.trim()).filter(Boolean); - const legacyTargets = [ - process.env.TKMIND_API_TARGET ?? "https://127.0.0.1:18006", - process.env.TKMIND_API_TARGET_1 - ].filter(Boolean); - return [.../* @__PURE__ */ new Set([...csvTargets, ...legacyTargets])]; -} -var PORT = Number(process.env.H5_PORT ?? 8081); -var HOST = String(process.env.H5_HOST ?? "127.0.0.1").trim() || "127.0.0.1"; -var API_TARGETS = parseApiTargets(); -var API_TARGET = API_TARGETS[0] ?? "https://127.0.0.1:18006"; -var API_SECRET = process.env.TKMIND_SERVER__SECRET_KEY ?? "local-dev-secret"; -var INTERNAL_AGENT_SECRET = process.env.MINDSPACE_INTERNAL_AGENT_SECRET ?? API_SECRET; -var ACCESS_PASSWORD = process.env.H5_ACCESS_PASSWORD; -var WECHAT_MP_CONFIG = loadWechatMpConfig(); -var WORKSPACE_MAINTENANCE_ENABLED = process.env.MEMIND_WORKSPACE_MAINTENANCE !== "0"; -var USERS_ROOT = process.env.H5_USERS_ROOT ?? path34.join(__dirname5, "users"); -var app = express2(); -app.set("trust proxy", 1); -var isSecureRequest = (req) => req.secure || req.get("x-forwarded-proto")?.split(",")[0]?.trim() === "https"; -var msFlags = mindspaceFlags(); -app.use(attachRequestId); -app.use((req, res, next) => { - res.setHeader("X-Content-Type-Options", "nosniff"); - res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); - res.setHeader("Permissions-Policy", "camera=(), microphone=(self), geolocation=()"); - res.setHeader("X-Frame-Options", "SAMEORIGIN"); - if (isSecureRequest(req)) { - res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - } - next(); -}); -function csrfOriginCheck(req, res, next) { - if (["GET", "HEAD", "OPTIONS"].includes(req.method)) return next(); - const host = req.get("host"); - const origin = req.get("origin"); - const referer = req.get("referer"); - if (!origin && !referer) return next(); - const requestHostname = (host ?? "").split(":")[0]; - const allowed = [origin, referer].some((value) => { - if (!value) return false; - try { - const source = new URL(value); - if (source.host === host) return true; - return isLocalDevHostname(requestHostname) && isLocalDevHostname(source.hostname); - } catch { - return false; - } - }); - if (!allowed) { - return sendError(res, req, 403, "csrf_failed", "\u6765\u6E90\u6821\u9A8C\u5931\u8D25"); - } - return next(); -} -app.use("/api", csrfOriginCheck); -app.use("/auth", csrfOriginCheck); -app.use("/mindspace", csrfOriginCheck); -var jsonBody = express2.json({ limit: "1mb" }); -var jsonUnlessMultipart = (req, res, next) => { - if ((req.headers["content-type"] ?? "").includes("multipart/form-data")) return next(); - return jsonBody(req, res, next); -}; -var rawUploadBody = express2.raw({ - type: "application/octet-stream", - limit: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES) -}); -var wikiAuth = createWikiAuth(path34.join(__dirname5, PUBLISH_ROOT_DIR, "wiki-db")); -var legacyAuth = null; -if (ACCESS_PASSWORD && !isDatabaseConfigured()) { - legacyAuth = createAuthManager({ password: ACCESS_PASSWORD }); -} else if (ACCESS_PASSWORD && isDatabaseConfigured()) { - console.log("H5_ACCESS_PASSWORD ignored: multi-user database auth is configured"); -} -var userAuth = null; -var tkmindProxy = null; -var agentRunGateway = null; -var toolGateway = null; -var sessionSnapshotService = null; -var conversationMemoryService = null; -var mindSpace = null; -var mindSpaceAssets = null; -var mindSpaceAudit = null; -var mindSpaceServiceFacade = null; -var mindSpaceConversationPackageRegistry = null; -var mindSpacePages = null; -var mindSpacePageLiveEdit = null; -var mindSpaceAssetAgent = null; -var mindSpacePageEditSession = null; -var mindSpacePublications = null; -var plazaPosts = null; -var plazaEvents = null; -var plazaRecommend = null; -var plazaInteractions = null; -var plazaSeo = null; -var plazaOps = null; -var plazaRedis = createNoopPlazaRedis(); -var mindSpaceCleanup = null; -var mindSpaceAgentJobs = null; -var mindSpaceAgentRunner = null; -var rechargeService = null; -var subscriptionService = null; -var wechatPayClient = null; -var wechatOAuthService = null; -var wechatMpService = null; -var notificationDispatcher = null; -var scheduleService = null; -var feedbackService = null; -var scheduleReminderWorker = null; -var llmProviderService = null; -var wordFilterService = null; -var authPool = null; -async function bootstrapUserAuth() { - try { - if (!isDatabaseConfigured()) return false; - const pool2 = createDbPool(); - authPool = pool2; - await initSchema(pool2); - await ensureMindSpaceConfig(pool2, { - env: process.env - }); - scheduleService = createScheduleService(pool2, { - defaultTimezone: process.env.H5_DEFAULT_TIMEZONE || "Asia/Shanghai" - }); - feedbackService = createFeedbackService(pool2); - mindSpace = createMindSpaceService(pool2, { - maxFileBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES), - aiDailyLimit: Number(process.env.MINDSPACE_FREE_AI_DAILY_LIMIT ?? 10), - publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5), - monthlyViewLimit: Number(process.env.MINDSPACE_FREE_MONTHLY_VIEW_LIMIT ?? 1e3), - scheduleService - }); - const mindSpaceStorageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"); - mindSpaceServiceFacade = createMindSpaceServiceFacade({ - storageAdapter: createLocalMindSpaceStorageAdapter(mindSpaceStorageRoot), - publicBaseUrl: resolvePublicBaseUrl(), - conversationPackageBackfill: ({ user, sessionId }) => backfillConversationPackageArtifacts({ - pool: pool2, - registry: mindSpaceConversationPackageRegistry, - storageRoot: mindSpaceStorageRoot, - h5Root: __dirname5, - user, - sessionId - }), - conversationPackagePublicHtmlHydrator: hydratePublicHtmlArtifactsForConversationPackage - }); - mindSpaceConversationPackageRegistry = createConversationPackageRegistry({ - store: createConversationPackageStore(pool2), - service: mindSpaceServiceFacade - }); - mindSpaceAssets = createAssetService(pool2, { - h5Root: __dirname5, - storageRoot: mindSpaceStorageRoot, - maxFileBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES), - conversationPackageRegistry: mindSpaceConversationPackageRegistry - }); - mindSpacePages = createPageService(pool2, { - h5Root: __dirname5, - storageRoot: mindSpaceStorageRoot, - conversationPackageRegistry: mindSpaceConversationPackageRegistry - }); - const resolveUserIdForAgentSession = async (sessionId) => { - const [rows] = await pool2.query( - `SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`, - [sessionId] - ); - return rows[0]?.user_id ?? null; - }; - mindSpacePageLiveEdit = createPageLiveEditService({ - pageService: mindSpacePages, - resolveUserIdForAgentSession - }); - mindSpaceAssetAgent = createAssetAgentService({ - assetService: mindSpaceAssets, - resolveUserIdForAgentSession - }); - mindSpacePublications = createPublicationService(pool2, { - h5Root: __dirname5, - storageRoot: mindSpaceStorageRoot, - publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5), - conversationPackageRegistry: mindSpaceConversationPackageRegistry - }); - setInterval(async () => { - try { - const result = await mindSpacePublications.cleanupExpiredUnconfirmedPublications(); - if (result.cleaned > 0) { - console.log(`[Publication Cleanup] Auto-privatized ${result.cleaned} expired unconfirmed publications`); - } - } catch (err) { - console.error("[Publication Cleanup Error]", err instanceof Error ? err.message : err); - } - }, 60 * 1e3); - await ensureAlgorithmConfig(pool2); - const plazaAlgorithmConfig = await loadAlgorithmConfig(pool2); - plazaRedis = await createPlazaRedis(process.env.PLAZA_REDIS_URL, pool2); - if (plazaRedis.enabled) { - console.log("Plaza Redis enabled"); - } - plazaSeo = createPlazaSeoService(pool2); - plazaInteractions = createPlazaInteractionService(pool2, { - formatPostRow, - plazaRedis - }); - plazaEvents = createPlazaEventService(pool2); - plazaRecommend = createPlazaRecommendService(pool2, { - eventService: plazaEvents, - formatPostRow, - loadViewerReactions: (viewerId, postIds) => plazaInteractions.loadViewerReactions(viewerId, postIds), - algorithmConfig: plazaAlgorithmConfig - }); - plazaPosts = createPlazaPostService(pool2, { - loadViewerReactions: (viewerId, postIds) => plazaInteractions.loadViewerReactions(viewerId, postIds), - plazaRedis, - algorithmConfig: plazaAlgorithmConfig, - recommendService: plazaRecommend, - onPostPublished: (postId) => plazaSeo?.notifyPostPublished(postId), - loadFeaturedPosts: async (viewerId) => { - if (!plazaOps) return { homepage_banner: [], trending: [], category_top: {} }; - return plazaOps.loadActiveFeaturedPosts(viewerId); - } - }); - plazaOps = createPlazaOpsService(pool2, { - formatPostRow, - reviewPost: (...args) => plazaPosts.reviewPost(...args), - invalidateFeedCaches: () => plazaRedis?.invalidateFeedCaches?.() - }); - startPlazaTasks({ - pool: pool2, - plazaRedis, - recalculateHotScores, - writebackPublications - }); - mindSpaceCleanup = createCleanupService(pool2, { - storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"), - h5Root: __dirname5 - }); - await ensurePlanCatalogSchema(pool2); - const planCatalogService = createPlanCatalogService(pool2); - subscriptionService = createSubscriptionService(pool2, { - getPlanAsync: (planType) => planCatalogService.getPlan(planType) - }); - subscriptionService._planCatalogService = planCatalogService; - userAuth = createUserAuth(pool2, { - usersRoot: USERS_ROOT, - h5Root: __dirname5, - defaultSignupBalanceCents: Number(process.env.H5_SIGNUP_BALANCE_CENTS ?? 500), - subscriptionService - }); - wechatPayClient = createWechatPayClient(loadWechatPayConfig()); - wechatOAuthService = createWechatOAuthService(pool2, loadWechatOAuthConfig(), { userAuth }); - rechargeService = createRechargeService(pool2, { - userAuth, - wechatPay: wechatPayClient - }); - if (wechatPayClient.enabled) { - console.log(`WeChat Pay recharge enabled (${wechatPayClient.apiVersion ?? "unknown"})`); - } - if (wechatOAuthService.enabled) { - console.log("WeChat OAuth login enabled"); - } - mindSpaceAgentJobs = createAgentJobService(pool2, { - pageService: mindSpacePages, - storageRoot: process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"), - maxOutputBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES) - }); - let experienceService = null; - if (process.env.MINDSPACE_EXPERIENCE_ENABLED !== "false") { - if (process.env.EXPERIENCE_PG_URL) { - try { - const { createPgExperienceService: createPgExperienceService2 } = await Promise.resolve().then(() => (init_experience_service_pg(), experience_service_pg_exports)); - experienceService = await createPgExperienceService2({ - connectionString: process.env.EXPERIENCE_PG_URL - }); - console.log("Experience store: PostgreSQL + pgvector"); - } catch (error) { - console.error( - "Experience PG init failed, falling back to MySQL store:", - error instanceof Error ? error.message : error - ); - experienceService = createExperienceService(pool2); - } - } else { - experienceService = createExperienceService(pool2); - } - } - mindSpaceAgentRunner = createMindSpaceAgentRunner({ - apiTarget: API_TARGET, - apiSecret: API_SECRET, - userAuth, - agentJobService: mindSpaceAgentJobs, - experienceService - }); - mindSpaceAudit = createMindSpaceAuditWriter(pool2); - if (process.env.MINDSPACE_AGENT_WORKER_ENABLED === "true") { - const workerConcurrency = Math.max( - 1, - Number(process.env.MINDSPACE_AGENT_WORKER_CONCURRENCY ?? 2) - ); - const workerPollMs = Math.max( - 200, - Number(process.env.MINDSPACE_AGENT_WORKER_POLL_MS ?? 1e3) - ); - const workerStaleMs = Math.max( - 1e4, - Number(process.env.MINDSPACE_AGENT_WORKER_STALE_MS ?? 5 * 60 * 1e3) - ); - let inFlight = 0; - let draining = false; - const drainQueue = async () => { - if (draining) return; - draining = true; - try { - while (inFlight < workerConcurrency) { - const claim = await mindSpaceAgentJobs.claimNextJob(); - if (!claim) break; - inFlight += 1; - void mindSpaceAgentRunner.runJob(claim.jobId, claim).catch((error) => { - console.error("Agent worker job failed:", error); - }).finally(() => { - inFlight -= 1; - }); - } - } catch (error) { - console.error("Agent worker drain failed:", error); - } finally { - draining = false; - } - }; - const workerTimer = setInterval(() => { - void drainQueue(); - }, workerPollMs); - const reaperTimer = setInterval(() => { - void mindSpaceAgentJobs.reapStaleJobs(workerStaleMs).then((reaped) => { - if (reaped > 0) { - console.warn(`Agent worker reaped ${reaped} stale running job(s)`); - } - }).catch((error) => { - console.error("Agent worker reaper failed:", error); - }); - }, Math.min(workerStaleMs, 6e4)); - workerTimer.unref?.(); - reaperTimer.unref?.(); - console.log( - `Agent job worker enabled (concurrency=${workerConcurrency}, poll=${workerPollMs}ms)` - ); - } - if (WORKSPACE_MAINTENANCE_ENABLED) { - startWorkspaceThumbnailWatcher(path34.join(__dirname5, PUBLISH_ROOT_DIR)); - startWorkspaceAssetSyncWatcher({ - publishRoot: path34.join(__dirname5, PUBLISH_ROOT_DIR), - syncUserWorkspaceByDirKey: async (dirKey, options) => { - let userId = dirKey; - if (!PUBLISH_KEY_UUID.test(dirKey)) { - const [rows] = await pool2.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [ - dirKey - ]); - userId = rows[0]?.id; - } - if (!userId) return; - await mindSpaceAssets.syncWorkspaceAssets(userId, options); - } - }); - void mindSpaceAssets.expireStaleUploads().catch(() => { - }); - setInterval(() => { - void mindSpaceAssets?.expireStaleUploads().catch(() => { - }); - }, 5 * 60 * 1e3).unref?.(); - } else { - console.log("Workspace maintenance daemons disabled (MEMIND_WORKSPACE_MAINTENANCE=0)"); - } - await userAuth.ensureAdminUser(); - llmProviderService = createLlmProviderService(pool2, { - apiTarget: API_TARGET, - apiTargets: API_TARGETS, - apiSecret: API_SECRET - }); - wordFilterService = createWordFilterService(pool2); - void llmProviderService.ensureBootstrapRelay().then((result) => { - if (result.created) { - console.log(`LLM relay bootstrap created: ${RELAY_BOOTSTRAP.name}`); - } - }).catch((err) => { - console.warn("LLM relay bootstrap skipped:", err instanceof Error ? err.message : err); - }); - void llmProviderService.syncSelectedToGoosed().catch((err) => { - console.warn("LLM provider boot sync skipped:", err instanceof Error ? err.message : err); - }); - conversationMemoryService = createConversationMemoryService(pool2); - sessionSnapshotService = createSessionSnapshotService(pool2, { - conversationMemoryService - }); - tkmindProxy = createTkmindProxy({ - apiTarget: API_TARGET, - apiTargets: API_TARGETS, - apiSecret: API_SECRET, - userAuth, - llmProviderService, - subscriptionService, - sessionSnapshotService, - conversationMemoryService, - localFetchAsset: mindSpaceAssets ? async (userId, assetId) => { - const { asset, path: assetPath } = await mindSpaceAssets.readAsset(userId, assetId); - const buffer = await fs32.promises.readFile(assetPath); - return { buffer, mimeType: asset.mimeType }; - } : null - }); - toolGateway = createToolGateway({ llmProviderService }); - agentRunGateway = createAgentRunGateway({ - pool: pool2, - userAuth, - tkmindProxy, - toolGateway, - autoDispatch: ["1", "true", "yes", "on"].includes( - String(process.env.MEMIND_AGENT_RUN_AUTODISPATCH ?? "1").trim().toLowerCase() - ), - maxConcurrentRuns: Number(process.env.MEMIND_AGENT_RUN_QUEUE_CONCURRENCY ?? 1), - runTimeoutMs: Number(process.env.MEMIND_AGENT_RUN_TIMEOUT_MS ?? 15 * 60 * 1e3) - }); - wechatMpService = createWechatMpService({ - config: WECHAT_MP_CONFIG, - userAuth, - apiFetch: tkmindProxy.apiFetch, - startAgentSession: ({ userId, workingDir, sessionPolicy }) => tkmindProxy.startSessionForUser(userId, { workingDir, sessionPolicy }), - sessionApiFetch: async (sessionId, pathname, init) => { - const target = await tkmindProxy.resolveTarget(sessionId); - return tkmindProxy.apiFetchTo(target, pathname, init); - }, - scheduleService: process.env.H5_SCHEDULE_ENABLED === "1" ? scheduleService : null, - applySessionLlmProvider: (sessionId) => tkmindProxy.applySessionLlmProvider(sessionId), - refreshSessionSnapshot: sessionSnapshotService?.isEnabled() ? (sessionId, userId) => sessionSnapshotService.refresh(sessionId, userId, async (pathname, init) => { - const target = await tkmindProxy.resolveTarget(sessionId); - return tkmindProxy.apiFetchTo(target, pathname, init); - }) : null - }); - notificationDispatcher = createNotificationDispatcher({ - sendWechatTextToUser: wechatMpService?.enabled ? (userId, text) => wechatMpService.sendTextToUser(userId, text) : null - }); - userAuth.setRechargeNotifier(async ({ userId, title, body, dedupeKey }) => { - await notificationDispatcher.sendRechargeSuccess({ userId, title, body, dedupeKey }); - }); - if (process.env.H5_REMINDER_WORKER_ENABLED === "1" && wechatMpService?.enabled && scheduleService) { - scheduleReminderWorker = startScheduleReminderWorker({ - scheduleService, - notificationDispatcher - }); - console.log("Schedule reminder worker enabled"); - } - if (subscriptionService) { - const subExpiryTimer = setInterval(async () => { - try { - const { renewed, failed } = await subscriptionService.processAutoRenewals(); - if (renewed > 0) console.log(`Auto-renewed ${renewed} subscription(s)`); - if (failed > 0) console.log(`Auto-renew failed for ${failed} subscription(s) (balance insufficient)`); - const n = await subscriptionService.expireStaleSubscriptions(); - if (n > 0) console.log(`Expired ${n} stale subscription(s)`); - } catch (err) { - console.warn("Subscription expiry check failed:", err); - } - }, 60 * 60 * 1e3); - subExpiryTimer.unref?.(); - } - mindSpacePageEditSession = createPageEditSessionService({ - apiTarget: API_TARGET, - apiSecret: API_SECRET, - userAuth, - pageService: mindSpacePages, - pageLiveEdit: mindSpacePageLiveEdit, - llmProviderService - }); - if (wechatMpService?.enabled) { - console.log("WeChat MP webhook enabled"); - } - console.log(`User auth enabled (MySQL), workspace root: ${USERS_ROOT}`); - return true; - } catch (err) { - console.error("User auth bootstrap failed:", err); - if (isDatabaseConfigured() && process.env.NODE_ENV === "production") { - console.error( - "Fatal: database is configured but user auth bootstrap failed; exiting so launchd can retry" - ); - process.exit(1); - } - return false; - } -} -var userAuthReady = bootstrapUserAuth(); -function legacySessionToken(req) { - return parseCookies(req.get("cookie"))[AUTH_COOKIE]; -} -function userToken(req) { - return parseCookies(req.get("cookie"))[USER_COOKIE]; -} -function setUserLoginCookies(res, req, token) { - res.set( - "Set-Cookie", - userLoginCookies(token, isSecureRequest(req), resolveCookieDomainForRequest(req)) - ); -} -function clearUserLoginCookies(res, req) { - res.set( - "Set-Cookie", - clearUserSessionCookie(isSecureRequest(req), resolveCookieDomainForRequest(req)) - ); -} -async function attachUserSession(req, _res, next) { - if (!userAuth) return next(); - const token = userToken(req); - req.userToken = token; - try { - req.userSession = token ? await userAuth.verify(token) : null; - req.userSessionError = null; - } catch (err) { - req.userSession = null; - req.userSessionError = err; - console.error("[Auth] session verify failed:", err instanceof Error ? err.message : err); - } - next(); -} -app.use(attachUserSession); -app.get("/auth/status", async (req, res) => { - await userAuthReady; - if (userAuth) { - try { - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.json({ authenticated: false, mode: "user" }); - const row = await userAuth.getUserById(me.id); - const capabilityState = await userAuth.resolveUserCapabilities(row); - return res.json({ - authenticated: true, - user: me, - mode: "user", - capabilities: capabilityState.capabilities, - grantedSkills: capabilityState.grantedSkills ?? [], - unrestricted: capabilityState.unrestricted - }); - } catch (err) { - console.error("[Auth] status failed:", err instanceof Error ? err.message : err); - return res.status(503).json({ - authenticated: false, - mode: "unavailable", - message: "\u7528\u6237\u8BA4\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" - }); - } - } - if (isDatabaseConfigured()) { - return res.status(503).json({ - authenticated: false, - mode: "unavailable", - message: "\u7528\u6237\u8BA4\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" - }); - } - if (legacyAuth) { - return res.json({ - authenticated: legacyAuth.verify(legacySessionToken(req)), - mode: "legacy" - }); - } - return res.json({ authenticated: false, mode: "none" }); -}); -app.post("/auth/login", jsonBody, async (req, res) => { - await userAuthReady; - const secure = isSecureRequest(req); - if (userAuth) { - const { username, password: password2 } = req.body ?? {}; - if (!username || !password2) { - return res.status(400).json({ message: "\u7528\u6237\u540D\u548C\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A" }); - } - const result2 = await userAuth.login({ username, password: password2, ip: req.ip }); - if (!result2.ok) { - if (result2.retryAfterMs > 0) { - res.set("Retry-After", String(Math.ceil(result2.retryAfterMs / 1e3))); - return res.status(429).json({ message: result2.message }); - } - return res.status(401).json({ message: result2.message }); - } - setUserLoginCookies(res, req, result2.token); - return res.json({ authenticated: true, user: result2.user, mode: "user" }); - } - if (!legacyAuth) { - return res.status(503).json({ message: "\u672A\u914D\u7F6E\u7528\u6237\u6570\u636E\u5E93\u6216\u8BBF\u95EE\u5BC6\u7801" }); - } - const password = typeof req.body?.password === "string" ? req.body.password : ""; - const result = legacyAuth.login(password, req.ip); - if (!result.ok) { - if (result.retryAfterMs > 0) { - res.set("Retry-After", String(Math.ceil(result.retryAfterMs / 1e3))); - return res.status(429).json({ message: "\u5C1D\u8BD5\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5" }); - } - return res.status(401).json({ message: "\u5BC6\u7801\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5" }); - } - res.set("Set-Cookie", sessionCookie(result.token, secure)); - return res.json({ authenticated: true, mode: "legacy" }); -}); -app.post("/auth/register", jsonBody, async (req, res) => { - await userAuthReady; - if (!userAuth) { - return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u6CE8\u518C" }); - } - const { username, password, displayName, email } = req.body ?? {}; - const result = await userAuth.register({ username, password, displayName, email }); - if (!result.ok) { - const status = result.message.includes("\u5DF2\u5B58\u5728") ? 409 : 400; - return res.status(status).json({ message: result.message }); - } - if (plazaSeo && req.body?.utm_source) { - void plazaSeo.recordAttribution( - { - event_type: "signup", - utm_source: req.body.utm_source, - utm_medium: req.body.utm_medium, - utm_campaign: req.body.utm_campaign, - ref_id: req.body.ref ?? req.body.ref_id, - user_id: result.user?.id ?? null - }, - plazaClientIp(req) - ).catch(() => { - }); - } - return res.json({ ok: true, user: result.user }); -}); -app.post("/auth/reset-password", jsonBody, async (req, res) => { - await userAuthReady; - if (!userAuth) { - return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - } - const { username, email, password } = req.body ?? {}; - const result = await userAuth.resetPassword({ username, email, password }); - if (!result.ok) { - return res.status(400).json({ message: result.message }); - } - return res.json({ ok: true }); -}); -app.get("/auth/wechat/config", async (req, res) => { - await userAuthReady; - if (!wechatOAuthService?.enabled) { - return res.json({ - enabled: false, - inWechat: isWechatUserAgent(req.get("user-agent") || ""), - scanEnabled: false - }); - } - return res.json(wechatOAuthService.publicConfig(req)); -}); -app.get("/auth/wechat/js-sdk-signature", async (req, res) => { - await userAuthReady; - if (!userAuth || !wechatMpService?.enabled) { - return res.status(503).json({ message: "\u5FAE\u4FE1 JS-SDK \u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const pageUrl = String(req.query?.url ?? "").split("#")[0]; - if (!pageUrl) { - return res.status(400).json({ message: "\u7F3A\u5C11 url" }); - } - try { - const normalizedUrl = validateWechatShareSignatureUrl(pageUrl, { - publicBaseUrl: WECHAT_MP_CONFIG.publicBaseUrl, - requestHost: req.get("x-forwarded-host") || req.get("host") || "" - }); - const payload = await wechatMpService.createJsSdkSignature(normalizedUrl); - return res.json(payload); - } catch (err) { - const message = err instanceof Error ? err.message : "\u5FAE\u4FE1 JS-SDK \u7B7E\u540D\u5931\u8D25"; - console.warn("WeChat JS-SDK signature failed:", message); - return res.status(502).json({ message }); - } -}); -app.get("/auth/wechat/public-js-sdk-signature", async (req, res) => { - await userAuthReady; - if (!wechatMpService?.enabled) { - return res.status(503).json({ message: "\u5FAE\u4FE1 JS-SDK \u672A\u542F\u7528" }); - } - const pageUrl = String(req.query?.url ?? "").split("#")[0]; - if (!pageUrl) { - return res.status(400).json({ message: "\u7F3A\u5C11 url" }); - } - try { - const normalizedUrl = validateWechatShareSignatureUrl(pageUrl, { - publicBaseUrl: WECHAT_MP_CONFIG.publicBaseUrl, - requestHost: req.get("x-forwarded-host") || req.get("host") || "" - }); - const payload = await wechatMpService.createJsSdkSignature(normalizedUrl); - return res.json(payload); - } catch (err) { - const message = err instanceof Error ? err.message : "\u5FAE\u4FE1 JS-SDK \u7B7E\u540D\u5931\u8D25"; - console.warn("WeChat public JS-SDK signature failed:", message); - return res.status(502).json({ message }); - } -}); -app.get("/auth/wechat/status", async (req, res) => { - await userAuthReady; - if (!userAuth || !wechatOAuthService?.enabled) { - return res.json({ enabled: false, bound: false }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const config = loadWechatOAuthConfig(); - const status = await userAuth.getWechatBindingStatus(me.id, config.appId); - return res.json({ enabled: true, ...status }); -}); -if (WECHAT_MP_CONFIG.enabled) { - app.get("/auth/wechat/agent-route", async (req, res) => { - await userAuthReady; - if (!userAuth || !wechatMpService?.enabled) { - return res.status(503).json({ message: "\u516C\u4F17\u53F7 Agent \u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - return res.json(await wechatMpService.getRouteStatusForUser(me.id)); - }); - app.post("/auth/wechat/agent-route/reset", async (req, res) => { - await userAuthReady; - if (!userAuth || !wechatMpService?.enabled) { - return res.status(503).json({ message: "\u516C\u4F17\u53F7 Agent \u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - try { - const result = await wechatMpService.recreateRouteForUser(me.id); - if (!result.ok) return res.status(400).json({ message: result.message }); - return res.json(result); - } catch (err) { - return res.status(500).json({ - message: err instanceof Error ? err.message : "\u91CD\u5EFA\u516C\u4F17\u53F7 Agent \u8DEF\u7531\u5931\u8D25" - }); - } - }); -} -app.get("/auth/wechat/pending/:token", async (req, res) => { - await userAuthReady; - if (!userAuth) return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - const pending = await userAuth.getWechatPendingBind(req.params.token); - if (!pending) return res.status(404).json({ message: "\u7ED1\u5B9A\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5FAE\u4FE1\u767B\u5F55" }); - return res.json({ - nickname: pending.nickname, - avatarUrl: pending.avatar_url, - returnTo: pending.return_to || "/" - }); -}); -app.post("/auth/wechat/register", jsonBody, async (req, res) => { - await userAuthReady; - const secure = isSecureRequest(req); - if (!userAuth) return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - const pendingToken = typeof req.body?.pendingToken === "string" ? req.body.pendingToken : ""; - if (!pendingToken) return res.status(400).json({ message: "\u7F3A\u5C11\u7ED1\u5B9A\u4F1A\u8BDD" }); - const result = await userAuth.completeWechatRegister({ pendingToken }); - if (!result.ok) return res.status(400).json({ message: result.message }); - if (result.isNewUser && plazaSeo) { - void plazaSeo.recordAttribution( - { - event_type: "signup", - utm_source: result.utmSource || "wechat", - utm_medium: result.utmMedium, - utm_campaign: result.utmCampaign, - user_id: result.user?.id ?? null - }, - plazaClientIp(req) - ).catch(() => { - }); - } - setUserLoginCookies(res, req, result.token); - return res.json({ - authenticated: true, - user: result.user, - returnTo: result.returnTo || "/" - }); -}); -app.post("/auth/wechat/bind", jsonBody, async (req, res) => { - await userAuthReady; - const secure = isSecureRequest(req); - if (!userAuth) return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - const pendingToken = typeof req.body?.pendingToken === "string" ? req.body.pendingToken : ""; - const username = typeof req.body?.username === "string" ? req.body.username : ""; - const password = typeof req.body?.password === "string" ? req.body.password : ""; - if (!pendingToken) return res.status(400).json({ message: "\u7F3A\u5C11\u7ED1\u5B9A\u4F1A\u8BDD" }); - if (!username || !password) { - return res.status(400).json({ message: "\u7528\u6237\u540D\u548C\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A" }); - } - const result = await userAuth.completeWechatBindAccount({ - pendingToken, - username, - password, - ip: req.ip - }); - if (!result.ok) { - const status = result.retryAfterMs > 0 ? 429 : 401; - if (result.retryAfterMs > 0) { - res.set("Retry-After", String(Math.ceil(result.retryAfterMs / 1e3))); - } - return res.status(status).json({ message: result.message }); - } - setUserLoginCookies(res, req, result.token); - return res.json({ - authenticated: true, - user: result.user, - bound: true, - returnTo: result.returnTo || "/" - }); -}); -app.post("/auth/wechat/scan/start", async (req, res) => { - await userAuthReady; - if (!wechatOAuthService?.enabled) { - return res.status(503).json({ message: "\u5FAE\u4FE1\u767B\u5F55\u672A\u542F\u7528" }); - } - try { - const payload = await wechatOAuthService.startScanLogin(req); - return res.json(payload); - } catch (err) { - return res.status(503).json({ - message: err instanceof Error ? err.message : "\u5FAE\u4FE1\u626B\u7801\u767B\u5F55\u4E0D\u53EF\u7528" - }); - } -}); -app.get("/auth/wechat/scan/poll", async (req, res) => { - await userAuthReady; - if (!wechatOAuthService?.enabled) { - return res.status(503).json({ message: "\u5FAE\u4FE1\u767B\u5F55\u672A\u542F\u7528" }); - } - const state = typeof req.query?.state === "string" ? req.query.state : ""; - if (!state) return res.status(400).json({ message: "\u7F3A\u5C11\u626B\u7801\u72B6\u6001" }); - const result = await wechatOAuthService.pollScanLogin(state); - if (result.status === "complete" && result.token) { - setUserLoginCookies(res, req, result.token); - } - return res.json(result); -}); -app.get("/auth/wechat/authorize", async (req, res) => { - await userAuthReady; - if (!wechatOAuthService?.enabled) { - return res.status(503).json({ message: "\u5FAE\u4FE1\u767B\u5F55\u672A\u542F\u7528" }); - } - try { - let bindUserId = null; - const intent = typeof req.query?.intent === "string" ? req.query.intent.trim().toLowerCase() : "login"; - if (intent === "bind" && userAuth) { - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u8BF7\u5148\u767B\u5F55\u540E\u518D\u7ED1\u5B9A\u5FAE\u4FE1" }); - bindUserId = me.id; - } - const redirectUrl = await wechatOAuthService.buildAuthorizeRedirect(req, { bindUserId }); - return res.redirect(302, redirectUrl); - } catch (err) { - console.error("WeChat authorize failed:", err); - return res.status(500).json({ message: err instanceof Error ? err.message : "\u5FAE\u4FE1\u6388\u6743\u5931\u8D25" }); - } -}); -app.get("/auth/wechat/callback", async (req, res) => { - await userAuthReady; - const secure = isSecureRequest(req); - if (!wechatOAuthService?.enabled || !userAuth) { - return res.redirect(302, "/?wechat_error=unavailable"); - } - try { - const result = await wechatOAuthService.handleCallback({ - code: typeof req.query?.code === "string" ? req.query.code : "", - state: typeof req.query?.state === "string" ? req.query.state : "", - ip: req.ip - }); - if (result.action === "binding_gate") { - const params = new URLSearchParams(); - params.set("wechat_pending", result.pendingToken); - if (result.returnTo && result.returnTo !== "/") { - params.set("return_to", result.returnTo); - } - return res.redirect(302, `/?${params.toString()}`); - } - if (result.action === "poll_error") { - return res.redirect( - 302, - `/?wechat_error=${encodeURIComponent(result.message || "\u5FAE\u4FE1\u767B\u5F55\u5931\u8D25")}` - ); - } - if (result.isNewUser && plazaSeo) { - void plazaSeo.recordAttribution( - { - event_type: "signup", - utm_source: result.utmSource || "wechat", - utm_medium: result.utmMedium, - utm_campaign: result.utmCampaign, - user_id: result.user?.id ?? null - }, - plazaClientIp(req) - ).catch(() => { - }); - } - if (result.authMode === "open" || result.authMode === "scan") { - return res.send(`\u5FAE\u4FE1\u767B\u5F55

\u626B\u7801\u767B\u5F55\u6210\u529F\uFF0C\u8BF7\u8FD4\u56DE\u7535\u8111\u7EE7\u7EED\u64CD\u4F5C\u3002

`); - } - setUserLoginCookies(res, req, result.token); - return res.redirect(302, result.returnTo || "/"); - } catch (err) { - console.error("WeChat callback failed:", err); - const message = encodeURIComponent(err instanceof Error ? err.message : "\u5FAE\u4FE1\u767B\u5F55\u5931\u8D25"); - return res.redirect(302, `/?wechat_error=${message}`); - } -}); -app.get("/auth/me", async (req, res) => { - await userAuthReady; - if (!userAuth) return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const [paths, capabilityState, subscription] = await Promise.all([ - userAuth.listPathGrants(me.id), - userAuth.resolveUserCapabilities(await userAuth.getUserById(me.id)), - subscriptionService ? subscriptionService.getActiveSubscription(me.id) : null - ]); - return res.json({ - user: { ...me, subscription }, - paths, - capabilities: capabilityState.capabilities, - grantedSkills: capabilityState.grantedSkills ?? [], - unrestricted: capabilityState.unrestricted - }); -}); -app.post("/auth/logout", async (req, res) => { - await userAuthReady; - const secure = isSecureRequest(req); - if (userAuth) { - await userAuth.revoke(userToken(req)); - clearUserLoginCookies(res, req); - } - if (legacyAuth) { - legacyAuth.revoke(legacySessionToken(req)); - res.set("Set-Cookie", clearSessionCookie(secure)); - } - res.status(204).end(); -}); -app.get("/auth/usage", async (req, res) => { - await userAuthReady; - if (!userAuth) return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const records = await userAuth.listUsageRecords({ userId: me.id, limit: 30 }); - res.json({ records }); -}); -app.post("/auth/feedback", jsonBody, async (req, res) => { - await userAuthReady; - if (!userAuth || !feedbackService) { - return res.status(503).json({ message: "\u53CD\u9988\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - try { - const result = await feedbackService.submit(me.id, { - type: req.body?.type, - title: req.body?.title, - description: req.body?.description, - contact: req.body?.contact, - images: req.body?.images, - context: req.body?.context - }); - res.status(201).json({ feedback: result }); - } catch (err) { - const code = err && typeof err === "object" && "code" in err ? String(err.code) : ""; - if (code === "invalid_input") { - return res.status(400).json({ message: err instanceof Error ? err.message : "\u63D0\u4EA4\u5185\u5BB9\u65E0\u6548" }); - } - console.warn("Submit feedback failed:", err instanceof Error ? err.message : err); - return res.status(500).json({ message: "\u53CD\u9988\u63D0\u4EA4\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" }); - } -}); -app.get("/auth/feedback", async (req, res) => { - await userAuthReady; - if (!userAuth || !feedbackService) { - return res.status(503).json({ message: "\u53CD\u9988\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const limit = Math.min(Math.max(Number(req.query?.limit) || 20, 1), 50); - const items = await feedbackService.listForUser(me.id, { limit }); - res.json({ items }); -}); -app.get("/auth/feedback/board", async (req, res) => { - await userAuthReady; - if (!userAuth || !feedbackService) { - return res.status(503).json({ message: "\u53CD\u9988\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const page = Math.max(Number(req.query?.page) || 1, 1); - const limit = Math.min(Math.max(Number(req.query?.limit) || 10, 1), 10); - try { - const result = await feedbackService.listAll({ page, limit }); - res.json(result); - } catch (err) { - console.warn("List feedback board failed:", err instanceof Error ? err.message : err); - return res.status(500).json({ message: "\u53CD\u9988\u5217\u8868\u52A0\u8F7D\u5931\u8D25" }); - } -}); -app.get("/auth/feedback/:feedbackId", async (req, res) => { - await userAuthReady; - if (!userAuth || !feedbackService) { - return res.status(503).json({ message: "\u53CD\u9988\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - try { - const item = await feedbackService.getById(req.params.feedbackId, me.id); - res.json({ item, isMine: item.userId === me.id }); - } catch (err) { - const code = err && typeof err === "object" && "code" in err ? String(err.code) : ""; - if (code === "feedback_not_found") { - return res.status(404).json({ message: err instanceof Error ? err.message : "\u53CD\u9988\u4E0D\u5B58\u5728" }); - } - console.warn("Get feedback detail failed:", err instanceof Error ? err.message : err); - return res.status(500).json({ message: "\u53CD\u9988\u8BE6\u60C5\u52A0\u8F7D\u5931\u8D25" }); - } -}); -app.get("/auth/notifications", async (req, res) => { - await userAuthReady; - if (!userAuth || !scheduleService) { - return res.status(503).json({ message: "\u901A\u77E5\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const rawStatus = typeof req.query?.status === "string" ? req.query.status : "unread"; - const status = ["all", "unread", "read"].includes(rawStatus) ? rawStatus : "all"; - const limit = Math.min(Math.max(Number(req.query?.limit) || 20, 1), 100); - try { - const notifications = await scheduleService.listUserNotifications({ userId: me.id, status, limit }); - res.json({ notifications }); - } catch (err) { - console.warn("List user notifications failed:", err instanceof Error ? err.message : err); - res.status(500).json({ message: "\u901A\u77E5\u5217\u8868\u52A0\u8F7D\u5931\u8D25" }); - } -}); -app.get("/auth/notifications/events", async (req, res) => { - await userAuthReady; - if (!userAuth || !scheduleService) { - return res.status(503).json({ message: "\u901A\u77E5\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - res.status(200); - res.setHeader("Content-Type", "text/event-stream; charset=utf-8"); - res.setHeader("Cache-Control", "no-cache, no-transform"); - res.setHeader("Connection", "keep-alive"); - res.flushHeaders?.(); - let closed = false; - let lastNotificationId = null; - const sendEvent = (event, data) => { - if (closed || res.destroyed) return; - res.write(`event: ${event} -`); - res.write(`data: ${JSON.stringify(data)} - -`); - }; - const checkUnread = async () => { - if (closed) return; - try { - const notifications = await scheduleService.listUserNotifications({ - userId: me.id, - status: "unread", - limit: 1 - }); - const latest = notifications[0] ?? null; - const nextId = latest?.id ?? null; - if (nextId && nextId !== lastNotificationId) { - lastNotificationId = nextId; - sendEvent("notification", { notification: latest }); - } else if (!nextId) { - lastNotificationId = null; - } - } catch { - sendEvent("sync", { reason: "check_failed" }); - } - }; - sendEvent("ready", { ok: true }); - await checkUnread(); - const checkTimer = setInterval(() => { - void checkUnread(); - }, 2500); - const keepaliveTimer = setInterval(() => { - sendEvent("ping", { at: Date.now() }); - }, 25e3); - req.on("close", () => { - closed = true; - clearInterval(checkTimer); - clearInterval(keepaliveTimer); - }); -}); -app.post("/auth/notifications/:id/read", async (req, res) => { - await userAuthReady; - if (!userAuth || !scheduleService) { - return res.status(503).json({ message: "\u901A\u77E5\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const ok = await scheduleService.markUserNotificationRead({ - userId: me.id, - notificationId: req.params.id - }); - if (!ok) return res.status(404).json({ message: "\u901A\u77E5\u4E0D\u5B58\u5728\u6216\u5DF2\u8BFB" }); - res.json({ ok: true }); -}); -app.post("/auth/notifications/read-all", async (req, res) => { - await userAuthReady; - if (!userAuth || !scheduleService) { - return res.status(503).json({ message: "\u901A\u77E5\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const updated = await scheduleService.markAllUserNotificationsRead({ userId: me.id }); - res.json({ ok: true, updated }); -}); -app.delete("/auth/notifications/:id", async (req, res) => { - await userAuthReady; - if (!userAuth || !scheduleService) { - return res.status(503).json({ message: "\u901A\u77E5\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const ok = await scheduleService.deleteUserNotification({ - userId: me.id, - notificationId: req.params.id - }); - if (!ok) return res.status(404).json({ message: "\u901A\u77E5\u4E0D\u5B58\u5728" }); - res.status(204).end(); -}); -app.delete("/auth/notifications", async (req, res) => { - await userAuthReady; - if (!userAuth || !scheduleService) { - return res.status(503).json({ message: "\u901A\u77E5\u670D\u52A1\u672A\u542F\u7528" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const status = typeof req.query?.status === "string" ? req.query.status : "all"; - const deleted = await scheduleService.clearUserNotifications({ userId: me.id, status }); - res.json({ ok: true, deleted }); -}); -app.get("/auth/billing/ledger", async (req, res) => { - await userAuthReady; - if (!userAuth) return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const limit = Math.min(Math.max(Number(req.query?.limit) || 30, 1), 100); - const entries = await userAuth.listBillingLedger({ - userId: me.id, - limit, - types: ["recharge", "adjust", "refund"] - }); - res.json({ entries }); -}); -app.get("/auth/billing/config", async (req, res) => { - await userAuthReady; - if (!userAuth || !rechargeService) { - return res.status(503).json({ message: "\u672A\u542F\u7528\u8BA1\u8D39\u7CFB\u7EDF" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const [config, sub] = await Promise.all([ - rechargeService.getBillingConfig(me.id), - subscriptionService ? subscriptionService.getActiveSubscription(me.id) : null - ]); - return res.json({ ...config, subscription: sub }); -}); -app.get("/auth/billing/subscription", async (req, res) => { - await userAuthReady; - if (!userAuth) return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const sub = subscriptionService ? await subscriptionService.getActiveSubscription(me.id) : null; - const planCatalog = subscriptionService?._planCatalogService; - const plans = !sub && planCatalog ? await planCatalog.listPlans({ includeInactive: false }) : sub ? void 0 : PLAN_CATALOG; - return res.json({ subscription: sub, plans }); -}); -app.get("/auth/billing/plans", async (req, res) => { - await userAuthReady; - if (!userAuth) return res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const planCatalog = subscriptionService?._planCatalogService; - const plans = planCatalog ? (await planCatalog.listPlans({ includeInactive: false })).filter((p) => p.priceCents > 0).map((p) => ({ key: p.planType, ...p })) : Object.entries(PLAN_CATALOG).filter(([, plan]) => plan.priceCents > 0).map(([key, plan]) => ({ key, ...plan })); - const [sub, wallet] = await Promise.all([ - subscriptionService ? subscriptionService.getActiveSubscription(me.id) : null, - userAuth.getUserById(me.id) - ]); - return res.json({ - plans, - subscription: sub, - balanceCents: wallet ? Number(wallet.balance_cents ?? 0) : 0 - }); -}); -app.post("/auth/billing/subscribe", jsonBody, async (req, res) => { - await userAuthReady; - if (!userAuth || !subscriptionService) { - return res.status(503).json({ message: "\u672A\u542F\u7528\u8BA2\u9605\u7CFB\u7EDF" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - if (me.status === "disabled") return res.status(403).json({ message: "\u8D26\u6237\u5DF2\u7981\u7528" }); - const { planType, autoRenew = false } = req.body ?? {}; - if (!planType || typeof planType !== "string") { - return res.status(400).json({ message: "\u8BF7\u9009\u62E9\u5957\u9910" }); - } - const result = await subscriptionService.purchaseSubscription(me.id, planType, Boolean(autoRenew)); - if (!result.ok) { - if (result.code === "INSUFFICIENT_BALANCE") { - return res.status(402).json({ - message: result.message, - code: result.code, - balanceCents: result.balanceCents, - requiredCents: result.requiredCents, - shortfallCents: result.shortfallCents - }); - } - if (result.code === "DOWNGRADE_NOT_ALLOWED") { - return res.status(409).json({ - message: result.message, - code: result.code, - currentPlanType: result.currentPlanType - }); - } - return res.status(400).json({ message: result.message }); - } - return res.json({ subscription: result.subscription, balanceCents: result.balanceCents }); -}); -app.post("/auth/billing/space-purchase", jsonBody, async (req, res) => { - await userAuthReady; - if (!userAuth || !mindSpace) { - return res.status(503).json({ message: "\u672A\u542F\u7528\u7A7A\u95F4\u7CFB\u7EDF" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const sizeMb = Number(req.body?.sizeMb); - const result = await userAuth.purchaseSpaceQuota(me.id, sizeMb); - if (!result.ok) { - if (result.code === "INSUFFICIENT_BALANCE") { - return res.status(402).json({ - message: result.message, - code: result.code, - details: { - code: "INSUFFICIENT_BALANCE", - balanceCents: result.balanceCents, - minRechargeCents: result.minRechargeCents, - suggestedTiers: result.suggestedTiers - } - }); - } - return res.status(400).json({ message: result.message }); - } - const quota = await mindSpace.getQuota(me.id); - return res.json({ - quota: quota ?? result.quota, - balanceCents: result.balanceCents, - purchasedMb: sizeMb, - costCents: sizeMb * 200 - }); -}); -app.post("/auth/billing/auto-renew", jsonBody, async (req, res) => { - await userAuthReady; - if (!userAuth || !subscriptionService) { - return res.status(503).json({ message: "\u672A\u542F\u7528\u8BA2\u9605\u7CFB\u7EDF" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const { enabled } = req.body ?? {}; - if (typeof enabled !== "boolean") { - return res.status(400).json({ message: "\u8BF7\u4F20\u5165 enabled: true/false" }); - } - const result = await subscriptionService.setAutoRenew(me.id, enabled); - return res.json(result); -}); -app.post("/auth/billing/recharge-orders", jsonBody, async (req, res) => { - await userAuthReady; - if (!userAuth || !rechargeService) { - return res.status(503).json({ message: "\u672A\u542F\u7528\u8BA1\u8D39\u7CFB\u7EDF" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const amountCents = Number(req.body?.amountCents); - const payScene = ["native", "h5", "jsapi"].includes(req.body?.payScene) ? req.body.payScene : "native"; - const result = await rechargeService.createOrder({ - userId: me.id, - amountCents, - payScene, - clientIp: req.ip - }); - if (!result.ok) return res.status(400).json({ message: result.message }); - return res.status(201).json({ order: result.order }); -}); -app.get("/auth/billing/recharge-orders/:orderId", async (req, res) => { - await userAuthReady; - if (!userAuth || !rechargeService) { - return res.status(503).json({ message: "\u672A\u542F\u7528\u8BA1\u8D39\u7CFB\u7EDF" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const order = await rechargeService.getOrderForUser(me.id, req.params.orderId); - if (!order) return res.status(404).json({ message: "\u8BA2\u5355\u4E0D\u5B58\u5728" }); - let balanceCents = null; - if (order.status === "paid") { - const user = await userAuth.getUserById(me.id); - balanceCents = user ? Number(user.balance_cents ?? 0) : null; - } - return res.json({ order, balanceCents }); -}); -var wechatNotifyBody = express2.raw({ - type: ["application/json", "text/xml", "application/xml"], - limit: "64kb" -}); -var wechatMpBody = express2.text({ - type: ["text/xml", "application/xml"], - limit: "128kb" -}); -app.post("/webhooks/wechat-pay/notify", wechatNotifyBody, async (req, res) => { - await userAuthReady; - const isV2 = wechatPayClient?.apiVersion === "v2"; - if (!rechargeService || !wechatPayClient?.enabled) { - if (isV2) { - return res.status(503).type("text/xml").send( - "" - ); - } - return res.status(503).json({ code: "FAIL", message: "\u652F\u4ED8\u672A\u542F\u7528" }); - } - try { - const bodyText = Buffer.isBuffer(req.body) ? req.body.toString("utf8") : String(req.body ?? ""); - await rechargeService.handleWechatNotify({ headers: req.headers, body: bodyText }); - if (isV2) { - return res.type("text/xml").send(WECHAT_NOTIFY_SUCCESS_V2); - } - return res.json({ code: "SUCCESS", message: "\u6210\u529F" }); - } catch (err) { - console.error("WeChat notify failed:", err); - const message = err instanceof Error ? err.message : "\u5904\u7406\u5931\u8D25"; - if (isV2) { - return res.status(500).type("text/xml").send( - `` - ); - } - return res.status(500).json({ code: "FAIL", message }); - } -}); -if (WECHAT_MP_CONFIG.enabled) { - app.get("/webhooks/wechat-mp/messages", async (req, res) => { - await userAuthReady; - if (!wechatMpService?.enabled) { - return res.status(503).send("wechat mp disabled"); - } - const result = wechatMpService.verifyUrlChallenge(req.query); - if (!result.ok) { - console.warn("WeChat MP verify failed:", { - encryptType: req.query.encrypt_type ?? null, - timestamp: req.query.timestamp ?? null, - nonce: req.query.nonce ?? null - }); - return res.status(result.status ?? 403).send(result.body ?? "invalid signature"); - } - console.log("WeChat MP verify ok:", { - encryptType: req.query.encrypt_type ?? null, - timestamp: req.query.timestamp ?? null, - nonce: req.query.nonce ?? null - }); - return res.type("text/plain").send(String(result.body ?? "")); - }); - app.post("/webhooks/wechat-mp/messages", wechatMpBody, async (req, res) => { - await userAuthReady; - if (!wechatMpService?.enabled) { - return res.status(503).send("wechat mp disabled"); - } - try { - const bodyText = String(req.body ?? ""); - const fromUser = bodyText.match(/<\/FromUserName>/)?.[1] ?? null; - const msgType = bodyText.match(/<\/MsgType>/)?.[1] ?? null; - const content = bodyText.match(/<\/Content>/)?.[1] ?? null; - console.log("WeChat MP message received:", { - at: (/* @__PURE__ */ new Date()).toISOString(), - fromUser: fromUser ? `${fromUser.slice(0, 8)}...` : null, - msgType, - contentPreview: content ? `${String(content).slice(0, 24)}` : null - }); - const result = await wechatMpService.handleInboundMessage(req.body, req.query); - if (result.task) void result.task; - if (result.contentType) res.type(result.contentType); - return res.status(result.status ?? 200).send(result.body ?? "success"); - } catch (err) { - console.error("WeChat MP message failed:", err); - return res.status(500).send("internal error"); - } - }); -} -function wikiCookie(token, secure) { - const parts = [ - `${wikiAuth.COOKIE_NAME}=${encodeURIComponent(token)}`, - "Path=/", - "HttpOnly", - "SameSite=Lax", - "Max-Age=604800" - ]; - if (secure) parts.push("Secure"); - return parts.join("; "); -} -function clearWikiCookie(secure) { - const parts = [`${wikiAuth.COOKIE_NAME}=`, "Path=/", "HttpOnly", "SameSite=Lax", "Max-Age=0"]; - if (secure) parts.push("Secure"); - return parts.join("; "); -} -var wikiApi = express2.Router(); -wikiApi.use(jsonBody); -wikiApi.post("/auth/register", (req, res) => { - const { username, password, displayName } = req.body || {}; - if (!username || !password) { - return res.status(400).json({ message: "\u7528\u6237\u540D\u548C\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A" }); - } - const result = wikiAuth.register(username, password, displayName); - if (!result.ok) return res.status(409).json({ message: result.message }); - return res.json({ ok: true, user: result.user }); -}); -wikiApi.post("/auth/login", (req, res) => { - const { username, password } = req.body || {}; - if (!username || !password) { - return res.status(400).json({ message: "\u7528\u6237\u540D\u548C\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A" }); - } - const result = wikiAuth.login(username, password); - if (!result.ok) return res.status(401).json({ message: result.message }); - const secure = isSecureRequest(req); - res.set("Set-Cookie", wikiCookie(result.token, secure)); - return res.json({ ok: true, user: result.user }); -}); -wikiApi.post("/auth/logout", (req, res) => { - const cookies = parseCookies(req.get("cookie")); - wikiAuth.revoke(cookies[wikiAuth.COOKIE_NAME]); - res.set("Set-Cookie", clearWikiCookie(isSecureRequest(req))); - return res.json({ ok: true }); -}); -wikiApi.get("/auth/me", (req, res) => { - const cookies = parseCookies(req.get("cookie")); - const session = wikiAuth.verify(cookies[wikiAuth.COOKIE_NAME]); - if (!session) return res.json({ authenticated: false, user: null }); - const user = wikiAuth.getUser(session.username); - return res.json({ authenticated: true, user }); -}); -function requireWikiAuth(req, res, next) { - const cookies = parseCookies(req.get("cookie")); - const session = wikiAuth.verify(cookies[wikiAuth.COOKIE_NAME]); - if (!session) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - req.wikiUser = session; - next(); -} -wikiApi.get("/pages", requireWikiAuth, (req, res) => { - const { q } = req.query; - if (q) return res.json(wikiAuth.searchPages(req.wikiUser.username, q)); - res.json(wikiAuth.listPages(req.wikiUser.username)); -}); -wikiApi.get("/pages/:slug", requireWikiAuth, (req, res) => { - const page = wikiAuth.getPage(req.wikiUser.username, req.params.slug); - if (!page) return res.status(404).json({ message: "\u9875\u9762\u4E0D\u5B58\u5728" }); - res.json(page); -}); -wikiApi.post("/pages/:slug", requireWikiAuth, (req, res) => { - const { title, content, tags } = req.body || {}; - const page = wikiAuth.savePage(req.wikiUser.username, req.params.slug, title, content, tags); - res.json(page); -}); -wikiApi.delete("/pages/:slug", requireWikiAuth, (req, res) => { - wikiAuth.deletePage(req.wikiUser.username, req.params.slug); - res.json({ ok: true }); -}); -app.use("/wiki-api", wikiApi); -var api = express2.Router(); -api.use(jsonUnlessMultipart); -api.use(async (req, res, next) => { - await userAuthReady; - if (req.path === "/status" || req.path === "/runtime/status") return next(); - if (req.path.startsWith("/internal/agent/")) return next(); - if (req.path === "/agent/mindspace_page_patch") return next(); - if (req.path === "/agent/mindspace_asset_delete") return next(); - if (req.path === "/config/blocked-words") return next(); - if (req.method === "GET" && /^\/mindspace\/v1\/assets\/[^/]+\/download$/.test(req.path)) { - return next(); - } - const plazaPublic = isPlazaPublicRead(req.path, req.method); - if (userAuth && tkmindProxy) { - if (req.userSessionError) { - if (plazaPublic) return next(); - return res.status(503).json({ message: "\u7528\u6237\u8BA4\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" }); - } - try { - if (req.userSession) { - const me2 = await userAuth.getMe(req.userToken); - if (me2) req.currentUser = me2; - } - if (plazaPublic) return next(); - if (!req.userSession) { - return res.status(401).json({ message: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }); - } - const me = await userAuth.getMe(req.userToken); - if (!me) return res.status(401).json({ message: "\u767B\u5F55\u5DF2\u8FC7\u671F" }); - req.currentUser = me; - return next(); - } catch (err) { - console.error("[Auth] API auth failed:", err instanceof Error ? err.message : err); - return res.status(503).json({ message: "\u7528\u6237\u8BA4\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" }); - } - } - if (legacyAuth?.verify(legacySessionToken(req))) return next(); - return res.status(401).json({ message: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }); -}); -attachAsrRoutes(api, { sendError, sendData }); -api.get("/config/blocked-words", async (_req, res) => { - await userAuthReady; - if (!wordFilterService) return res.json({ words: [] }); - const words = await wordFilterService.listAllForFrontend(); - res.json({ words }); -}); -api.get("/status", async (_req, res, next) => { - await userAuthReady; - if (userAuth && tkmindProxy) { - try { - const upstream = await tkmindProxy.apiFetch("/status", { method: "GET" }); - const text = await upstream.text(); - return res.status(upstream.status).send(text); - } catch (err) { - return res.status(502).json({ message: err instanceof Error ? err.message : "\u4EE3\u7406\u5931\u8D25" }); - } - } - return next(); -}); -function runtimeEnvFlag(value, fallback = false) { - const raw = String(value ?? "").trim().toLowerCase(); - if (!raw) return fallback; - return ["1", "true", "yes", "on"].includes(raw); -} -function runtimeCsvList(value) { - return String(value ?? "").split(",").map((item) => item.trim()).filter(Boolean); -} -function runtimeCodeRunPolicyStatus() { - return { - enabled: runtimeEnvFlag(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), - userAllowlist: runtimeCsvList(process.env.MEMIND_AGENT_CODE_RUNS_USER_IDS), - taskTypeAllowlist: runtimeCsvList(process.env.MEMIND_AGENT_CODE_RUN_TASK_TYPES), - requireValidation: runtimeEnvFlag(process.env.MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION) - }; -} -api.get("/runtime/status", async (_req, res) => { - await userAuthReady; - if (!tkmindProxy?.getRuntimeStatus) { - return res.status(503).json({ ok: false, message: "runtime router unavailable" }); - } - try { - const status = await tkmindProxy.getRuntimeStatus(); - const toolQueue = agentRunGateway?.getQueueStatus ? await agentRunGateway.getQueueStatus().catch((err) => ({ - error: err instanceof Error ? err.message : String(err) - })) : null; - if (toolQueue) { - status.toolRuntime = { - ...status.toolRuntime ?? {}, - codeRunPolicy: runtimeCodeRunPolicyStatus(), - queue: toolQueue - }; - } - return res.json({ - ok: true, - timestamp: (/* @__PURE__ */ new Date()).toISOString(), - ...status - }); - } catch (err) { - return res.status(502).json({ - ok: false, - message: err instanceof Error ? err.message : "runtime status failed" - }); - } -}); -async function ensureUserMemoryCapability(req, res) { - if (!userAuth) { - res.status(503).json({ message: "\u672A\u542F\u7528\u7528\u6237\u7CFB\u7EDF" }); - return null; - } - const userRow = await userAuth.getUserById(req.currentUser.id); - if (!userRow) { - res.status(404).json({ message: "\u7528\u6237\u4E0D\u5B58\u5728" }); - return null; - } - const capabilityState = await userAuth.resolveUserCapabilities(userRow); - if (!capabilityState.unrestricted && !capabilityState.capabilities.memory_store) { - res.status(403).json({ message: "\u5F53\u524D\u8D26\u6237\u672A\u5F00\u901A\u957F\u671F\u8BB0\u5FC6\uFF0C\u65E0\u6CD5\u8BBF\u95EE\u8BE5 API" }); - return null; - } - return capabilityState; -} -async function loadUserVisibleConversation(sessionId) { - const target = await tkmindProxy.resolveTarget(sessionId); - const upstream = await tkmindProxy.apiFetchTo(target, `/sessions/${encodeURIComponent(sessionId)}`, { - method: "GET" - }); - if (!upstream.ok) { - const message = await upstream.text().catch(() => ""); - throw new Error(message || "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25"); - } - const session = await upstream.json(); - return (session?.conversation ?? []).filter((message) => message?.metadata?.userVisible); -} -async function syncUserMemoriesIntoSession(userId, sessionId) { - if (!tkmindProxy || !sessionId) return false; - await tkmindProxy.reconcileSessionPolicyForUser(userId, sessionId); - return true; -} -api.post("/user-memory/v1/remember-recent", async (req, res) => { - if (!conversationMemoryService?.isEnabled?.()) { - return res.status(503).json({ message: "\u957F\u671F\u8BB0\u5FC6\u529F\u80FD\u672A\u542F\u7528" }); - } - if (!tkmindProxy) { - return res.status(503).json({ message: "\u4F1A\u8BDD\u4EE3\u7406\u5C1A\u672A\u5C31\u7EEA" }); - } - const capabilityState = await ensureUserMemoryCapability(req, res); - if (!capabilityState) return; - const sessionId = String(req.body?.sessionId ?? "").trim(); - if (!sessionId) { - return res.status(400).json({ message: "\u7F3A\u5C11 sessionId" }); - } - const owns = await userAuth.ownsSession(req.currentUser.id, sessionId); - if (!owns) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - try { - const messages = await loadUserVisibleConversation(sessionId); - const result = await conversationMemoryService.saveAndAnalyze( - sessionId, - req.currentUser.id, - messages - ); - const syncedToSession = await syncUserMemoriesIntoSession(req.currentUser.id, sessionId); - const memories = await conversationMemoryService.listMemories(req.currentUser.id, { limit: 200 }); - return res.json({ - ok: true, - analyzed: result.analyzed ?? 0, - memories: result.memories ?? 0, - totalMemories: memories.length, - syncedToSession - }); - } catch (err) { - return res.status(500).json({ message: err instanceof Error ? err.message : "\u4FDD\u5B58\u957F\u671F\u8BB0\u5FC6\u5931\u8D25" }); - } -}); -api.post("/user-memory/v1/sync", async (req, res) => { - if (!conversationMemoryService?.isEnabled?.()) { - return res.status(503).json({ message: "\u957F\u671F\u8BB0\u5FC6\u529F\u80FD\u672A\u542F\u7528" }); - } - const capabilityState = await ensureUserMemoryCapability(req, res); - if (!capabilityState) return; - const sessionId = String(req.body?.sessionId ?? "").trim(); - if (!sessionId) { - return res.status(400).json({ message: "\u7F3A\u5C11 sessionId" }); - } - const owns = await userAuth.ownsSession(req.currentUser.id, sessionId); - if (!owns) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - try { - const result = await conversationMemoryService.analyzeUser(req.currentUser.id); - const syncedToSession = await syncUserMemoriesIntoSession(req.currentUser.id, sessionId); - const memories = await conversationMemoryService.listMemories(req.currentUser.id, { limit: 200 }); - return res.json({ - ok: true, - analyzed: result.analyzed ?? 0, - memories: result.memories ?? 0, - totalMemories: memories.length, - syncedToSession - }); - } catch (err) { - return res.status(500).json({ message: err instanceof Error ? err.message : "\u5237\u65B0\u957F\u671F\u8BB0\u5FC6\u5931\u8D25" }); - } -}); -api.get("/mindspace/v1/space", async (req, res) => { - if (!mindSpace || !ensureMindSpaceEnabled(res, req)) return; - const space = await mindSpace.getSpace(req.currentUser.id); - if (!space) return sendError(res, req, 404, "resource_not_found", "\u7528\u6237\u7A7A\u95F4\u4E0D\u5B58\u5728"); - return sendData(res, req, space); -}); -api.post("/mindspace/v1/schedule/reminders/:reminderId/ignore", async (req, res) => { - if (!ensureMindSpaceEnabled(res, req) || !scheduleService) { - return sendError(res, req, 503, "feature_disabled", "\u65E5\u7A0B\u670D\u52A1\u672A\u542F\u7528"); - } - try { - const reminder = await scheduleService.cancelReminder({ - userId: req.currentUser.id, - reminderId: req.params.reminderId - }); - return sendData(res, req, reminder); - } catch (err) { - const message = err instanceof Error ? err.message : "\u5FFD\u7565\u63D0\u9192\u5931\u8D25"; - return sendError(res, req, 400, "invalid_schedule_input", message); - } -}); -api.post("/mindspace/v1/schedule/reminders/bulk-delete", async (req, res) => { - if (!ensureMindSpaceEnabled(res, req) || !scheduleService) { - return sendError(res, req, 503, "feature_disabled", "\u65E5\u7A0B\u670D\u52A1\u672A\u542F\u7528"); - } - try { - const ids = Array.isArray(req.body?.ids) ? req.body.ids.map(String) : []; - const deleted = await scheduleService.deleteReminders({ - userId: req.currentUser.id, - reminderIds: ids - }); - return sendData(res, req, { deleted }); - } catch (err) { - const message = err instanceof Error ? err.message : "\u5220\u9664\u63D0\u9192\u5931\u8D25"; - return sendError(res, req, 400, "invalid_schedule_input", message); - } -}); -api.get("/mindspace/v1/space/quota", async (req, res) => { - if (!mindSpace) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - const quota = await mindSpace.getQuota(req.currentUser.id); - if (!quota) return res.status(404).json({ message: "\u7528\u6237\u7A7A\u95F4\u4E0D\u5B58\u5728" }); - return res.json({ data: quota }); -}); -api.get("/mindspace/v1/space/categories", async (req, res) => { - if (!mindSpace) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - const categories = await mindSpace.listCategories(req.currentUser.id); - if (!categories) return res.status(404).json({ message: "\u7528\u6237\u7A7A\u95F4\u4E0D\u5B58\u5728" }); - return res.json({ data: categories }); -}); -api.get("/mindspace/v1/space/cleanup", async (req, res) => { - if (!mindSpaceCleanup || !ensureMindSpaceEnabled(res, req)) return; - try { - const username = req.currentUser.username ?? req.currentUser.slug; - const items = await mindSpaceCleanup.listCandidates(req.currentUser.id, username); - const totalBytes = items.reduce((sum, item) => sum + item.sizeBytes, 0); - return sendData(res, req, { items, totalBytes }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/space/cleanup", async (req, res) => { - if (!mindSpaceCleanup || !ensureMindSpaceEnabled(res, req)) return; - try { - const username = req.currentUser.username ?? req.currentUser.slug; - const itemIds = Array.isArray(req.body?.item_ids) ? req.body.item_ids : []; - const result = await mindSpaceCleanup.runCleanup(req.currentUser.id, username, itemIds); - const quota = await mindSpace.getQuota(req.currentUser.id); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "space.cleanup", - objectType: "space", - objectId: req.currentUser.id, - ip: req.ip, - detail: { removedCount: result.removedCount, freedBytes: result.freedBytes } - }); - return sendData(res, req, { ...result, quota }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -function isPlazaPublicRead(path35, method) { - if (!path35.startsWith("/plaza/v1/")) return false; - if (method === "POST" && path35 === "/plaza/v1/events") return true; - if (method !== "GET") return false; - return path35 === "/plaza/v1/feed" || path35 === "/plaza/v1/categories" || path35 === "/plaza/v1/seo/sitemap" || /^\/plaza\/v1\/posts\/[^/]+$/.test(path35) || /^\/plaza\/v1\/posts\/[^/]+\/comments$/.test(path35) || /^\/plaza\/v1\/users\/[^/]+$/.test(path35) || /^\/plaza\/v1\/users\/[^/]+\/posts$/.test(path35); -} -var PLAZA_SID_COOKIE = "plaza_sid"; -function resolvePlazaSessionId(req, res) { - const cookies = parseCookies(req.get("cookie")); - let sessionId = cookies[PLAZA_SID_COOKIE]; - if (!sessionId) { - sessionId = crypto36.randomUUID(); - res.append( - "Set-Cookie", - `${PLAZA_SID_COOKIE}=${sessionId}; Path=/; Max-Age=31536000; SameSite=Lax; HttpOnly` - ); - } - return sessionId; -} -function recordPlazaEventsAsync(req, res, events) { - if (!plazaEvents || !Array.isArray(events) || events.length === 0) return; - const sessionId = resolvePlazaSessionId(req, res); - void plazaEvents.recordEvents({ - userId: req.currentUser?.id ?? null, - sessionId, - events - }).catch(() => { - }); -} -function reactionEventType(type) { - if (type === "like" || type === "collect" || type === "share") return type; - return null; -} -function ensurePlazaInteractions(res, req) { - if (!plazaInteractions) { - sendError(res, req, 503, "plaza_unavailable", "Plaza \u672A\u542F\u7528"); - return false; - } - return true; -} -function plazaRouteError(res, req, error) { - const status = mapPlazaError(error); - const code = error?.code ?? "internal_error"; - const message = error instanceof Error ? error.message : "Plaza \u8BF7\u6C42\u5931\u8D25"; - return sendError(res, req, status, code, message, error?.details); -} -function ensurePlazaEnabled(res, req) { - if (!plazaPosts) { - sendError(res, req, 503, "plaza_unavailable", "Plaza \u672A\u542F\u7528"); - return false; - } - return true; -} -function plazaClientIp(req) { - const forwarded = req.headers["x-forwarded-for"]; - if (typeof forwarded === "string" && forwarded.length > 0) { - return forwarded.split(",")[0].trim(); - } - return req.ip; -} -function mindSpaceError(res, req, error) { - const statusByCode = { - invalid_filename: 400, - invalid_file_size: 400, - category_not_uploadable: 400, - file_size_mismatch: 409, - invalid_upload_state: 409, - file_too_large: 413, - unsupported_file_type: 415, - category_not_found: 404, - upload_not_found: 404, - asset_not_found: 404, - asset_in_use: 409, - page_not_found: 404, - upload_expired: 410, - quota_exceeded: 429, - public_page_limit_exceeded: 429, - space_unavailable: 423, - invalid_page_input: 400, - page_content_too_large: 413, - source_message_not_found: 404, - invalid_source_message: 409, - version_conflict: 409, - slug_conflict: 409, - invalid_state_transition: 409, - invalid_publish_input: 400, - publication_not_found: 404, - security_scan_required: 422, - security_ack_required: 422, - security_risk_blocked: 422, - invalid_agent_job_input: 400, - invalid_agent_job_output: 400, - agent_job_not_found: 404, - agent_job_token_invalid: 401, - agent_job_expired: 410, - feature_disabled: 503, - cover_ai_unavailable: 503, - llm_not_configured: 503, - cover_ai_failed: 502, - cover_ai_invalid_output: 422, - thumbnail_not_supported: 422, - thumbnail_image_required: 400, - thumbnail_image_invalid: 400, - category_not_pageable: 400, - redaction_not_needed: 409, - invalid_category_code: 400, - invalid_page_path: 400, - empty_page_content: 400, - static_page_not_found: 404, - preview_not_supported: 422 - }; - const code = error?.code ?? "internal_error"; - const status = statusByCode[code] ?? 500; - const message = status === 500 && (!error?.code || code === "internal_error") ? "MindSpace \u670D\u52A1\u5F02\u5E38" : error?.message || "MindSpace \u670D\u52A1\u5F02\u5E38"; - return sendError(res, req, status, code, message, error?.details); -} -function ensureMindSpaceEnabled(res, req, { upload = false, agent = false } = {}) { - try { - assertMindSpaceRoute(msFlags, upload ? "upload" : agent ? "agent" : void 0); - return true; - } catch (error) { - mindSpaceError(res, req, error); - return false; - } -} -function bearerToken(req) { - const header = req.get("authorization") ?? ""; - const [scheme, value] = header.split(/\s+/, 2); - if (scheme?.toLowerCase() !== "bearer" || !value) return null; - return value.trim(); -} -function requireInternalAgentSecret(req, res) { - if (bearerToken(req) === INTERNAL_AGENT_SECRET) return true; - sendError(res, req, 401, "agent_job_token_invalid", "\u5185\u90E8 Agent \u51ED\u636E\u65E0\u6548"); - return false; -} -api.post("/mindspace/v1/agent/jobs", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - try { - const job = await mindSpaceAgentJobs.createJob(req.currentUser.id, { - jobType: req.body?.job_type, - instruction: req.body?.instruction, - allowedAssetIds: req.body?.allowed_asset_ids, - outputCategoryId: req.body?.output_category_id, - outputType: req.body?.output_type, - idempotencyKey: req.body?.idempotency_key, - locale: req.body?.locale, - timezone: req.body?.timezone, - capabilities: req.body?.capabilities - }); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "agent_access", - objectType: "agent_job", - objectId: job.id, - ip: req.ip, - detail: { - jobType: job.jobType, - assetIds: job.assets.map((asset) => asset.assetId) - } - }); - return sendData(res, req, job, 201); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/agent/jobs/:jobId", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - try { - return sendData(res, req, await mindSpaceAgentJobs.getJob(req.currentUser.id, req.params.jobId)); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/agent/jobs/:jobId/stream", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "timed_out"]); - let job; - try { - job = await mindSpaceAgentJobs.getJob(req.currentUser.id, req.params.jobId); - } catch (error) { - return mindSpaceError(res, req, error); - } - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache, no-transform", - Connection: "keep-alive", - "X-Accel-Buffering": "no" - }); - const send = (event, payload) => { - res.write(`event: ${event} -`); - res.write(`data: ${JSON.stringify(payload)} - -`); - }; - let lastSignature = ""; - const emitIfChanged = (current) => { - const signature = `${current.status}:${JSON.stringify(current.progress ?? {})}`; - if (signature !== lastSignature) { - lastSignature = signature; - send("progress", current); - } - return signature; - }; - emitIfChanged(job); - if (TERMINAL.has(job.status)) { - send("done", job); - return res.end(); - } - let closed = false; - const cleanup = () => { - if (closed) return; - closed = true; - clearInterval(pollTimer); - clearInterval(keepAliveTimer); - }; - const pollTimer = setInterval(async () => { - if (closed) return; - try { - const current = await mindSpaceAgentJobs.getJob(req.currentUser.id, req.params.jobId); - emitIfChanged(current); - if (TERMINAL.has(current.status)) { - send("done", current); - cleanup(); - res.end(); - } - } catch { - cleanup(); - res.end(); - } - }, Math.max(500, Number(process.env.MINDSPACE_AGENT_SSE_POLL_MS ?? 1e3))); - const keepAliveTimer = setInterval(() => { - if (!closed) res.write(": keep-alive\n\n"); - }, 15e3); - pollTimer.unref?.(); - keepAliveTimer.unref?.(); - req.on("close", cleanup); -}); -api.get("/mindspace/v1/agent/jobs", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - try { - const result = await mindSpaceAgentJobs.listJobs(req.currentUser.id, { - limit: Number(req.query.limit ?? 10), - offset: Number(req.query.offset ?? 0) - }); - return res.json({ - data: result.items, - page: { - total: result.total, - offset: result.offset, - limit: result.limit, - has_more: result.hasMore - }, - request_id: req.requestId - }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/agent/jobs/:jobId/cancel", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - try { - return sendData( - res, - req, - await mindSpaceAgentJobs.cancelJob(req.currentUser.id, req.params.jobId) - ); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/agent/jobs/:jobId/retry", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - try { - return sendData( - res, - req, - await mindSpaceAgentJobs.retryJob(req.currentUser.id, req.params.jobId) - ); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/agent/jobs/:jobId/run", async (req, res) => { - if (!mindSpaceAgentJobs || !mindSpaceAgentRunner || !ensureMindSpaceEnabled(res, req, { agent: true })) { - return; - } - try { - const job = await mindSpaceAgentJobs.getJob(req.currentUser.id, req.params.jobId); - if (job.status !== "queued") { - return sendData(res, req, job); - } - void mindSpaceAgentRunner.runJob(req.params.jobId).catch((error) => { - console.error("MindSpace agent job run failed:", error); - }); - return sendData(res, req, { started: true, jobId: req.params.jobId }, 202); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/internal/agent/jobs/:jobId/claim", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - if (!requireInternalAgentSecret(req, res)) return; - try { - return sendData(res, req, await mindSpaceAgentJobs.claimJob(req.params.jobId)); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/internal/agent/jobs/:jobId/assets/:assetId", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - try { - const asset = await mindSpaceAgentJobs.getAssetForJob( - req.params.jobId, - bearerToken(req), - req.params.assetId - ); - res.set("Content-Type", asset.mimeType); - res.set("Content-Disposition", `inline; filename="${encodeURIComponent(asset.displayName)}"`); - res.set("Cache-Control", "private, no-store"); - res.setHeader("X-Request-Id", req.requestId); - return res.send(await fs32.promises.readFile(asset.path)); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/internal/agent/jobs/:jobId/heartbeat", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - try { - return sendData( - res, - req, - await mindSpaceAgentJobs.heartbeat(req.params.jobId, bearerToken(req), { - stage: req.body?.stage, - message: req.body?.message - }) - ); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/internal/agent/jobs/:jobId/complete", async (req, res) => { - if (!mindSpaceAgentJobs || !ensureMindSpaceEnabled(res, req, { agent: true })) return; - try { - const job = await mindSpaceAgentJobs.completeJob(req.params.jobId, bearerToken(req), { - status: req.body?.status, - errorCode: req.body?.error_code, - errorMessage: req.body?.error_message, - outputType: req.body?.output_type, - title: req.body?.title, - summary: req.body?.summary, - content: req.body?.content, - contentFormat: req.body?.content_format, - pageType: req.body?.page_type, - templateId: req.body?.template_id, - sourceAssetIds: req.body?.source_asset_ids - }); - return sendData(res, req, job); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/conversation-packages/:sessionId", createGetConversationPackageHandler({ - getRegistry: () => mindSpaceConversationPackageRegistry, - getUserAuth: () => userAuth, - ensureMindSpaceEnabled, - sendData, - mindSpaceError, - beforeReadManifest: ({ req, sessionId }) => mindSpaceServiceFacade?.prepareConversationPackageRead({ - user: req.currentUser, - sessionId - }) -})); -api.get( - "/mindspace/v1/conversation-packages/:sessionId/manifest.json", - createDownloadConversationPackageManifestHandler({ - getRegistry: () => mindSpaceConversationPackageRegistry, - getUserAuth: () => userAuth, - ensureMindSpaceEnabled, - mindSpaceError, - beforeReadManifest: ({ req, sessionId }) => mindSpaceServiceFacade?.prepareConversationPackageRead({ - user: req.currentUser, - sessionId - }) - }) -); -api.get( - "/mindspace/v1/conversation-packages/:sessionId/artifacts/:artifactId/download", - createDownloadConversationPackageArtifactHandler({ - getRegistry: () => mindSpaceConversationPackageRegistry, - getUserAuth: () => userAuth, - ensureMindSpaceEnabled, - mindSpaceError - }) -); -api.post("/mindspace/v1/uploads", async (req, res) => { - if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req, { upload: true })) return; - try { - const upload = await mindSpaceAssets.createUpload(req.currentUser.id, { - categoryId: req.body?.category_id, - filename: req.body?.filename, - sizeBytes: req.body?.size_bytes, - declaredMimeType: req.body?.declared_mime_type, - sourceSessionId: req.body?.session_id, - sourceMessageId: req.body?.message_id - }); - return sendData(res, req, upload, 201); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.put("/mindspace/v1/uploads/:uploadId/content", rawUploadBody, async (req, res) => { - if (!mindSpaceAssets) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const result = await mindSpaceAssets.writeUploadContent( - req.currentUser.id, - req.params.uploadId, - req.body - ); - return res.json({ data: result }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/uploads/:uploadId/complete", async (req, res) => { - if (!mindSpaceAssets) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const asset = await mindSpaceAssets.completeUpload( - req.currentUser.id, - req.params.uploadId - ); - return res.status(201).json({ data: asset }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/conversation-packages/:sessionId/claim-uploads", async (req, res) => { - if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req, { upload: true })) return; - try { - const result = await mindSpaceAssets.claimUploadArtifactsForConversation(req.currentUser.id, { - sessionId: req.params.sessionId, - messageId: req.body?.message_id - }); - return sendData(res, req, result); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.delete("/mindspace/v1/uploads/:uploadId", async (req, res) => { - if (!mindSpaceAssets) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const result = await mindSpaceAssets.cancelUpload( - req.currentUser.id, - req.params.uploadId - ); - return res.json({ data: result }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/assets", async (req, res) => { - if (!mindSpaceAssets) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const assets = await mindSpaceAssets.listAssets(req.currentUser.id, { - categoryId: typeof req.query.category_id === "string" ? req.query.category_id : void 0, - categoryCode: typeof req.query.category_code === "string" ? req.query.category_code : void 0 - }); - return res.json({ data: assets, page: { next_cursor: null, has_more: false } }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/authorize-image", async (req, res) => { - if (!mindSpacePublications) { - return res.status(503).json({ error: "Service unavailable" }); - } - const assetId = String(req.query.asset_id ?? ""); - if (!assetId) { - return res.status(400).json({ error: "Missing asset_id parameter" }); - } - try { - const [refs] = await pool.query( - `SELECT pr.access_mode, pr.expires_at - FROM h5_publication_asset_refs refs - JOIN h5_publish_records pr ON refs.publication_id = pr.id - WHERE refs.asset_id = ? AND pr.status = 'online' - ORDER BY CASE - WHEN pr.access_mode = 'public' THEN 0 - WHEN pr.access_mode = 'time_limited' THEN 1 - ELSE 2 - END, - pr.expires_at DESC - LIMIT 1`, - [assetId] - ); - if (!refs[0]) { - return res.status(403).json({ error: "Forbidden" }); - } - const publication = refs[0]; - const now = Date.now(); - if (publication.access_mode === "public") { - res.set("Cache-Control", "public, max-age=31536000, immutable"); - return res.status(200).json({ ok: true }); - } - if (publication.access_mode === "time_limited" && publication.expires_at && Number(publication.expires_at) > now) { - res.set("Cache-Control", "public, max-age=60"); - return res.status(200).json({ ok: true }); - } - return res.status(403).json({ error: "Forbidden" }); - } catch (error) { - console.error("[authorize-image]", error instanceof Error ? error.message : error); - return res.status(500).json({ error: "Internal server error" }); - } -}); -api.get("/mindspace/v1/assets/:assetId/download", async (req, res) => { - if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return; - try { - const assetId = req.params.assetId; - const currentUserId = req.currentUser?.id ?? null; - let allowedByPublication = false; - let publicationAccessMode = null; - if (!currentUserId) { - const referrer = String(req.get("referer") ?? req.get("referrer") ?? ""); - const isPublicPageReferrer = (() => { - if (!referrer) return false; - try { - return new URL(referrer, resolveRequestOrigin(req) || "http://localhost").pathname.includes("/public/"); - } catch { - return referrer.includes("/public/"); - } - })(); - const [refs] = await authPool.query( - `SELECT pr.access_mode, pr.expires_at - FROM h5_publication_asset_refs refs - JOIN h5_publish_records pr ON refs.publication_id = pr.id - WHERE refs.asset_id = ? AND pr.status = 'online' - ORDER BY CASE - WHEN pr.access_mode = 'public' THEN 0 - WHEN pr.access_mode = 'time_limited' THEN 1 - ELSE 2 - END, - pr.expires_at DESC - LIMIT 1`, - [assetId] - ); - const publication = refs[0] ?? null; - if (publication?.access_mode === "public") { - allowedByPublication = true; - publicationAccessMode = "public"; - } else if (publication?.access_mode === "time_limited" && publication.expires_at && Number(publication.expires_at) > Date.now()) { - allowedByPublication = true; - publicationAccessMode = "time_limited"; - } - if (!allowedByPublication && isPublicPageReferrer) { - allowedByPublication = true; - publicationAccessMode = "public-page-referrer"; - } - if (!allowedByPublication) { - return res.status(401).json({ message: "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }); - } - } - const { asset, path: assetPath } = currentUserId ? await mindSpaceAssets.readAsset(currentUserId, assetId) : await mindSpaceAssets.readPublicAsset(assetId); - await mindSpaceAudit?.write({ - userId: req.currentUser?.id ?? null, - action: "asset.download", - objectType: "asset", - objectId: assetId, - ip: req.ip, - riskLevel: asset.riskLevel, - detail: allowedByPublication ? { accessMode: publicationAccessMode } : void 0 - }); - res.type(asset.mimeType); - const inline = req.query.inline === "1" || req.query.disposition === "inline" || req.get("sec-fetch-dest") === "iframe"; - if (inline && asset.mimeType.startsWith("image/") && wantsInlineImageViewer(req)) { - const downloadUrl = `/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download?inline=1`; - const html = renderImageAssetViewerHtml({ asset, downloadUrl }); - res.set("Content-Type", "text/html; charset=utf-8"); - res.set("Cache-Control", "private, no-store"); - res.setHeader("X-Request-Id", req.requestId); - return res.send(html); - } - res.set( - "Content-Disposition", - inline ? `inline; filename*=UTF-8''${encodeURIComponent(asset.filename)}` : `attachment; filename*=UTF-8''${encodeURIComponent(asset.filename)}` - ); - res.setHeader("X-Request-Id", req.requestId); - return res.sendFile(assetPath); - } catch (error) { - await mindSpaceAudit?.write({ - userId: req.currentUser?.id ?? null, - action: "asset.download", - objectType: "asset", - objectId: req.params.assetId, - ip: req.ip, - result: "denied", - riskLevel: error?.code === "security_risk_blocked" ? "high" : null - }); - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/assets/:assetId/thumbnail", async (req, res) => { - if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return; - try { - const svg = await mindSpaceAssets.renderAssetThumbnail(req.currentUser.id, req.params.assetId); - res.set("Content-Type", "image/svg+xml; charset=utf-8"); - res.set("Cache-Control", "private, max-age=300"); - res.setHeader("X-Request-Id", req.requestId); - return res.send(svg); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/assets/:assetId/preview", async (req, res) => { - if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return; - try { - const html = await mindSpaceAssets.renderAssetPreview(req.currentUser.id, req.params.assetId); - res.set("Content-Type", "text/html; charset=utf-8"); - res.set("Cache-Control", "private, no-store"); - res.setHeader("X-Request-Id", req.requestId); - return res.send(html); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/from-asset", async (req, res) => { - if (!mindSpacePages || !mindSpaceAssets) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const assetId = req.body?.asset_id; - const existingPage = await mindSpacePages.findPageBySourceAsset(req.currentUser.id, assetId); - if (existingPage) { - return sendData(res, req, { kind: "page", categoryCode: existingPage.categoryCode ?? "draft", page: existingPage }); - } - const { asset, path: assetPath } = await mindSpaceAssets.readAsset( - req.currentUser.id, - assetId - ); - if (asset.mimeType === "text/html") { - const relativePath = normalizeWorkspaceRelativePath2( - String(asset.originalFilename ?? asset.original_filename ?? "").includes("/") ? asset.originalFilename ?? asset.original_filename : `public/${asset.originalFilename ?? asset.original_filename ?? ""}` - ); - const existingByPath = relativePath ? await mindSpacePages.findPageByRelativePath(req.currentUser.id, relativePath).catch(() => null) : null; - if (existingByPath) { - return sendData(res, req, { - kind: "page", - categoryCode: existingByPath.categoryCode ?? "draft", - page: existingByPath - }); - } - } - const content = await fs32.promises.readFile(assetPath, "utf8"); - const contentFormat = asset.mimeType === "text/html" ? "html" : "markdown"; - const htmlRelativePath = contentFormat === "html" ? normalizeWorkspaceRelativePath2( - String(asset.originalFilename ?? asset.original_filename ?? "").includes("/") ? asset.originalFilename ?? asset.original_filename : `public/${asset.originalFilename ?? asset.original_filename ?? ""}` - ) : null; - const page = await mindSpacePages.createFromChat( - req.currentUser.id, - { - title: req.body?.title || asset.displayName, - summary: req.body?.summary, - content, - contentFormat, - templateId: req.body?.template_id, - categoryCode: "draft" - }, - { - assetId: asset.id, - snapshot: { - source_asset_id: asset.id, - source_category: asset.categoryCode, - content_mode: contentFormat, - ...htmlRelativePath ? { relative_path: htmlRelativePath } : {} - } - } - ); - return res.status(201).json({ data: { kind: "page", categoryCode: "draft", page } }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.delete("/mindspace/v1/assets/:assetId", async (req, res) => { - if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req)) return; - try { - const result = await mindSpaceAssets.deleteAsset(req.currentUser.id, req.params.assetId); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "asset.delete", - objectType: "asset", - objectId: req.params.assetId, - ip: req.ip - }); - return sendData(res, req, result); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -function messageText2(message) { - return (message?.content ?? []).filter((item) => item?.type === "text" && typeof item.text === "string").map((item) => item.text).join("").trim(); -} -async function resolveOwnedAssistantMessage(userId, sessionId, messageId2) { - if (!sessionId || !messageId2) { - throw Object.assign(new Error("\u7F3A\u5C11\u6765\u6E90\u4F1A\u8BDD\u6216\u6D88\u606F"), { - code: "invalid_page_input" - }); - } - if (!await userAuth.ownsSession(userId, sessionId)) { - throw Object.assign(new Error("\u6765\u6E90\u4F1A\u8BDD\u4E0D\u5B58\u5728"), { code: "source_message_not_found" }); - } - const upstream = await tkmindProxy.apiFetch(`/sessions/${encodeURIComponent(sessionId)}`, { - method: "GET" - }); - if (!upstream.ok) { - throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u6765\u6E90\u4F1A\u8BDD"), { code: "source_message_not_found" }); - } - const session = await upstream.json(); - const message = (session.conversation ?? []).find((item) => item.id === messageId2); - if (!message) { - throw Object.assign(new Error("\u6765\u6E90\u6D88\u606F\u4E0D\u5B58\u5728"), { code: "source_message_not_found" }); - } - const content = messageText2(message); - if (message.role !== "assistant" || !message.metadata?.userVisible || !content) { - throw Object.assign(new Error("\u53EA\u6709\u53EF\u89C1\u7684 AI \u6587\u672C\u6D88\u606F\u53EF\u4EE5\u4FDD\u5B58\u4E3A\u9875\u9762"), { - code: "invalid_source_message" - }); - } - return { session, message, content }; -} -async function resolveExistingSavedPage(userId, { sessionId, messageId: messageId2, relativePath } = {}) { - if (!mindSpacePages || !userId) return null; - const byMessage = await mindSpacePages.findPageBySourceMessage(userId, sessionId, messageId2).catch(() => null); - if (byMessage) return byMessage; - const normalizedPath = normalizeWorkspaceRelativePath2(relativePath); - if (!normalizedPath) return null; - return mindSpacePages.findPageByRelativePath(userId, normalizedPath).catch(() => null); -} -var SAVE_TARGET_CATEGORIES = /* @__PURE__ */ new Set(["draft", "oa", "public"]); -var pageSyncInFlight = /* @__PURE__ */ new Map(); -async function syncUserGeneratedPages(userId) { - if (!mindSpacePages || !authPool || !userId) return; - let inFlight = pageSyncInFlight.get(userId); - if (!inFlight) { - inFlight = syncGeneratedPagesFromPublicAssets({ - pool: authPool, - pageService: mindSpacePages, - assetService: mindSpaceAssets, - userId, - publishDir: resolvePublishDir(__dirname5, { id: userId }), - syncWorkspaceAssets: mindSpaceAssets && WORKSPACE_MAINTENANCE_ENABLED ? (targetUserId, options) => mindSpaceAssets.syncWorkspaceAssets(targetUserId, options) : null - }).catch((error) => { - console.warn("[MindSpace] page sync failed:", error?.message ?? error); - }).finally(() => { - if (pageSyncInFlight.get(userId) === inFlight) { - pageSyncInFlight.delete(userId); - } - }); - pageSyncInFlight.set(userId, inFlight); - } - await inFlight; -} -async function resolveChatSaveBundle(user, h5Root, input = {}) { - const sessionId = input.sessionId ?? input.session_id; - const messageId2 = input.messageId ?? input.message_id; - const selectedLinkIndex = Number(input.selectedLinkIndex ?? input.selected_link_index ?? 0); - const previewTitle = String(input.previewTitle ?? input.preview_title ?? "").trim(); - const previewSummary = String(input.previewSummary ?? input.preview_summary ?? "").trim(); - const source = await resolveOwnedAssistantMessage(user.id, sessionId, messageId2); - let { analysis, resolvedHtml } = await resolveChatSaveAnalysis({ - content: source.content, - userId: user.id, - username: user.username, - h5Root, - selectedLinkIndex - }); - if (resolvedHtml?.content && resolvedHtml.relativePath && authPool) { - const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"); - let htmlContent = resolvedHtml.content; - const repaired = await repairMissingHtmlAssetReferences({ - pool: authPool, - userId: user.id, - sourceContent: source.content, - html: htmlContent - }).catch(() => ({ html: htmlContent, changed: false })); - if (repaired.changed) { - htmlContent = repaired.html; - } - const materialized = await materializePrivateAssetsInWorkspaceHtml({ - pool: authPool, - storageRoot, - h5Root, - userId: user.id, - html: htmlContent, - htmlRelativePath: resolvedHtml.relativePath, - writeBack: false - }).catch(() => ({ html: htmlContent, count: 0, changed: false })); - if (materialized.changed) { - htmlContent = materialized.html; - } - if (htmlContent !== resolvedHtml.content) { - const publishDir = resolvePublishDir(h5Root, { id: user.id }); - await fsPromises4.writeFile( - path34.join(publishDir, resolvedHtml.relativePath), - htmlContent, - "utf8" - ); - resolvedHtml = { ...resolvedHtml, content: htmlContent }; - } - } - return { - source, - analysis, - resolvedHtml, - selectedLinkIndex, - previewTitle, - previewSummary, - previewFrameUrl: resolvedHtml != null ? buildChatSavePreviewFrameUrl({ sessionId, messageId: messageId2, selectedLinkIndex }) : null, - thumbnailUrl: resolvedHtml != null ? buildChatSaveThumbnailUrl({ - sessionId, - messageId: messageId2, - selectedLinkIndex, - previewTitle, - previewSummary - }) : null, - localPreviewUrl: resolvedHtml?.relativePath != null ? buildWorkspaceAssetUrl(user.id, resolvedHtml.relativePath) : null, - localThumbnailUrl: resolvedHtml?.relativePath != null ? buildWorkspaceThumbnailUrl(user.id, resolvedHtml.relativePath) : null - }; -} -async function registerPublishedLongImageArtifactForConversation({ - result, - image, - canonicalUrl -}) { - const ownerId = String(result?.ownerId ?? "").trim(); - const pageSource = result?.pageSource ?? {}; - const sessionId = String(pageSource.sourceSessionId ?? "").trim(); - const messageId2 = String(pageSource.sourceMessageId ?? "").trim(); - const publicationId = String(result?.publication?.id ?? "").trim(); - if (!mindSpaceConversationPackageRegistry || !ownerId || !sessionId || !publicationId || !Buffer.isBuffer(image)) { - return; - } - try { - const hash = crypto36.createHash("sha256").update(`${ownerId}:${sessionId}:${publicationId}:long-image`).digest("hex").slice(0, 16); - const artifactId = `ca_long_image_${hash}`; - const filename = `${publicationId || "published-page"}.long.png`; - const relativePath = `artifacts/${artifactId}/${filename}`; - const now = Date.now(); - const { packageRecord, writeResult } = await mindSpaceConversationPackageRegistry.putObjectForSession({ - userId: ownerId, - sessionId, - title: pageSource.title ?? null, - relativePath, - body: image - }); - await mindSpaceConversationPackageRegistry.recordArtifact({ - id: artifactId, - packageId: packageRecord.id, - artifactKind: "long_image", - role: "assistant", - pageId: pageSource.pageId ?? result?.publication?.pageId ?? null, - publicationId, - messageId: messageId2 || null, - displayName: filename, - mimeType: "image/png", - sizeBytes: image.length, - storageKey: writeResult.key, - canonicalUrl, - sortOrder: now, - now - }); - await mindSpaceConversationPackageRegistry.writeManifestForSession({ - userId: ownerId, - sessionId - }); - } catch (error) { - console.warn("[MindSpace] failed to record published long image artifact:", error?.message ?? error); - } -} -async function registerPublicHtmlArtifactsForConversation({ - user, - publishDir, - sessionId, - title = null, - relativePaths = [], - artifactRefs = [] -}) { - if (!mindSpaceConversationPackageRegistry || !user?.id || !publishDir || !sessionId || (!Array.isArray(relativePaths) || relativePaths.length === 0) && (!Array.isArray(artifactRefs) || artifactRefs.length === 0)) { - return []; - } - try { - const publishRoot = path34.resolve(publishDir); - const packageTitle = String(title ?? "").trim() || null; - const existingManifest = await mindSpaceConversationPackageRegistry.readManifestForSession({ userId: user.id, sessionId }).catch(() => null); - const packageRecord = existingManifest?.packageId && existingManifest.title ? { id: existingManifest.packageId } : await mindSpaceConversationPackageRegistry.ensurePackage({ - userId: user.id, - sessionId, - title: packageTitle ?? existingManifest?.title ?? null - }); - const recorded = []; - const now = Date.now(); - const refs = Array.isArray(artifactRefs) && artifactRefs.length > 0 ? artifactRefs : relativePaths.map((relativePath) => ({ relativePath })); - for (const ref of refs) { - const relativePath = typeof ref === "string" ? ref : ref?.relativePath; - const normalized = normalizePublicHtmlRelativePath(relativePath); - if (!normalized || !normalized.toLowerCase().endsWith(".html")) continue; - const absolutePath = path34.resolve(publishRoot, normalized); - if (absolutePath !== publishRoot && !absolutePath.startsWith(`${publishRoot}${path34.sep}`)) continue; - let stat = null; - try { - stat = await fsPromises4.stat(absolutePath); - } catch { - continue; - } - if (!stat.isFile()) continue; - const hash = crypto36.createHash("sha256").update(`${user.id}:${sessionId}:${normalized}`).digest("hex").slice(0, 16); - const artifactId = `ca_public_html_${hash}`; - await mindSpaceConversationPackageRegistry.recordArtifact({ - id: artifactId, - packageId: packageRecord.id, - artifactKind: "public_html", - role: "assistant", - messageId: typeof ref?.messageId === "string" && ref.messageId.trim() ? ref.messageId.trim() : null, - displayName: path34.posix.basename(normalized), - mimeType: "text/html", - sizeBytes: stat.size, - canonicalUrl: buildPublicUrl(resolvePublicBaseUrl(), user.id, normalized), - sortOrder: Number.isFinite(stat.mtimeMs) ? Math.round(stat.mtimeMs) : now, - now - }); - recorded.push({ artifactId, relativePath: normalized }); - } - if (recorded.length > 0) { - await mindSpaceConversationPackageRegistry.writeManifestForSession({ - userId: user.id, - sessionId - }); - } - return recorded; - } catch (error) { - console.warn("[MindSpace] failed to record public html artifacts:", error?.message ?? error); - return []; - } -} -async function hydratePublicHtmlArtifactsForConversationPackage({ - user, - sessionId -}) { - if (!sessionSnapshotService?.isEnabled?.() || !user?.id || !sessionId) return []; - const publishDir = resolvePublishDir(__dirname5, { id: user.id }); - const snapshot = await sessionSnapshotService.get(sessionId).catch(() => null); - const messages = Array.isArray(snapshot?.messages) ? snapshot.messages : []; - const artifactRefs = collectOwnPublicHtmlArtifactRefs({ - messages, - currentUser: user, - publishDir - }); - return registerPublicHtmlArtifactsForConversation({ - user, - publishDir, - sessionId, - title: snapshot?.session?.name, - artifactRefs - }); -} -api.get("/mindspace/v1/pages/chat-save-preview", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const bundle = await resolveChatSaveBundle(req.currentUser, __dirname5, req.query); - if (!bundle.resolvedHtml) { - throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u94FE\u63A5\u9875\u9762\u5185\u5BB9"), { code: "static_page_not_found" }); - } - const baseHref = buildWorkspaceBaseHref(req.currentUser.id, bundle.resolvedHtml.relativePath); - const html = injectHtmlBaseHref(bundle.resolvedHtml.content, baseHref); - res.set("Content-Type", "text/html; charset=utf-8"); - res.set("Cache-Control", "private, no-store"); - res.setHeader("X-Request-Id", req.requestId); - return res.send(html); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/pages/chat-save-thumbnail", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const bundle = await resolveChatSaveBundle(req.currentUser, __dirname5, req.query); - if (!bundle.resolvedHtml) { - throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u94FE\u63A5\u9875\u9762\u5185\u5BB9"), { code: "static_page_not_found" }); - } - const publishDir = resolvePublishDir(__dirname5, req.currentUser); - const thumbRel = workspaceThumbnailRelativePath(bundle.resolvedHtml.relativePath); - const title = bundle.previewTitle || bundle.resolvedHtml.suggestedTitle || bundle.analysis.suggestedTitle; - const subtitle = bundle.previewSummary || bundle.resolvedHtml.suggestedSummary || bundle.analysis.suggestedSummary; - await ensureWorkspaceHtmlThumbnail( - publishDir, - bundle.resolvedHtml.relativePath, - bundle.resolvedHtml.content, - { title, subtitle, force: true } - ).catch(() => { - }); - const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"); - const resolveAssetDataUri = createAssetDataUriResolver( - authPool, - storageRoot, - req.currentUser.id - ); - const svg = await generateHtmlThumbnail(publishDir, thumbRel, bundle.resolvedHtml.content, { - title, - subtitle, - contentBaseDir: path34.dirname(bundle.resolvedHtml.absolute), - resolveAssetDataUri, - force: true - }); - res.set("Content-Type", "image/svg+xml; charset=utf-8"); - res.set("Cache-Control", "private, no-store"); - res.setHeader("X-Request-Id", req.requestId); - return res.send(svg); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/analyze-chat-save", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const sessionId = req.body?.session_id; - const messageId2 = req.body?.message_id; - const bundle = await resolveChatSaveBundle(req.currentUser, __dirname5, req.body); - const { - source, - analysis, - resolvedHtml, - previewTitle, - previewSummary, - previewFrameUrl, - thumbnailUrl, - localPreviewUrl, - localThumbnailUrl - } = bundle; - let thumbnailReady = false; - if (resolvedHtml?.content && resolvedHtml.relativePath) { - const publishDir = resolvePublishDir(__dirname5, req.currentUser); - try { - await ensureWorkspaceHtmlThumbnail(publishDir, resolvedHtml.relativePath, resolvedHtml.content, { - title: previewTitle || resolvedHtml.suggestedTitle, - subtitle: previewSummary || resolvedHtml.suggestedSummary, - force: Boolean(previewTitle || previewSummary) - }); - thumbnailReady = true; - } catch { - thumbnailReady = false; - } - } - const existingPage = await resolveExistingSavedPage(req.currentUser.id, { - sessionId, - messageId: messageId2, - relativePath: resolvedHtml?.relativePath ?? analysis.relativePath - }); - return sendData(res, req, { - contentMode: analysis.contentMode, - links: analysis.links, - selectedLinkIndex: analysis.selectedLink ? analysis.links.findIndex((link) => link.publicUrl === analysis.selectedLink.publicUrl) : -1, - suggestedTitle: resolvedHtml?.suggestedTitle ?? analysis.suggestedTitle, - suggestedSummary: resolvedHtml?.suggestedSummary ?? analysis.suggestedSummary, - previewUrl: analysis.previewUrl, - previewFrameUrl, - localPreviewUrl, - localThumbnailUrl, - thumbnailUrl: resolvedHtml ? thumbnailUrl : null, - thumbnailReady, - relativePath: resolvedHtml?.relativePath ?? analysis.relativePath, - filename: resolvedHtml?.filename ?? analysis.filename, - hasHtmlContent: Boolean(resolvedHtml), - privacyScan: scanContent(resolvedHtml?.content ?? source.content), - existingPage: existingPage ? { id: existingPage.id, title: existingPage.title, categoryCode: existingPage.categoryCode } : null - }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/quick-share-from-chat", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const bundle = await resolveChatSaveBundle(req.currentUser, __dirname5, req.body); - console.log("[quick-share] bundle resolved, hasHtml:", Boolean(bundle.resolvedHtml)); - if (!bundle.resolvedHtml) { - throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u94FE\u63A5\u9875\u9762\u5185\u5BB9"), { code: "static_page_not_found" }); - } - const storageRoot = process.env.MINDSPACE_STORAGE_ROOT ?? path34.join(__dirname5, "data", "mindspace"); - const { html: localizedHtml } = await inlinePrivateAssetsInHtml( - authPool, - storageRoot, - req.currentUser.id, - bundle.resolvedHtml.content - ); - const publishDir = resolvePublishDir(__dirname5, req.currentUser); - const sharedDir = path34.join(publishDir, PUBLIC_ZONE_DIR, "shared"); - await fsPromises4.mkdir(sharedDir, { recursive: true }); - const basename = bundle.resolvedHtml.filename.replace(/\.html$/i, ""); - const filename = `${basename}-${crypto36.randomUUID().slice(0, 8)}.html`; - const sharedRelativePath = `${PUBLIC_ZONE_DIR}/shared/${filename}`; - const sharedHtml = rewriteWorkspacePublicAssetReferences(localizedHtml, sharedRelativePath); - const destPath = path34.join(sharedDir, filename); - await fsPromises4.writeFile(destPath, sharedHtml, "utf8"); - const publishKey = req.currentUser.id; - const publicUrl2 = buildPublicUrl(resolvePublicBaseUrl(), publishKey, sharedRelativePath); - return res.status(201).json({ data: { publicUrl: publicUrl2, filename } }); - } catch (error) { - console.error("[quick-share] error:", error); - return mindSpaceError(res, req, error); - } -}); -async function handleChatSaveDocx(req, res) { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const bundle = await resolveChatSaveBundle(req.currentUser, __dirname5, req.body); - const title = bundle.previewTitle || bundle.resolvedHtml?.suggestedTitle || bundle.analysis?.suggestedTitle || "AI \u521B\u4F5C\u6587\u6863"; - const content = bundle.resolvedHtml?.content ?? bundle.source.content; - const contentFormat = bundle.resolvedHtml?.content ? "html" : "markdown"; - const buffer = generateDocxBuffer({ title, content, contentFormat }); - const filenameBase = String(title).replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "mindspace-document"; - const filename = `${filenameBase}.docx`; - await registerChatDocxArtifactForConversation({ - registry: mindSpaceConversationPackageRegistry, - user: req.currentUser, - bundle, - requestBody: req.body, - filename, - buffer - }).catch((error) => { - console.warn("[MindSpace] failed to record chat docx artifact:", error?.message ?? error); - }); - res.set("Content-Type", DOCX_MIME_TYPE); - res.set( - "Content-Disposition", - `attachment; filename="${encodeURIComponent(filename)}"; filename*=UTF-8''${encodeURIComponent(filename)}` - ); - res.set("Cache-Control", "no-store"); - res.setHeader("X-Request-Id", req.requestId); - return res.send(buffer); - } catch (error) { - return mindSpaceError(res, req, error); - } -} -api.post("/mindspace/v1/pages/chat-save-docx", handleChatSaveDocx); -api.post("/v1/pages/chat-save-docx", handleChatSaveDocx); -api.post("/mindspace/v1/pages/save-from-chat", async (req, res) => { - if (!mindSpacePages || !mindSpaceAssets) { - return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - } - try { - const categoryCode = String(req.body?.category_code ?? "draft"); - if (!SAVE_TARGET_CATEGORIES.has(categoryCode)) { - throw Object.assign(new Error("\u65E0\u6548\u7684\u4FDD\u5B58\u76EE\u6807"), { code: "invalid_category_code" }); - } - const source = await resolveOwnedAssistantMessage( - req.currentUser.id, - req.body?.session_id, - req.body?.message_id - ); - const analysis = analyzeChatMessageForSave({ - content: source.content, - userId: req.currentUser.id, - username: req.currentUser.username, - h5Root: __dirname5, - selectedLinkIndex: Number(req.body?.selected_link_index ?? 0) - }); - const snapshot = { - session_name: source.session.name, - message_created: source.message.created, - role: source.message.role, - content_mode: analysis.contentMode, - public_url: analysis.previewUrl, - relative_path: analysis.relativePath ? normalizeWorkspaceRelativePath2(analysis.relativePath) : analysis.relativePath - }; - let resolvedHtml = null; - if (analysis.contentMode === "static_html") { - resolvedHtml = await resolveStaticHtmlContent(analysis).catch(() => null); - } - const privacyScan = scanContent(resolvedHtml?.content ?? source.content); - if (categoryCode !== "draft") { - let buffer; - let filename; - let displayName = req.body?.title; - if (analysis.contentMode === "static_html") { - if (!resolvedHtml) { - throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u94FE\u63A5\u9875\u9762\u5185\u5BB9"), { code: "static_page_not_found" }); - } - buffer = Buffer.from(resolvedHtml.content, "utf8"); - filename = resolvedHtml.filename; - displayName = displayName || resolvedHtml.suggestedTitle; - } else { - buffer = Buffer.from(source.content, "utf8"); - filename = `${String(displayName || "chat-export").replace(/[^\w\u4e00-\u9fff-]+/g, "-").slice(0, 48) || "chat-export"}.md`; - } - const asset = await mindSpaceAssets.createChatAsset(req.currentUser.id, { - categoryCode, - buffer, - filename, - displayName, - sourceType: "chat" - }); - return res.status(201).json({ - data: { - kind: "asset", - categoryCode, - asset - } - }); - } - let pageInput = { - title: req.body?.title, - summary: req.body?.summary, - templateId: req.body?.template_id, - pageType: req.body?.page_type, - categoryCode: "draft" - }; - if (analysis.contentMode === "static_html") { - if (!resolvedHtml) { - throw Object.assign(new Error("\u65E0\u6CD5\u8BFB\u53D6\u94FE\u63A5\u9875\u9762\u5185\u5BB9"), { code: "static_page_not_found" }); - } - pageInput = { - ...pageInput, - title: req.body?.title || resolvedHtml.suggestedTitle, - summary: req.body?.summary || resolvedHtml.suggestedSummary, - content: resolvedHtml.content, - contentFormat: "html", - pageType: "html" - }; - } else { - pageInput = { - ...pageInput, - content: source.content, - contentFormat: "markdown" - }; - } - if (analysis.contentMode === "static_html" && resolvedHtml?.content && analysis.relativePath) { - const publishDir = resolvePublishDir(__dirname5, req.currentUser); - await ensureWorkspaceHtmlThumbnail(publishDir, analysis.relativePath, resolvedHtml.content, { - title: pageInput.title, - subtitle: pageInput.summary - }).catch(() => { - }); - } - const saveAsNew = Boolean(req.body?.save_as_new); - let replacePageId = req.body?.replace_page_id ? String(req.body.replace_page_id).trim() : null; - if (!replacePageId && !saveAsNew && analysis.contentMode === "static_html" && analysis.relativePath) { - const existingByPath = await mindSpacePages.findPageByRelativePath(req.currentUser.id, analysis.relativePath).catch(() => null); - if (existingByPath) replacePageId = existingByPath.id; - } - let page; - if (replacePageId) { - const existingPage = await mindSpacePages.getPage(req.currentUser.id, replacePageId); - page = await mindSpacePages.updatePage(req.currentUser.id, replacePageId, { - ...pageInput, - expectedVersion: existingPage.versionNo - }); - } else { - page = await mindSpacePages.createFromChat(req.currentUser.id, pageInput, { - sessionId: req.body.session_id, - messageId: req.body.message_id, - snapshot - }); - } - return res.status(201).json({ data: { kind: "page", categoryCode: "draft", page } }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const page = await mindSpacePages.createPage(req.currentUser.id, { - title: req.body?.title, - summary: req.body?.summary, - content: req.body?.content, - contentFormat: req.body?.content_format === "html" ? "html" : void 0, - templateId: req.body?.template_id, - pageType: req.body?.page_type, - categoryCode: req.body?.category_code - }); - return res.status(201).json({ data: page }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/pages", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - await syncUserGeneratedPages(req.currentUser.id); - const limit = Number.parseInt(String(req.query.limit ?? ""), 10); - const offset = Number.parseInt(String(req.query.offset ?? ""), 10); - const result = await mindSpacePages.listPages(req.currentUser.id, { - status: typeof req.query.status === "string" ? req.query.status : void 0, - categoryCode: typeof req.query.category_code === "string" ? req.query.category_code : void 0, - ...Number.isFinite(limit) ? { limit } : {}, - ...Number.isFinite(offset) ? { offset } : {} - }); - return res.json({ - data: result.items, - page: { - total: result.total, - limit: result.limit, - offset: result.offset, - has_more: result.hasMore, - next_cursor: null - } - }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/pages/:pageId", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const [page, versions, publication] = await Promise.all([ - mindSpacePages.getPage(req.currentUser.id, req.params.pageId), - mindSpacePages.listVersions(req.currentUser.id, req.params.pageId), - mindSpacePublications?.getCurrent(req.currentUser.id, req.params.pageId) ?? null - ]); - return res.json({ data: { ...page, versions, publication } }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.delete("/mindspace/v1/pages/:pageId", async (req, res) => { - if (!mindSpacePages || !ensureMindSpaceEnabled(res, req)) return; - try { - const removeFromPlaza = String(req.query?.remove_from_plaza ?? req.body?.remove_from_plaza ?? "").toLowerCase() === "true" || req.query?.remove_from_plaza === "1" || req.body?.remove_from_plaza === true; - const result = await mindSpacePages.deletePage(req.currentUser.id, req.params.pageId, { - removeFromPlaza - }); - const quota = await mindSpace.getQuota(req.currentUser.id); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "page.delete", - objectType: "page", - objectId: req.params.pageId, - ip: req.ip, - detail: { - offlinedPublicationCount: result.offlinedPublicationCount, - hiddenPlazaPostCount: result.hiddenPlazaPostCount, - deletedAssetCount: result.deletedAssetCount, - freedBytes: result.freedBytes - } - }); - return sendData(res, req, { ...result, quota }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/pages/:pageId/delete-preview", async (req, res) => { - if (!mindSpacePages || !ensureMindSpaceEnabled(res, req)) return; - try { - return sendData(res, req, await mindSpacePages.getDeletePreview(req.currentUser.id, req.params.pageId)); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.put("/mindspace/v1/pages/:pageId", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const page = await mindSpacePages.updatePage(req.currentUser.id, req.params.pageId, { - expectedVersion: req.body?.expected_version, - title: req.body?.title, - summary: req.body?.summary, - content: req.body?.content, - templateId: req.body?.template_id, - pageType: req.body?.page_type, - changeNote: req.body?.change_note - }); - return res.json({ data: page }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/live-edit/bind", async (req, res) => { - if (!mindSpacePageLiveEdit || !mindSpacePages) { - return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - } - try { - const sessionId = String(req.body?.session_id ?? "").trim(); - if (!sessionId) { - return sendError(res, req, 400, "invalid_request", "\u7F3A\u5C11 session_id"); - } - const owns = await userAuth.ownsSession(req.currentUser.id, sessionId); - if (!owns) { - return sendError(res, req, 403, "forbidden", "\u65E0\u6743\u7ED1\u5B9A\u8BE5 Agent \u4F1A\u8BDD"); - } - await mindSpacePages.getPage(req.currentUser.id, req.params.pageId); - const parentSessionId = String(req.body?.parent_session_id ?? "").trim() || null; - return sendData( - res, - req, - mindSpacePageLiveEdit.bindSession({ - userId: req.currentUser.id, - sessionId, - pageId: req.params.pageId, - parentSessionId - }) - ); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/live-edit/fork-session", async (req, res) => { - if (!mindSpacePageEditSession || !mindSpacePages) { - return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - } - try { - const parentSessionId = String(req.body?.parent_session_id ?? "").trim(); - if (!parentSessionId) { - return sendError(res, req, 400, "invalid_request", "\u7F3A\u5C11 parent_session_id"); - } - const gate = await userAuth.canUseChat(req.currentUser.id); - if (!gate.ok) { - return sendError(res, req, 402, gate.code ?? "insufficient_balance", gate.message); - } - return sendData( - res, - req, - await mindSpacePageEditSession.forkSession({ - userId: req.currentUser.id, - pageId: req.params.pageId, - parentSessionId, - h5ApiBase: String(req.body?.h5_api_base ?? req.body?.h5ApiBase ?? "").trim() || null - }) - ); - } catch (error) { - if (error?.code === "forbidden") { - return sendError(res, req, 403, error.code, error.message); - } - if (error?.code === "invalid_request") { - return sendError(res, req, 400, error.code, error.message); - } - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/live-edit/close-session", async (req, res) => { - if (!mindSpacePageEditSession || !mindSpacePages) { - return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - } - try { - const sessionId = String(req.body?.session_id ?? "").trim(); - if (!sessionId) { - return sendError(res, req, 400, "invalid_request", "\u7F3A\u5C11 session_id"); - } - return sendData( - res, - req, - await mindSpacePageEditSession.closeSession({ - userId: req.currentUser.id, - pageId: req.params.pageId, - sessionId, - parentSessionId: String(req.body?.parent_session_id ?? "").trim() || null, - summary: String(req.body?.summary ?? "") - }) - ); - } catch (error) { - if (error?.code === "forbidden" || error?.code === "page_binding_mismatch") { - return sendError(res, req, 403, error.code, error.message); - } - if (error?.code === "invalid_request") { - return sendError(res, req, 400, error.code, error.message); - } - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/pages/:pageId/live-edit/revision", async (req, res) => { - if (!mindSpacePageLiveEdit || !mindSpacePages) { - return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - } - try { - return sendData( - res, - req, - await mindSpacePageLiveEdit.getRevisionSnapshot(req.currentUser.id, req.params.pageId) - ); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/agent/mindspace_page_patch", async (req, res) => { - if (!mindSpacePageLiveEdit) { - return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - } - try { - const result = await mindSpacePageLiveEdit.applyAgentPatch(req.body ?? {}); - return res.json({ - data: { - pageId: result.page.id, - title: result.page.title, - summary: result.page.summary, - versionNo: result.page.versionNo, - updatedAt: result.page.updatedAt, - liveRevision: result.liveRevision - } - }); - } catch (error) { - if (error?.code === "forbidden" || error?.code === "page_binding_mismatch") { - return sendError(res, req, 403, error.code, error.message); - } - if (error?.code === "invalid_request") { - return sendError(res, req, 400, error.code, error.message); - } - return mindSpaceError(res, req, error); - } -}); -api.post("/agent/mindspace_asset_delete", async (req, res) => { - if (!mindSpaceAssetAgent) { - return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - } - try { - const result = await mindSpaceAssetAgent.applyAgentDelete(req.body ?? {}); - await mindSpaceAudit?.write({ - userId: result.userId ?? null, - action: "asset.delete", - objectType: "asset", - objectId: result.assetId, - ip: req.ip, - metadata: { via: "agent" } - }); - return sendData(res, req, result); - } catch (error) { - if (error?.code === "forbidden") { - return sendError(res, req, 403, error.code, error.message); - } - if (error?.code === "invalid_request" || error?.code === "confirmation_required") { - return sendError(res, req, 400, error.code, error.message); - } - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/pages/:pageId/thumbnail", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const svg = await mindSpacePages.renderThumbnail(req.currentUser.id, req.params.pageId); - res.set("Content-Type", "image/svg+xml; charset=utf-8"); - res.set("Cache-Control", "private, max-age=300"); - res.setHeader("X-Request-Id", req.requestId); - return res.send(svg); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/thumbnail/upload", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const result = await mindSpacePages.uploadThumbnail(req.currentUser.id, req.params.pageId, { - imageBase64: req.body?.image_base64, - mimeType: req.body?.mime_type, - title: req.body?.title, - summary: req.body?.summary, - html: req.body?.html - }); - return sendData(res, req, result); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/thumbnail/regenerate", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const result = await mindSpacePages.regenerateThumbnail( - req.currentUser.id, - req.params.pageId, - { - html: req.body?.html, - title: req.body?.title, - summary: req.body?.summary, - useAi: Boolean(req.body?.use_ai), - instruction: req.body?.instruction - }, - { - suggestCoverMeta: req.body?.use_ai ? (input) => { - if (!authPool) { - throw Object.assign(new Error("LLM \u670D\u52A1\u672A\u5C31\u7EEA"), { code: "llm_not_configured" }); - } - return suggestCoverMetaWithAi(authPool, { - ...input, - encryptionKey: process.env.H5_SETTINGS_ENCRYPTION_KEY ?? process.env.TKMIND_SERVER__SECRET_KEY - }); - } : null - } - ); - return sendData(res, req, result); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/rewrite-download-links", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const content = String(req.body?.content ?? "").trim(); - if (!content) { - throw Object.assign(new Error("\u7F3A\u5C11\u9875\u9762\u5185\u5BB9"), { code: "invalid_page_input" }); - } - const html = await mindSpacePages.rewriteHtmlDownloadLinksForPage( - req.currentUser.id, - req.params.pageId, - content - ); - return sendData(res, req, { html }); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/pages/:pageId/preview", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const { html, contentFormat } = await mindSpacePages.renderPreview( - req.currentUser.id, - req.params.pageId - ); - res.set("Content-Type", "text/html; charset=utf-8"); - res.set( - "Content-Security-Policy", - pageInternals.previewContentSecurityPolicy(contentFormat) - ); - res.set("Cache-Control", "private, no-store"); - res.set("X-Content-Type-Options", "nosniff"); - return res.send(html); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/preview-draft", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const { html, contentFormat } = await mindSpacePages.renderDraftPreview( - req.currentUser.id, - req.params.pageId, - { - title: req.body?.title, - summary: req.body?.summary, - content: req.body?.content, - templateId: req.body?.template_id - } - ); - res.set("Content-Type", "text/html; charset=utf-8"); - res.set( - "Content-Security-Policy", - pageInternals.previewContentSecurityPolicy(contentFormat) - ); - res.set("Cache-Control", "private, no-store"); - res.set("X-Content-Type-Options", "nosniff"); - return res.send(html); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/publish-check", async (req, res) => { - if (!mindSpacePublications) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const result = await mindSpacePublications.check(req.currentUser.id, req.params.pageId, { - pageVersionId: req.body?.page_version_id, - accessMode: req.body?.access_mode, - urlSlug: req.body?.url_slug, - password: req.body?.password, - expiresAt: req.body?.expires_at - }); - return sendData(res, req, result); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/redact", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const result = await mindSpacePages.redactPage(req.currentUser.id, req.params.pageId, { - pageVersionId: req.body?.page_version_id, - expectedVersion: req.body?.expected_version, - title: req.body?.title, - summary: req.body?.summary, - content: req.body?.content - }); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "page.redact", - objectType: "page", - objectId: result.page.id, - ip: req.ip, - riskLevel: result.originalScan.riskLevel - }); - return sendData(res, req, result); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/publish-fix", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const result = await mindSpacePages.localizePrivateResources(req.currentUser.id, req.params.pageId, { - pageVersionId: req.body?.page_version_id, - expectedVersion: req.body?.expected_version, - title: req.body?.title, - summary: req.body?.summary, - content: req.body?.content - }); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "page.publish_fix", - objectType: "page", - objectId: result.page.id, - ip: req.ip, - riskLevel: result.originalScan.riskLevel - }); - return sendData(res, req, result); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/redacted-copy", async (req, res) => { - if (!mindSpacePages) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const result = await mindSpacePages.redactPage(req.currentUser.id, req.params.pageId, { - pageVersionId: req.body?.page_version_id, - expectedVersion: req.body?.expected_version, - title: req.body?.title, - summary: req.body?.summary, - content: req.body?.content - }); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "page.redact", - objectType: "page", - objectId: result.page.id, - ip: req.ip, - riskLevel: result.originalScan.riskLevel - }); - return sendData(res, req, result); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/pages/:pageId/publish", async (req, res) => { - if (!mindSpacePublications) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const publication = await mindSpacePublications.publish( - req.currentUser.id, - req.params.pageId, - { - pageVersionId: req.body?.page_version_id, - accessMode: req.body?.access_mode, - urlSlug: req.body?.url_slug, - password: req.body?.password, - expiresAt: req.body?.expires_at, - acknowledgedFindingIds: req.body?.acknowledged_finding_ids - } - ); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "page.publish", - objectType: "publication", - objectId: publication.id, - ip: req.ip - }); - return sendData(res, req, publication, 201); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/publications/:publicationId/update-status", async (req, res) => { - if (!mindSpacePublications) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const publication = await mindSpacePublications.updatePublicationStatus( - req.currentUser.id, - req.params.publicationId, - { - accessMode: req.body?.access_mode, - expiresAt: req.body?.expires_at - } - ); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "publication.status_updated", - objectType: "publication", - objectId: req.params.publicationId, - ip: req.ip - }); - return sendData(res, req, publication); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.post("/mindspace/v1/publications/:publicationId/offline", async (req, res) => { - if (!mindSpacePublications) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - const publication = await mindSpacePublications.offline( - req.currentUser.id, - req.params.publicationId - ); - await mindSpaceAudit?.write({ - userId: req.currentUser.id, - action: "page.offline", - objectType: "publication", - objectId: req.params.publicationId, - ip: req.ip - }); - return sendData(res, req, publication); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/mindspace/v1/publications/:publicationId/stats", async (req, res) => { - if (!mindSpacePublications) return res.status(503).json({ message: "MindSpace \u672A\u542F\u7528" }); - try { - return sendData( - res, - req, - await mindSpacePublications.getStats( - req.currentUser.id, - req.params.publicationId - ) - ); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); -api.get("/plaza/v1/categories", async (req, res) => { - if (!ensurePlazaEnabled(res, req)) return; - try { - return sendData(res, req, { categories: await plazaPosts.listCategories() }); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.get("/plaza/v1/seo/sitemap", async (req, res) => { - if (!ensurePlazaEnabled(res, req)) return; - if (!plazaSeo) return sendError(res, req, 503, "plaza_unavailable", "Plaza SEO \u672A\u542F\u7528"); - try { - const data = await plazaSeo.listSitemapData({ - postLimit: req.query.post_limit, - userLimit: req.query.user_limit - }); - return sendData(res, req, data); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/attribution/events", async (req, res) => { - if (!plazaSeo) return sendError(res, req, 503, "plaza_unavailable", "Plaza \u672A\u542F\u7528"); - try { - const result = await plazaSeo.recordAttribution(req.body ?? {}, plazaClientIp(req)); - return sendData(res, req, result, 201); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/posts/:id/reports", async (req, res) => { - if (!plazaOps) return sendError(res, req, 503, "plaza_unavailable", "Plaza \u672A\u542F\u7528"); - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const report = await plazaOps.createReport(req.currentUser.id, { - target_type: "post", - target_id: req.params.id, - reason: req.body?.reason, - detail: req.body?.detail - }); - return sendData(res, req, { report }, 201); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/comments/:id/reports", async (req, res) => { - if (!plazaOps) return sendError(res, req, 503, "plaza_unavailable", "Plaza \u672A\u542F\u7528"); - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const report = await plazaOps.createReport(req.currentUser.id, { - target_type: "comment", - target_id: req.params.id, - reason: req.body?.reason, - detail: req.body?.detail - }); - return sendData(res, req, { report }, 201); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.get("/plaza/v1/feed", async (req, res) => { - if (!ensurePlazaEnabled(res, req)) return; - try { - const sessionId = resolvePlazaSessionId(req, res); - const feed = await plazaPosts.listFeed({ - sort: req.query.sort, - categorySlug: req.query.category ?? null, - cursor: req.query.cursor ?? null, - limit: req.query.limit, - viewerId: req.currentUser?.id ?? null, - sessionId - }); - return sendData(res, req, feed); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/events", async (req, res) => { - if (!ensurePlazaEnabled(res, req)) return; - if (!plazaEvents) return sendError(res, req, 503, "plaza_unavailable", "Plaza \u672A\u542F\u7528"); - try { - const sessionId = String(req.body?.session_id ?? "").trim() || resolvePlazaSessionId(req, res); - const result = await plazaEvents.recordEvents({ - userId: req.currentUser?.id ?? null, - sessionId, - events: req.body?.events ?? [] - }); - return sendData(res, req, { ...result, session_id: sessionId }, 201); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/posts/:id/reactions", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const result = await plazaInteractions.addReaction( - req.currentUser.id, - req.params.id, - req.body?.type - ); - const eventType = reactionEventType(result.type); - if (eventType) { - recordPlazaEventsAsync(req, res, [{ event_type: eventType, post_id: req.params.id }]); - } - return sendData(res, req, result); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.delete("/plaza/v1/posts/:id/reactions/:type", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const result = await plazaInteractions.removeReaction( - req.currentUser.id, - req.params.id, - req.params.type - ); - return sendData(res, req, result); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.get("/plaza/v1/posts/:id/comments", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - try { - const comments = await plazaInteractions.listComments(req.params.id, { - cursor: req.query.cursor ?? null, - limit: req.query.limit, - parentId: req.query.parent_id ?? null, - viewerId: req.currentUser?.id ?? null - }); - return sendData(res, req, comments); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/posts/:id/comments", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const comment = await plazaInteractions.createComment(req.currentUser.id, req.params.id, req.body ?? {}); - recordPlazaEventsAsync(req, res, [{ event_type: "comment", post_id: req.params.id }]); - return sendData(res, req, { comment }, 201); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.delete("/plaza/v1/comments/:id", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const comment = await plazaInteractions.deleteComment(req.currentUser.id, req.params.id); - return sendData(res, req, { comment }); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/comments/:id/reactions", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const liked = req.body?.liked !== false; - const result = await plazaInteractions.toggleCommentLike(req.currentUser.id, req.params.id, liked); - return sendData(res, req, result); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.get("/plaza/v1/users/:slug", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - try { - const profile = await plazaInteractions.getUserProfile( - req.params.slug, - req.currentUser?.id ?? null - ); - return sendData(res, req, profile); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.get("/plaza/v1/users/:slug/posts", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - try { - const feed = await plazaInteractions.listUserPosts(req.params.slug, { - cursor: req.query.cursor ?? null, - limit: req.query.limit, - viewerId: req.currentUser?.id ?? null - }); - return sendData(res, req, feed); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/users/:slug/follow", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const result = await plazaInteractions.followUser(req.currentUser.id, req.params.slug); - return sendData(res, req, result); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.delete("/plaza/v1/users/:slug/follow", async (req, res) => { - if (!ensurePlazaInteractions(res, req)) return; - if (!req.currentUser) return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - try { - const result = await plazaInteractions.unfollowUser(req.currentUser.id, req.params.slug); - return sendData(res, req, result); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.get("/plaza/v1/posts/:id", async (req, res) => { - if (!ensurePlazaEnabled(res, req)) return; - try { - const post = await plazaPosts.getPostById(req.params.id, { - viewerId: req.currentUser?.id ?? null - }); - void plazaRedis.recordView(req.params.id, plazaClientIp(req)).catch(() => { - }); - recordPlazaEventsAsync(req, res, [{ event_type: "view", post_id: req.params.id }]); - return sendData(res, req, { post }); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.post("/plaza/v1/posts", async (req, res) => { - if (!ensurePlazaEnabled(res, req)) return; - if (!req.currentUser) { - return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - } - try { - const post = await plazaPosts.createPost(req.currentUser.id, req.body ?? {}); - return sendData(res, req, { post }, 201); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.patch("/plaza/v1/posts/:id", async (req, res) => { - if (!ensurePlazaEnabled(res, req)) return; - if (!req.currentUser) { - return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - } - try { - const post = await plazaPosts.updatePost(req.currentUser.id, req.params.id, req.body ?? {}); - return sendData(res, req, { post }); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -api.delete("/plaza/v1/posts/:id", async (req, res) => { - if (!ensurePlazaEnabled(res, req)) return; - if (!req.currentUser) { - return sendError(res, req, 401, "unauthorized", "\u672A\u6388\u6743\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55"); - } - try { - const post = await plazaPosts.hidePost(req.currentUser.id, req.params.id); - return sendData(res, req, { post }); - } catch (error) { - return plazaRouteError(res, req, error); - } -}); -function runHandlerChain(chain, req, res, next) { - let index = 0; - const run = (err) => { - if (err) return next(err); - const layer = chain[index++]; - if (!layer) return; - layer(req, res, (error) => run(error)); - }; - run(); -} -api.post("/llm/apply-local-fallback", async (req, res) => { - await userAuthReady; - if (!userAuth || !llmProviderService || !tkmindProxy) { - return res.status(503).json({ message: "\u672A\u542F\u7528 LLM \u914D\u7F6E" }); - } - const me = await userAuth.getMe(userToken(req)); - if (!me) return res.status(401).json({ message: "\u672A\u767B\u5F55" }); - const sessionId = String(req.body?.session_id ?? "").trim(); - if (!sessionId) return res.status(400).json({ message: "\u7F3A\u5C11 session_id" }); - const owns = await userAuth.ownsSession(me.id, sessionId); - if (!owns) return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - try { - const result = await tkmindProxy.applyLocalFallbackForSession(sessionId); - if (!result.ok) return res.status(503).json(result); - res.json(result); - } catch (err) { - res.status(500).json({ - message: err instanceof Error ? err.message : "\u5207\u6362\u672C\u5730 LLM \u5931\u8D25" - }); - } -}); -api.post("/agent/start", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy) return next(); - return runHandlerChain(tkmindProxy.handlers["POST /agent/start"], req, res, next); -}); -api.post("/agent/runs", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy || !agentRunGateway) return next(); - return runHandlerChain( - [ - tkmindProxy.requireUser, - tkmindProxy.ensureChatAllowed, - createPostAgentRunsHandler({ userAuth, agentRunGateway }) - ], - req, - res, - next - ); -}); -api.get("/agent/runs/:runId", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy || !agentRunGateway) return next(); - return runHandlerChain( - [ - tkmindProxy.requireUser, - createGetAgentRunHandler({ agentRunGateway }) - ], - req, - res, - next - ); -}); -api.get("/agent/runs/:runId/events", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy || !agentRunGateway) return next(); - return runHandlerChain( - [ - tkmindProxy.requireUser, - createAgentRunEventsHandler({ agentRunGateway }) - ], - req, - res, - next - ); -}); -api.post("/agent/resume", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy) return next(); - return runHandlerChain(tkmindProxy.handlers["POST /agent/resume"], req, res, next); -}); -api.get("/sessions", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy) return next(); - return runHandlerChain(tkmindProxy.handlers["GET /sessions"], req, res, next); -}); -api.get("/sessions/:sessionId", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy) return next(); - const sessionId = req.params.sessionId; - const owns = await userAuth.ownsSession(req.currentUser.id, sessionId); - if (!owns) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - const hintMc = req.query.hint_mc ? Number(req.query.hint_mc) : null; - const hintUa = req.query.hint_ua ? String(req.query.hint_ua) : null; - try { - if (sessionSnapshotService?.isEnabled()) { - const snapshot = await sessionSnapshotService.get(sessionId); - if (snapshot) { - const canUseSnapshotCache = hintMc != null && hintUa != null; - const mcMatch = snapshot.meta.synced_msg_count === hintMc; - const uaMatch = snapshot.meta.source_updated_at === hintUa; - if (canUseSnapshotCache && mcMatch && uaMatch) { - const sanitizedMessages = sanitizeSessionConversationPublicHtmlLinks( - snapshot.messages, - req.currentUser - ); - const cachedGooseSession = { - ...snapshot.session, - // Embed only userVisible messages so getSession callers still work. - conversation: sanitizedMessages - }; - return res.json(cachedGooseSession); - } - } - } - } catch { - } - try { - const target = await tkmindProxy.resolveTarget(sessionId); - const upstream = await tkmindProxy.apiFetchTo( - target, - `/sessions/${encodeURIComponent(sessionId)}`, - { method: "GET" } - ); - if (!upstream.ok) { - const text = await upstream.text().catch(() => ""); - return res.status(upstream.status).send(text); - } - const gooseSession = await upstream.json(); - if (Array.isArray(gooseSession.conversation)) { - gooseSession.conversation = sanitizeSessionConversationPublicHtmlLinks( - gooseSession.conversation, - req.currentUser - ); - } - if (sessionSnapshotService?.isEnabled()) { - const messages = (gooseSession.conversation ?? []).filter((m) => m.metadata?.userVisible); - if (messages.length > 0) { - void sessionSnapshotService.save(sessionId, req.currentUser.id, gooseSession, messages).catch(() => { - }); - } - } - return res.json(gooseSession); - } catch (err) { - return res.status(502).json({ message: err instanceof Error ? err.message : "\u8BFB\u53D6\u4F1A\u8BDD\u5931\u8D25" }); - } -}); -api.delete("/sessions/:sessionId", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy) return next(); - const sessionId = req.params.sessionId; - const owns = await userAuth.ownsSession(req.currentUser.id, sessionId); - if (!owns) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - try { - const deleteTarget = await tkmindProxy.resolveTarget(sessionId); - const upstream = await tkmindProxy.apiFetchTo(deleteTarget, `/sessions/${encodeURIComponent(sessionId)}`, { - method: "DELETE" - }); - if (!upstream.ok && upstream.status !== 404) { - const text = await upstream.text().catch(() => ""); - return res.status(upstream.status).send(text || "\u5220\u9664\u4F1A\u8BDD\u5931\u8D25"); - } - await userAuth.unregisterAgentSession(req.currentUser.id, sessionId); - void sessionSnapshotService?.remove(sessionId).catch(() => { - }); - return res.status(204).end(); - } catch (err) { - return res.status(500).json({ message: err instanceof Error ? err.message : "\u5220\u9664\u4F1A\u8BDD\u5931\u8D25" }); - } -}); -api.get("/sessions/:sessionId/events", async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy) return next(); - const sessionId = req.params.sessionId; - const owns = await userAuth.ownsSession(req.currentUser.id, sessionId); - if (!owns) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - const publishDir = resolvePublishDir(__dirname5, { id: req.currentUser.id }); - const syncPublicHtmlDuringStream = (event) => { - materializePublicHtmlWritesFromSessionEvent(event, { publishDir }); - }; - const onAfterFinish = async (sid, uid) => { - const apiFetchFn = async (pathname, init) => { - const target = await tkmindProxy.resolveTarget(sid); - return tkmindProxy.apiFetchTo(target, pathname, init); - }; - let messages = null; - if (sessionSnapshotService?.isEnabled()) { - await sessionSnapshotService.refresh(sid, uid, apiFetchFn); - messages = (await sessionSnapshotService.get(sid))?.messages ?? null; - } else { - try { - const upstream = await apiFetchFn(`/sessions/${encodeURIComponent(sid)}`, { method: "GET" }); - if (upstream.ok) { - const payload = await upstream.json().catch(() => null); - messages = Array.isArray(payload?.conversation) ? payload.conversation.filter((message) => message?.metadata?.userVisible) : null; - } - } catch { - messages = null; - } - } - const syncResult = await syncPublicHtmlAfterFinish({ - messages, - currentUser: req.currentUser, - publishDir, - sessionId: sid, - syncWorkspaceAssets: WORKSPACE_MAINTENANCE_ENABLED && mindSpaceAssets ? (userId, options) => mindSpaceAssets.syncWorkspaceAssets(userId, options) : null, - registerPublicHtmlArtifacts: (_userId, options) => registerPublicHtmlArtifactsForConversation({ - user: req.currentUser, - publishDir, - sessionId: options?.sessionId, - relativePaths: options?.relativePaths, - artifactRefs: options?.artifactRefs - }) - }); - if (Array.isArray(syncResult?.docxSync?.missing) && syncResult.docxSync.missing.length > 0) { - console.warn( - `[MindSpace] missing public download files after finish for user ${uid}: ${syncResult.docxSync.missing.join(", ")}` - ); - } - await syncUserGeneratedPages(uid); - }; - return tkmindProxy.proxySessionEvents(req, res, sessionId, { - onAfterFinish, - onEvent: syncPublicHtmlDuringStream - }); -}); -api.use(async (req, res, next) => { - await userAuthReady; - if (!userAuth || !tkmindProxy) return next(); - if (isNativeH5ApiPath(req.path)) { - return sendError(res, req, 404, "not_found", `\u63A5\u53E3\u4E0D\u5B58\u5728\uFF1A${req.method} ${req.path}`); - } - if (req.method === "GET" && /^\/agent\/runs\/[^/]+\/events$/.test(req.path)) { - return res.status(503).json({ - message: "Agent Run SSE \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" - }); - } - const sessionMatch = req.path.match(/^\/sessions\/([^/]+)/); - if (sessionMatch) { - const sessionId = sessionMatch[1]; - if (req.path.endsWith("/events") && req.method === "GET") { - return next(); - } - if (req.path.endsWith("/reply") && req.method === "POST") { - return res.status(410).json({ - message: "\u804A\u5929\u63D0\u4EA4\u5165\u53E3\u5DF2\u7EDF\u4E00\u4E3A POST /agent/runs", - code: "AGENT_RUNS_REQUIRED" - }); - } - const owns = await userAuth.ownsSession(req.currentUser.id, sessionId); - if (!owns) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - } - if (req.method === "POST" && req.path === "/agent/resume" && req.body?.session_id) { - const owns = await userAuth.ownsSession(req.currentUser.id, req.body.session_id); - if (!owns) { - return res.status(403).json({ message: "\u65E0\u6743\u8BBF\u95EE\u8BE5\u4F1A\u8BDD" }); - } - } - if (req.body?.working_dir) { - const allowed = await userAuth.isPathAllowed(req.currentUser.id, req.body.working_dir); - if (!allowed) { - return res.status(403).json({ message: "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5728\u6388\u6743\u8303\u56F4\u5185" }); - } - } - return tkmindProxy.proxyFallback(req, res); -}); -api.use( - createProxyMiddleware({ - target: API_TARGET, - changeOrigin: true, - secure: false, - pathRewrite: { "^/api": "" }, - on: { - proxyReq: (proxyReq) => { - proxyReq.setHeader("X-Secret-Key", API_SECRET); - } - } - }) -); -app.use("/api", api); -app.use(`/${PUBLISH_ROOT_DIR}`, async (req, res, next) => { - await userAuthReady; - return serveUserPublishFile(req, res, next); -}); -app.use("/mindspace", api); -function scriptSrcDirective({ inline = false, urls = [], hashes = [] } = {}) { - const parts = []; - if (inline) parts.push("'unsafe-inline'"); - for (const hash of hashes) parts.push(`'sha256-${hash}'`); - for (const url of urls) parts.push(url); - return parts.length ? `script-src ${parts.join(" ")}` : "script-src 'none'"; -} -function publishedPageCsp(html, { embed = false, raw = false, wechatShare = false, scriptHashes = [] } = {}) { - const isFullHtml = /^\s*]/i.test(html); - if (embed && isFullHtml) { - return publishedPageCspForEmbed(true); - } - if (wechatShare && isFullHtml) { - return "default-src 'none'; style-src 'unsafe-inline' https:; img-src 'self' data: http: https:; font-src 'self' https: data:; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; script-src 'unsafe-inline' https://res.wx.qq.com"; - } - if (raw && isFullHtml) { - return "default-src 'none'; style-src 'unsafe-inline' https:; img-src 'self' data: http: https:; font-src 'self' https: data:; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; script-src 'unsafe-inline'"; - } - if (isFullHtml) { - return `default-src 'none'; style-src 'unsafe-inline' https:; img-src 'self' data: http: https:; font-src 'self' https: data:; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; ${scriptSrcDirective({ hashes: scriptHashes })}`; - } - return "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'"; -} -var PUBLIC_FILE_SHARE_SCRIPT = `(function(){var root=document.querySelector('[data-mindspace-public-share]');if(!root)return;var shareButton=root.querySelector('[data-action="share"]');var captureButton=root.querySelector('[data-action="capture"]');var status=root.querySelector('small');var timer=null;function setStatus(text,err){if(!status)return;status.textContent=text||'';status.classList.toggle('is-error',!!err);if(timer)clearTimeout(timer);if(text)timer=setTimeout(function(){status.textContent='';status.classList.remove('is-error');},2200);}function cleanUrl(){var url=new URL(location.href);url.hash='';url.searchParams.delete('download');url.searchParams.delete('export');return url.toString();}function withParam(url,key,value){var next=new URL(url);next.searchParams.set(key,value);return next.toString();}function fallbackCopy(text){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();var ok=document.execCommand('copy');ta.remove();if(!ok)throw new Error('\u590D\u5236\u5931\u8D25');}async function copyText(text){if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(text);return;}catch(e){}}fallbackCopy(text);}shareButton&&shareButton.addEventListener('click',async function(){var url=cleanUrl();var title=document.title||'MindSpace';try{if(navigator.share){await navigator.share({title:title,url:url});setStatus('\u5DF2\u6253\u5F00\u5206\u4EAB');return;}await copyText(url);setStatus('\u94FE\u63A5\u5DF2\u590D\u5236');}catch(e){setStatus(e&&e.message?e.message:'\u5206\u4EAB\u5931\u8D25',true);}});captureButton&&captureButton.addEventListener('click',function(){setStatus('\u6B63\u5728\u751F\u6210\u957F\u56FE\uFF0C\u8BF7\u7A0D\u5019');var link=document.createElement('a');link.href=withParam(cleanUrl(),'download','long-image');link.download='';document.body.appendChild(link);link.click();link.remove();});})();`; -var PUBLIC_FILE_SHARE_SCRIPT_HASH = crypto36.createHash("sha256").update(PUBLIC_FILE_SHARE_SCRIPT).digest("base64"); -function injectPublicFileShareButton(html) { - const source = String(html ?? ""); - if (!source || source.includes("data-mindspace-public-share")) { - return { html: source, scriptHashes: [] }; - } - const markup = ` - -
-`; - 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} - - - -
- - -
- - - - -`; -} -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 - - - -
-

\u6B64\u9875\u9762\u53D7\u5BC6\u7801\u4FDD\u62A4

-

\u8BF7\u8F93\u5165\u53D1\u5E03\u8005\u63D0\u4F9B\u7684\u8BBF\u95EE\u5BC6\u7801\u3002\u5BC6\u7801\u4E0D\u4F1A\u5199\u5165\u94FE\u63A5\u6216\u6D4F\u89C8\u5668\u65E5\u5FD7\u3002

- - -
- -`; -} -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")}

-
- ${new Date(page.publishedAt).toLocaleDateString("zh-CN")} - \u6253\u5F00\u9875\u9762 -
-
` - ).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 ? `
${cards}
` : '
\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