var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // server/_core/notification.ts var notification_exports = {}; __export(notification_exports, { getWebhookStatus: () => getWebhookStatus, notifyApprovalAlert: () => notifyApprovalAlert, notifyBackupFailure: () => notifyBackupFailure, notifyCrawlerError: () => notifyCrawlerError, notifyOwner: () => notifyOwner }); import { TRPCError as TRPCError2 } from "@trpc/server"; function buildWeComPayload(payload) { const { title, content, level = "info", module } = payload; const color = LEVEL_COLORS[level] || "green"; const moduleTag = module ? `\u3010${module}\u3011` : ""; return { msgtype: "markdown", markdown: { content: `# ${LEVEL_EMOJI[level]} ${moduleTag}${title} ${content} > \u65F6\u95F4\uFF1A${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} > \u7CFB\u7EDF\uFF1ANAC Knowledge Engine Admin > \u7EA7\u522B\uFF1A${level.toUpperCase()}` } }; } function buildDingTalkPayload(payload) { const { title, content, level = "info", module } = payload; const moduleTag = module ? `\u3010${module}\u3011` : ""; return { msgtype: "markdown", markdown: { title: `${LEVEL_EMOJI[level]} ${moduleTag}${title}`, text: `## ${LEVEL_EMOJI[level]} ${moduleTag}${title} ${content} --- **\u65F6\u95F4\uFF1A** ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} **\u7CFB\u7EDF\uFF1A** NAC Knowledge Engine Admin **\u7EA7\u522B\uFF1A** ${level.toUpperCase()}` }, at: { isAtAll: level === "critical" } }; } function buildFeishuPayload(payload) { const { title, content, level = "info", module } = payload; const moduleTag = module ? `[${module}] ` : ""; const colorMap = { info: "green", warning: "yellow", error: "red", critical: "red" }; return { msg_type: "interactive", card: { config: { wide_screen_mode: true }, header: { title: { tag: "plain_text", content: `${LEVEL_EMOJI[level]} ${moduleTag}${title}` }, template: colorMap[level] || "green" }, elements: [ { tag: "div", text: { tag: "lark_md", content } }, { tag: "hr" }, { tag: "div", fields: [ { is_short: true, text: { tag: "lark_md", content: `**\u65F6\u95F4** ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}` } }, { is_short: true, text: { tag: "lark_md", content: `**\u7EA7\u522B** ${level.toUpperCase()}` } } ] } ] } }; } function buildGenericPayload(payload) { return { title: payload.title, content: payload.content, level: payload.level || "info", module: payload.module, timestamp: (/* @__PURE__ */ new Date()).toISOString(), system: "NAC Knowledge Engine Admin" }; } async function sendWebhook(url, type, payload) { let body; switch (type) { case "wecom": body = buildWeComPayload(payload); break; case "dingtalk": body = buildDingTalkPayload(payload); break; case "feishu": body = buildFeishuPayload(payload); break; default: body = buildGenericPayload(payload); } try { const response = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); if (response.ok) { console.log(`[Notification] ${type}\u901A\u77E5\u5DF2\u53D1\u9001: ${payload.title}`); return true; } const errText = await response.text().catch(() => ""); console.warn(`[Notification] ${type}\u901A\u77E5\u53D1\u9001\u5931\u8D25 (${response.status}): ${errText.slice(0, 200)}`); return false; } catch (error) { console.warn(`[Notification] ${type}\u901A\u77E5\u8C03\u7528\u5931\u8D25:`, error.message); return false; } } async function notifyOwner(payload) { const validated = validatePayload(payload); let sent = false; const webhooks = [ { url: process.env.NAC_NOTIFY_WECOM_URL, type: "wecom" }, { url: process.env.NAC_NOTIFY_DINGTALK_URL, type: "dingtalk" }, { url: process.env.NAC_NOTIFY_FEISHU_URL, type: "feishu" }, { url: process.env.NAC_NOTIFY_WEBHOOK_URL, type: "generic" } ]; for (const { url, type } of webhooks) { if (url) { const ok = await sendWebhook(url, type, validated); if (ok) sent = true; } } if (!sent) { const levelTag = validated.level ? `[${validated.level.toUpperCase()}]` : "[INFO]"; const moduleTag = validated.module ? `[${validated.module}]` : ""; console.log(`[Notification] ${levelTag}${moduleTag} ${validated.title}: ${validated.content.slice(0, 300)}`); } return true; } async function notifyCrawlerError(crawlerName, error) { await notifyOwner({ title: `\u91C7\u96C6\u5668\u5F02\u5E38\uFF1A${crawlerName}`, content: `\u91C7\u96C6\u5668 **${crawlerName}** \u53D1\u751F\u5F02\u5E38\uFF0C\u8BF7\u53CA\u65F6\u5904\u7406\u3002 **\u9519\u8BEF\u4FE1\u606F\uFF1A** ${error}`, level: "error", module: "crawler" }); } async function notifyBackupFailure(database, error) { await notifyOwner({ title: `MongoDB\u5907\u4EFD\u5931\u8D25\uFF1A${database}`, content: `\u6570\u636E\u5E93 **${database}** \u5907\u4EFD\u4EFB\u52A1\u5931\u8D25\uFF0C\u8BF7\u7ACB\u5373\u68C0\u67E5\u5907\u4EFD\u811A\u672C\u548C\u78C1\u76D8\u7A7A\u95F4\u3002 **\u9519\u8BEF\u4FE1\u606F\uFF1A** ${error}`, level: "critical", module: "backup" }); } async function notifyApprovalAlert(caseNumber, message) { await notifyOwner({ title: `\u5BA1\u6279\u6848\u4F8B\u544A\u8B66\uFF1A${caseNumber}`, content: message, level: "warning", module: "approval" }); } function getWebhookStatus() { const wecom = !!process.env.NAC_NOTIFY_WECOM_URL; const dingtalk = !!process.env.NAC_NOTIFY_DINGTALK_URL; const feishu = !!process.env.NAC_NOTIFY_FEISHU_URL; const generic = !!process.env.NAC_NOTIFY_WEBHOOK_URL; return { wecom, dingtalk, feishu, generic, anyConfigured: wecom || dingtalk || feishu || generic }; } var TITLE_MAX_LENGTH, CONTENT_MAX_LENGTH, trimValue, isNonEmptyString, validatePayload, LEVEL_COLORS, LEVEL_EMOJI; var init_notification = __esm({ "server/_core/notification.ts"() { "use strict"; TITLE_MAX_LENGTH = 1200; CONTENT_MAX_LENGTH = 2e4; trimValue = (value) => value.trim(); isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0; validatePayload = (input) => { if (!isNonEmptyString(input.title)) { throw new TRPCError2({ code: "BAD_REQUEST", message: "Notification title is required." }); } if (!isNonEmptyString(input.content)) { throw new TRPCError2({ code: "BAD_REQUEST", message: "Notification content is required." }); } const title = trimValue(input.title); const content = trimValue(input.content); if (title.length > TITLE_MAX_LENGTH) { throw new TRPCError2({ code: "BAD_REQUEST", message: `Notification title must be at most ${TITLE_MAX_LENGTH} characters.` }); } if (content.length > CONTENT_MAX_LENGTH) { throw new TRPCError2({ code: "BAD_REQUEST", message: `Notification content must be at most ${CONTENT_MAX_LENGTH} characters.` }); } return { title, content, level: input.level || "info", module: input.module }; }; LEVEL_COLORS = { info: "green", warning: "orange", error: "red", critical: "red" }; LEVEL_EMOJI = { info: "\u2705", warning: "\u26A0\uFE0F", error: "\u274C", critical: "\u{1F6A8}" }; } }); // server/_core/vite.ts var vite_exports = {}; __export(vite_exports, { serveStatic: () => serveStatic, setupVite: () => setupVite }); import express from "express"; import fs from "fs"; import { nanoid } from "nanoid"; import path from "path"; import { fileURLToPath } from "url"; import { createServer as createViteServer } from "vite"; async function setupVite(app, server) { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const serverOptions = { middlewareMode: true, hmr: { server }, allowedHosts: true }; const vite = await createViteServer({ configFile: path.resolve(__dirname, "../../vite.config.ts"), server: serverOptions, appType: "custom" }); app.use(vite.middlewares); app.use("*", async (req, res, next) => { const url = req.originalUrl; try { const clientTemplate = path.resolve( __dirname, "../..", "client", "index.html" ); let template = await fs.promises.readFile(clientTemplate, "utf-8"); template = template.replace( `src="/src/main.tsx"`, `src="/src/main.tsx?v=${nanoid()}"` ); const page = await vite.transformIndexHtml(url, template); res.status(200).set({ "Content-Type": "text/html" }).end(page); } catch (e) { vite.ssrFixStacktrace(e); next(e); } }); } function serveStatic(app) { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const distPath = process.env.NODE_ENV === "development" ? path.resolve(__dirname, "../..", "dist", "public") : path.resolve(__dirname, "public"); if (!fs.existsSync(distPath)) { console.error( `Could not find the build directory: ${distPath}, make sure to build the client first` ); } app.use(express.static(distPath)); app.use("*", (_req, res) => { res.sendFile(path.resolve(distPath, "index.html")); }); } var init_vite = __esm({ "server/_core/vite.ts"() { "use strict"; } }); // server/_core/static.ts var static_exports = {}; __export(static_exports, { serveStatic: () => serveStatic2 }); import express2 from "express"; import fs2 from "fs"; import path2 from "path"; import { fileURLToPath as fileURLToPath2 } from "url"; function serveStatic2(app) { const __filename = fileURLToPath2(import.meta.url); const __dirname = path2.dirname(__filename); const distPath = path2.resolve(__dirname, "public"); if (!fs2.existsSync(distPath)) { console.error( `[Static] Could not find build directory: ${distPath}` ); console.error(`[Static] Make sure to run 'pnpm build' first`); app.use("*", (_req, res) => { res.status(503).send("Service starting up, please wait..."); }); return; } console.log(`[Static] Serving files from: ${distPath}`); app.use(express2.static(distPath)); app.use("*", (_req, res) => { res.sendFile(path2.resolve(distPath, "index.html")); }); } var init_static = __esm({ "server/_core/static.ts"() { "use strict"; } }); // server/_core/index.ts import "dotenv/config"; import express3 from "express"; import cookieParser from "cookie-parser"; import { createServer } from "http"; import net from "net"; import { createExpressMiddleware } from "@trpc/server/adapters/express"; // server/routers.ts import { z as z2 } from "zod"; import { TRPCError as TRPCError3 } from "@trpc/server"; // shared/const.ts var ONE_YEAR_MS = 1e3 * 60 * 60 * 24 * 365; var UNAUTHED_ERR_MSG = "Please login (10001)"; var NOT_ADMIN_ERR_MSG = "You do not have required permission (10002)"; // server/_core/trpc.ts import { initTRPC, TRPCError } from "@trpc/server"; import superjson from "superjson"; var t = initTRPC.context().create({ transformer: superjson }); var router = t.router; var publicProcedure = t.procedure; var requireUser = t.middleware(async (opts) => { const { ctx, next } = opts; if (!ctx.user) { throw new TRPCError({ code: "UNAUTHORIZED", message: UNAUTHED_ERR_MSG }); } return next({ ctx: { ...ctx, user: ctx.user } }); }); var protectedProcedure = t.procedure.use(requireUser); var adminProcedure = t.procedure.use( t.middleware(async (opts) => { const { ctx, next } = opts; if (!ctx.user || ctx.user.role !== "admin") { throw new TRPCError({ code: "FORBIDDEN", message: NOT_ADMIN_ERR_MSG }); } return next({ ctx: { ...ctx, user: ctx.user } }); }) ); // server/_core/systemRouter.ts init_notification(); import { z } from "zod"; var systemRouter = router({ health: publicProcedure.input( z.object({ timestamp: z.number().min(0, "timestamp cannot be negative") }) ).query(() => ({ ok: true })), notifyOwner: adminProcedure.input( z.object({ title: z.string().min(1, "title is required"), content: z.string().min(1, "content is required") }) ).mutation(async ({ input }) => { const delivered = await notifyOwner(input); return { success: delivered }; }) }); // server/nacAuth.ts import mysql from "mysql2/promise"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; // server/secrets.ts function readSecret(key) { return process.env[key]; } function getNacMysqlUrl() { const val = readSecret("NAC_MYSQL_URL"); if (!val) throw new Error("[Secrets] NAC_MYSQL_URL \u672A\u914D\u7F6E"); return val; } function getNacMongoUrl() { const val = readSecret("NAC_MONGO_URL"); if (!val) throw new Error("[Secrets] NAC_MONGO_URL \u672A\u914D\u7F6E"); return val; } function getNacJwtSecret() { const val = readSecret("NAC_JWT_SECRET"); if (!val) throw new Error("[Secrets] NAC_JWT_SECRET \u672A\u914D\u7F6E"); return val; } // server/nacAuth.ts var pool = null; function getNacMysqlPool() { if (pool) return pool; const url = new URL(getNacMysqlUrl()); pool = mysql.createPool({ host: url.hostname, port: parseInt(url.port || "3306"), user: url.username, password: url.password, database: url.pathname.slice(1), waitForConnections: true, connectionLimit: 5, connectTimeout: 1e4 }); return pool; } function resolveRole(kyc_level, node_status) { if (kyc_level >= 2 && node_status === "constitutional") return "admin"; if (kyc_level >= 1) return "legal"; return "reviewer"; } async function loginWithNacCredentials(email, password) { const pool2 = getNacMysqlPool(); const [rows] = await pool2.execute( "SELECT id, name, email, password, kyc_level, node_status, is_active FROM users WHERE email = ? AND is_active = 1", [email] ); if (rows.length === 0) return null; const dbUser = rows[0]; const valid = await bcrypt.compare(password, dbUser.password); if (!valid) return null; const role = resolveRole(dbUser.kyc_level, dbUser.node_status); const user = { id: dbUser.id, name: dbUser.name, email: dbUser.email, kyc_level: dbUser.kyc_level, node_status: dbUser.node_status, is_active: dbUser.is_active, role }; const token = jwt.sign( { id: user.id, email: user.email, role: user.role }, getNacJwtSecret(), { expiresIn: "24h" } ); return { user, token }; } function verifyNacToken(token) { try { return jwt.verify(token, getNacJwtSecret()); } catch { return null; } } async function getNacUserById(id) { const pool2 = getNacMysqlPool(); const [rows] = await pool2.execute( "SELECT id, name, email, kyc_level, node_status, is_active FROM users WHERE id = ? AND is_active = 1", [id] ); if (rows.length === 0) return null; const r = rows[0]; return { ...r, role: resolveRole(r.kyc_level, r.node_status) }; } async function listNacUsers(limit = 50, offset = 0) { const pool2 = getNacMysqlPool(); const [rows] = await pool2.execute( "SELECT id, name, email, kyc_level, node_status, is_active, created_at, last_login_at FROM users ORDER BY id DESC LIMIT ? OFFSET ?", [limit, offset] ); return rows.map((r) => ({ ...r, role: resolveRole(r.kyc_level, r.node_status) })); } async function getNacUserCount() { const pool2 = getNacMysqlPool(); const [rows] = await pool2.execute("SELECT COUNT(*) as cnt FROM users"); return rows[0]?.cnt || 0; } // server/mongodb.ts import { MongoClient } from "mongodb"; var client = null; var db = null; async function getMongoDb() { if (db) return db; try { client = new MongoClient(getNacMongoUrl(), { serverSelectionTimeoutMS: 5e3, connectTimeoutMS: 5e3 }); await client.connect(); db = client.db("nac_knowledge_engine"); console.log("[MongoDB] Connected to nac_knowledge_engine"); return db; } catch (error) { console.error("[MongoDB] Connection failed:", error.message); return null; } } var COLLECTIONS = { COMPLIANCE_RULES: "compliance_rules", CRAWLERS: "crawlers", CRAWLER_LOGS: "crawler_logs", APPROVAL_CASES: "approval_cases", TAG_RULES: "tag_rules", PROTOCOL_REGISTRY: "protocol_registry", AUDIT_LOGS: "audit_logs", KNOWLEDGE_STATS: "knowledge_stats", // AI智能体对话历史 AGENT_CONVERSATIONS: "agent_conversations", AGENT_MESSAGES: "agent_messages" }; // server/routers.ts import { ObjectId as ObjectId2 } from "mongodb"; // server/_core/cookies.ts function isSecureRequest(req) { if (req.protocol === "https") return true; const forwardedProto = req.headers["x-forwarded-proto"]; if (!forwardedProto) return false; const protoList = Array.isArray(forwardedProto) ? forwardedProto : forwardedProto.split(","); return protoList.some((proto) => proto.trim().toLowerCase() === "https"); } function getSessionCookieOptions(req) { return { httpOnly: true, path: "/", sameSite: "none", secure: isSecureRequest(req) }; } // server/i18nTranslation.ts var SUPPORTED_LANGUAGES = ["zh", "en", "ar", "ja", "ko", "fr", "ru"]; var LANGUAGE_NAMES = { zh: "\u4E2D\u6587\uFF08\u7B80\u4F53\uFF09", en: "English", ar: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", ja: "\u65E5\u672C\u8A9E", ko: "\uD55C\uAD6D\uC5B4", fr: "Fran\xE7ais", ru: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" }; var RTL_LANGUAGES = ["ar"]; function isRTL(lang) { return RTL_LANGUAGES.includes(lang); } function isAiTranslationConfigured() { return !!(process.env.NAC_AI_API_URL && process.env.NAC_AI_API_KEY); } async function callAiTranslation(systemPrompt, userPrompt) { const apiUrl = process.env.NAC_AI_API_URL; const apiKey = process.env.NAC_AI_API_KEY; const model = process.env.NAC_AI_MODEL || "gpt-3.5-turbo"; if (!apiUrl || !apiKey) { throw new Error( "[AI\u7FFB\u8BD1] \u672A\u914D\u7F6EAI\u63A5\u53E3\u3002\u8BF7\u5728 .env \u6587\u4EF6\u4E2D\u8BBE\u7F6E NAC_AI_API_URL \u548C NAC_AI_API_KEY\u3002\n\u652F\u6301\u4EFB\u4F55OpenAI\u517C\u5BB9\u63A5\u53E3\uFF08\u5982 OpenAI\u3001Azure OpenAI\u3001\u56FD\u5185\u5927\u6A21\u578B\u7B49\uFF09\u3002" ); } const endpoint = `${apiUrl.replace(/\/$/, "")}/v1/chat/completions`; const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt } ], max_tokens: 2048, temperature: 0.3 // 低温度保证翻译一致性 }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`AI\u63A5\u53E3\u8C03\u7528\u5931\u8D25: ${response.status} ${response.statusText} \u2013 ${errorText}`); } const result = await response.json(); const content = result.choices?.[0]?.message?.content; return (typeof content === "string" ? content : "").trim(); } async function translateText(sourceText, sourceLang, targetLang, context) { if (sourceLang === targetLang) return sourceText; if (!sourceText.trim()) return sourceText; const contextHint = context ? ` \u80CC\u666F\u4FE1\u606F\uFF08\u4EC5\u4F9B\u53C2\u8003\uFF0C\u4E0D\u8981\u7FFB\u8BD1\uFF09\uFF1A${context}` : ""; const arabicHint = targetLang === "ar" ? "\n\u7279\u522B\u8BF4\u660E\uFF1A\u7FFB\u8BD1\u6210\u6807\u51C6\u73B0\u4EE3\u963F\u62C9\u4F2F\u8BED\uFF08MSA\uFF09\uFF0C\u4F7F\u7528Unicode\u963F\u62C9\u4F2F\u5B57\u7B26\uFF0C\u6587\u672C\u65B9\u5411\u4E3A\u4ECE\u53F3\u5230\u5DE6\uFF08RTL\uFF09\u3002" : ""; const systemPrompt = `\u4F60\u662FNAC\uFF08NewAssetChain\uFF09\u516C\u94FE\u7684\u4E13\u4E1A\u6CD5\u5F8B\u5408\u89C4\u7FFB\u8BD1\u4E13\u5BB6\u3002 NAC\u662F\u4E00\u6761\u4E13\u6CE8\u4E8ERWA\uFF08\u771F\u5B9E\u4E16\u754C\u8D44\u4EA7\uFF09\u7684\u539F\u751F\u516C\u94FE\uFF0C\u4F7F\u7528Charter\u667A\u80FD\u5408\u7EA6\u8BED\u8A00\u3001NVM\u865A\u62DF\u673A\u3001CBPP\u5171\u8BC6\u534F\u8BAE\u3002 \u4F60\u7684\u4EFB\u52A1\u662F\u5C06\u5408\u89C4\u89C4\u5219\u6587\u672C\u4ECE${LANGUAGE_NAMES[sourceLang]}\u7FFB\u8BD1\u6210${LANGUAGE_NAMES[targetLang]}\u3002 \u8981\u6C42\uFF1A 1. \u4FDD\u6301\u6CD5\u5F8B\u672F\u8BED\u7684\u51C6\u786E\u6027\u548C\u4E13\u4E1A\u6027 2. \u4FDD\u7559\u4E13\u6709\u540D\u8BCD\uFF08\u5982\uFF1ANAC\u3001RWA\u3001Charter\u3001NVM\u3001CBPP\u3001CSNP\u3001CNNL\u3001ACC-20\u3001GNACS\u3001XTZH\uFF09\u4E0D\u7FFB\u8BD1 3. \u4FDD\u7559\u673A\u6784\u540D\u79F0\uFF08\u5982\uFF1ASEC\u3001SFC\u3001MAS\u3001ESMA\u3001DFSA\u3001DLD\uFF09\u4E0D\u7FFB\u8BD1 4. \u53EA\u8FD4\u56DE\u7FFB\u8BD1\u7ED3\u679C\uFF0C\u4E0D\u8981\u6DFB\u52A0\u4EFB\u4F55\u89E3\u91CA\u6216\u6CE8\u91CA${arabicHint}`; const userPrompt = `\u8BF7\u5C06\u4EE5\u4E0B\u6587\u672C\u7FFB\u8BD1\u6210${LANGUAGE_NAMES[targetLang]}\uFF1A ${sourceText}${contextHint}`; try { const translated = await callAiTranslation(systemPrompt, userPrompt); return translated || sourceText; } catch (error) { console.error(`[Translation] \u7FFB\u8BD1\u5230 ${targetLang} \u5931\u8D25:`, error.message); return sourceText; } } async function generateRuleTranslations(ruleName, description, sourceLang = "zh", existingTranslations) { const ruleNameI18n = { ...existingTranslations?.ruleNameI18n || {} }; const descriptionI18n = { ...existingTranslations?.descriptionI18n || {} }; ruleNameI18n[sourceLang] = ruleName; descriptionI18n[sourceLang] = description; const targetLangs = SUPPORTED_LANGUAGES.filter( (lang) => lang !== sourceLang && !ruleNameI18n[lang] ); await Promise.all( targetLangs.map(async (targetLang) => { const [translatedName, translatedDesc] = await Promise.all([ translateText(ruleName, sourceLang, targetLang, `\u8FD9\u662F\u4E00\u6761${description.slice(0, 50)}...\u7684\u5408\u89C4\u89C4\u5219`), translateText(description, sourceLang, targetLang) ]); ruleNameI18n[targetLang] = translatedName; descriptionI18n[targetLang] = translatedDesc; }) ); return { ruleNameI18n, descriptionI18n }; } async function migrateRuleToMultiLang(rule) { const sourceLang = "zh"; return generateRuleTranslations( rule.ruleName, rule.description, sourceLang, { ruleNameI18n: rule.ruleNameI18n, descriptionI18n: rule.descriptionI18n } ); } var ARABIC_RTL_TEST_CASES = [ { id: "ar-rtl-001", sourceLang: "zh", targetLang: "ar", sourceText: "\u4E0D\u52A8\u4EA7\u767B\u8BB0\u8BC1\u8981\u6C42", expectedContains: ["\u0639\u0642\u0627\u0631", "\u062A\u0633\u062C\u064A\u0644", "\u0634\u0647\u0627\u062F\u0629"], // 应包含房产/登记/证书相关词汇 isRTL: true, description: "\u623F\u4EA7\u767B\u8BB0\u8BC1\u8981\u6C42\uFF08\u963F\u62C9\u4F2F\u8BEDRTL\u6D4B\u8BD5\uFF09" }, { id: "ar-rtl-002", sourceLang: "en", targetLang: "ar", sourceText: "RWA asset compliance verification", expectedContains: ["\u0627\u0645\u062A\u062B\u0627\u0644", "\u0623\u0635\u0648\u0644", "\u0627\u0644\u062A\u062D\u0642\u0642"], // 应包含合规/资产/验证相关词汇 isRTL: true, description: "RWA\u8D44\u4EA7\u5408\u89C4\u9A8C\u8BC1\uFF08\u963F\u62C9\u4F2F\u8BEDRTL\u6D4B\u8BD5\uFF09" }, { id: "ar-rtl-003", sourceLang: "zh", targetLang: "ar", sourceText: "NAC\u516C\u94FE\u667A\u80FD\u5408\u7EA6\u5BA1\u6279\u6D41\u7A0B", expectedContains: ["NAC", "\u0639\u0642\u062F", "\u0645\u0648\u0627\u0641\u0642\u0629"], // NAC不翻译,包含合约/审批相关词汇 isRTL: true, description: "NAC\u4E13\u6709\u540D\u8BCD\u4FDD\u7559\u6D4B\u8BD5\uFF08\u963F\u62C9\u4F2F\u8BEDRTL\uFF09" } ]; async function runArabicRTLTests() { if (!isAiTranslationConfigured()) { return { passed: 0, failed: ARABIC_RTL_TEST_CASES.length, results: ARABIC_RTL_TEST_CASES.map((tc) => ({ id: tc.id, description: tc.description, passed: false, translated: "", isRTL: true, issues: ["AI\u7FFB\u8BD1\u670D\u52A1\u672A\u914D\u7F6E\uFF0C\u8BF7\u8BBE\u7F6E NAC_AI_API_URL \u548C NAC_AI_API_KEY"] })) }; } const results = await Promise.all( ARABIC_RTL_TEST_CASES.map(async (tc) => { const issues = []; let translated = ""; try { translated = await translateText(tc.sourceText, tc.sourceLang, tc.targetLang); const hasArabicChars = /[\u0600-\u06FF]/.test(translated); if (!hasArabicChars) { issues.push("\u7FFB\u8BD1\u7ED3\u679C\u4E0D\u5305\u542B\u963F\u62C9\u4F2F\u5B57\u7B26"); } if (tc.sourceText.includes("NAC") && !translated.includes("NAC")) { issues.push("\u4E13\u6709\u540D\u8BCDNAC\u88AB\u9519\u8BEF\u7FFB\u8BD1\uFF0C\u5E94\u4FDD\u7559\u82F1\u6587"); } if (!translated.trim()) { issues.push("\u7FFB\u8BD1\u7ED3\u679C\u4E3A\u7A7A"); } } catch (error) { issues.push(`\u7FFB\u8BD1\u5931\u8D25: ${error.message}`); } return { id: tc.id, description: tc.description, passed: issues.length === 0, translated, isRTL: tc.isRTL, issues }; }) ); const passed = results.filter((r) => r.passed).length; const failed = results.filter((r) => !r.passed).length; return { passed, failed, results }; } // server/ragRetrieval.ts function extractKeywords(query) { const STOP_WORDS = /* @__PURE__ */ new Set([ "\u7684", "\u4E86", "\u662F", "\u5728", "\u6211", "\u6709", "\u548C", "\u5C31", "\u4E0D", "\u4EBA", "\u90FD", "\u4E00", "\u4E00\u4E2A", "\u4E0A", "\u4E5F", "\u5F88", "\u5230", "\u8BF4", "\u8981", "\u53BB", "\u4F60", "\u4F1A", "\u7740", "\u6CA1\u6709", "\u770B", "\u597D", "\u81EA\u5DF1", "\u8FD9", "\u90A3", "\u4EC0\u4E48", "\u5982\u4F55", "\u600E\u4E48", "\u8BF7\u95EE", "\u5E2E\u6211", "\u544A\u8BC9", "\u4ECB\u7ECD", "\u5173\u4E8E", "\u5BF9\u4E8E", "\u9488\u5BF9", "\u9700\u8981", "\u53EF\u4EE5", "\u5E94\u8BE5", "\u5FC5\u987B", "\u89C4\u5B9A", "\u8981\u6C42", "the", "a", "an", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "do", "does", "did", "will", "would", "could", "should", "what", "how", "when", "where", "why", "which", "who" ]); const chineseTerms = query.match(/[\u4e00-\u9fa5]{2,8}/g) || []; const englishTerms = query.match(/[a-zA-Z]{3,}/g) || []; const numbers = query.match(/\d+/g) || []; const allTerms = [...chineseTerms, ...englishTerms, ...numbers]; const filtered = allTerms.filter((t2) => !STOP_WORDS.has(t2.toLowerCase())); return Array.from(new Set(filtered)).slice(0, 8); } async function retrieveRelevantRules(query, options = {}) { const { maxResults = 5, jurisdictions, categories, language = "zh" } = options; const db2 = await getMongoDb(); if (!db2) { return { rules: [], totalFound: 0, retrievalMethod: "none", queryKeywords: [] }; } const keywords = extractKeywords(query); const collection = db2.collection(COLLECTIONS.COMPLIANCE_RULES); const baseFilter = {}; if (jurisdictions && jurisdictions.length > 0) { baseFilter.jurisdiction = { $in: jurisdictions }; } if (categories && categories.length > 0) { baseFilter.category = { $in: categories }; } let rules = []; let retrievalMethod = "none"; if (keywords.length > 0) { try { const searchText = keywords.join(" "); const textFilter = { ...baseFilter, $text: { $search: searchText } }; const textResults = await collection.find(textFilter, { projection: { score: { $meta: "textScore" }, ruleId: 1, ruleName: 1, jurisdiction: 1, category: 1, content: 1, description: 1, // 多语言字段 "translations.zh": 1, "translations.en": 1 } }).sort({ score: { $meta: "textScore" } }).limit(maxResults).toArray(); if (textResults.length > 0) { rules = textResults.map((doc, idx) => formatRule(doc, language, idx, textResults.length)); retrievalMethod = "fulltext"; } } catch (e) { console.warn("[RAG] \u5168\u6587\u68C0\u7D22\u5931\u8D25\uFF0C\u964D\u7EA7\u5230\u6B63\u5219\u68C0\u7D22:", e.message); } } if (rules.length === 0 && keywords.length > 0) { try { const regexConditions = keywords.slice(0, 4).map((kw) => ({ $or: [ { ruleName: { $regex: kw, $options: "i" } }, { description: { $regex: kw, $options: "i" } }, { content: { $regex: kw, $options: "i" } }, { "translations.zh": { $regex: kw, $options: "i" } } ] })); const regexFilter = { ...baseFilter, $and: regexConditions }; const regexResults = await collection.find(regexFilter).limit(maxResults).toArray(); if (regexResults.length > 0) { rules = regexResults.map((doc, idx) => formatRule(doc, language, idx, regexResults.length)); retrievalMethod = "regex"; } } catch (e) { console.warn("[RAG] \u6B63\u5219\u68C0\u7D22\u5931\u8D25:", e.message); } } if (rules.length === 0) { try { const sampleResults = await collection.aggregate([ { $match: baseFilter }, { $sample: { size: maxResults } } ]).toArray(); if (sampleResults.length > 0) { rules = sampleResults.map((doc, idx) => formatRule(doc, language, idx, sampleResults.length, 0.3)); retrievalMethod = "sample"; } } catch (e) { console.warn("[RAG] \u968F\u673A\u91C7\u6837\u5931\u8D25:", e.message); } } return { rules, totalFound: rules.length, retrievalMethod, queryKeywords: keywords }; } function formatRule(doc, language, idx, total, baseScore) { const score = baseScore !== void 0 ? baseScore : Math.max(0.4, 1 - idx / total * 0.5); const translations = doc.translations; let content = ""; if (translations && translations[language]) { content = translations[language]; } else if (typeof doc.content === "string") { content = doc.content; } else if (translations?.zh) { content = translations.zh; } else if (translations?.en) { content = translations.en; } const truncatedContent = content.length > 500 ? content.slice(0, 500) + "..." : content; const ruleId = String(doc.ruleId || doc._id || ""); const ruleName = String(doc.ruleName || "\u672A\u547D\u540D\u89C4\u5219"); const jurisdiction = String(doc.jurisdiction || "\u672A\u77E5"); const category = String(doc.category || "\u901A\u7528"); const description = doc.description ? String(doc.description) : void 0; return { ruleId, ruleName, jurisdiction, category, content: truncatedContent, description, score, source: `${jurisdiction}\xB7${category}\xB7${ruleName.slice(0, 20)}` }; } function buildRAGPromptContext(ragCtx) { if (ragCtx.rules.length === 0) { return ""; } const lines = [ "\u3010\u77E5\u8BC6\u5E93\u68C0\u7D22\u7ED3\u679C\u3011", `\uFF08\u5171\u68C0\u7D22\u5230 ${ragCtx.totalFound} \u6761\u76F8\u5173\u89C4\u5219\uFF0C\u68C0\u7D22\u65B9\u5F0F\uFF1A${ragCtx.retrievalMethod}\uFF09`, "" ]; ragCtx.rules.forEach((rule, idx) => { lines.push(`\u3010\u89C4\u5219 ${idx + 1}\u3011${rule.ruleName}`); lines.push(` \u7BA1\u8F96\u533A\uFF1A${rule.jurisdiction} | \u5206\u7C7B\uFF1A${rule.category} | \u76F8\u5173\u5EA6\uFF1A${Math.round(rule.score * 100)}%`); if (rule.description) { lines.push(` \u6458\u8981\uFF1A${rule.description}`); } lines.push(` \u5185\u5BB9\uFF1A${rule.content}`); lines.push(""); }); lines.push("\u8BF7\u57FA\u4E8E\u4EE5\u4E0A\u77E5\u8BC6\u5E93\u5185\u5BB9\u56DE\u7B54\u7528\u6237\u95EE\u9898\uFF0C\u5E76\u5728\u56DE\u7B54\u4E2D\u6CE8\u660E\u5F15\u7528\u7684\u89C4\u5219\u6765\u6E90\u3002"); return lines.join("\n"); } // server/aiAgents.ts function isAgentConfigured() { return !!(process.env.NAC_AI_API_URL && process.env.NAC_AI_API_KEY); } async function callAgentLLM(messages, maxTokens = 2048, temperature = 0.7) { const apiUrl = process.env.NAC_AI_API_URL; const apiKey = process.env.NAC_AI_API_KEY; const model = process.env.NAC_AI_MODEL || "qwen-plus"; if (!apiUrl || !apiKey) { throw new Error( "[AI Agent] \u672A\u914D\u7F6EAI\u63A5\u53E3\u3002\u8BF7\u5728 .env \u6587\u4EF6\u4E2D\u8BBE\u7F6E NAC_AI_API_URL \u548C NAC_AI_API_KEY\u3002\n\u63A8\u8350\uFF1A\u963F\u91CC\u4E91\u901A\u4E49\u5343\u95EE https://dashscope.aliyuncs.com/compatible-mode" ); } const endpoint = `${apiUrl.replace(/\/$/, "")}/v1/chat/completions`; const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model, messages, max_tokens: maxTokens, temperature, stream: false }) }); if (!response.ok) { const errorText = await response.text().catch(() => ""); throw new Error(`AI\u63A5\u53E3\u8C03\u7528\u5931\u8D25: ${response.status} ${response.statusText} \u2013 ${errorText.slice(0, 500)}`); } const result = await response.json(); const content = result.choices?.[0]?.message?.content; if (!content) throw new Error("AI\u63A5\u53E3\u8FD4\u56DE\u7A7A\u5185\u5BB9"); return content.trim(); } var KNOWLEDGE_QA_SYSTEM_PROMPT = `\u4F60\u662FNAC\uFF08NewAssetChain\uFF09\u516C\u94FE\u7684\u5408\u89C4\u77E5\u8BC6\u5E93\u4E13\u5BB6\u52A9\u624B\u3002 NAC\u662F\u4E00\u6761\u4E13\u6CE8\u4E8ERWA\uFF08\u771F\u5B9E\u4E16\u754C\u8D44\u4EA7\uFF09\u7684\u539F\u751F\u516C\u94FE\uFF0C\u4F7F\u7528Charter\u667A\u80FD\u5408\u7EA6\u8BED\u8A00\u3001NVM\u865A\u62DF\u673A\u3001CBPP\u5171\u8BC6\u534F\u8BAE\u3001CSNP\u7F51\u7EDC\u3002 \u4F60\u7684\u804C\u8D23\uFF1A 1. \u56DE\u7B54\u5173\u4E8ENAC\u5408\u89C4\u89C4\u5219\u7684\u95EE\u9898 2. \u89E3\u91CA\u5404\u53F8\u6CD5\u7BA1\u8F96\u533A\uFF08\u4E2D\u56FDCN\u3001\u9999\u6E2FHK\u3001\u7F8E\u56FDUS\u3001\u6B27\u76DFEU\u3001\u65B0\u52A0\u5761SG\u3001\u963F\u8054\u914BAE\uFF09\u7684\u5408\u89C4\u8981\u6C42 3. \u6307\u5BFC\u7528\u6237\u4E86\u89E3RWA\u8D44\u4EA7\u4E0A\u94FE\u7684\u5408\u89C4\u6D41\u7A0B 4. \u89E3\u91CA\u4E03\u5C42\u5408\u89C4\u9A8C\u8BC1\u6846\u67B6\uFF08L1\u8EAB\u4EFD\u9A8C\u8BC1\u2192L7\u6700\u7EC8\u5BA1\u6279\uFF09 \u56DE\u7B54\u8981\u6C42\uFF1A - \u4E13\u4E1A\u3001\u51C6\u786E\u3001\u7B80\u6D01 - \u5F15\u7528\u5177\u4F53\u7684\u5408\u89C4\u89C4\u5219\u540D\u79F0 - \u5BF9\u4E8E\u4E0D\u786E\u5B9A\u7684\u5185\u5BB9\uFF0C\u660E\u786E\u8BF4\u660E\u9700\u8981\u8FDB\u4E00\u6B65\u6838\u5B9E - \u4FDD\u7559\u4E13\u6709\u540D\u8BCD\uFF08NAC\u3001RWA\u3001Charter\u3001NVM\u3001CBPP\u3001CSNP\u3001CNNL\u3001ACC-20\u3001GNACS\u3001XTZH\uFF09\u4E0D\u7FFB\u8BD1`; async function runKnowledgeQAAgent(userMessage, history, context) { const ragCtx = await retrieveRelevantRules(userMessage, { maxResults: 5, jurisdictions: context?.jurisdiction ? [String(context.jurisdiction)] : void 0, language: String(context?.language || "zh") }); const ragPromptSection = buildRAGPromptContext(ragCtx); const sources = ragCtx.rules.map((r) => r.source); const baseConfidence = ragCtx.retrievalMethod === "fulltext" ? 0.9 : ragCtx.retrievalMethod === "regex" ? 0.8 : ragCtx.retrievalMethod === "sample" ? 0.65 : 0.55; const systemPrompt = KNOWLEDGE_QA_SYSTEM_PROMPT + (context?.jurisdiction ? ` \u5F53\u524D\u5173\u6CE8\u7684\u53F8\u6CD5\u7BA1\u8F96\u533A\uFF1A${context.jurisdiction}` : "") + (ragPromptSection ? ` ${ragPromptSection}` : ""); const messages = [ { role: "system", content: systemPrompt }, ...history.slice(-6), // 保留最近6条历史 { role: "user", content: userMessage } ]; const reply = await callAgentLLM(messages, 1024, 0.5); return { agentType: "knowledge_qa", message: reply, confidence: baseConfidence, sources, suggestions: [ "\u67E5\u770B\u76F8\u5173\u53F8\u6CD5\u7BA1\u8F96\u533A\u7684\u5B8C\u6574\u5408\u89C4\u89C4\u5219", "\u63D0\u4EA4\u8D44\u4EA7\u4E0A\u94FE\u7533\u8BF7", "\u4E86\u89E3\u4E03\u5C42\u5408\u89C4\u9A8C\u8BC1\u6D41\u7A0B" ], metadata: { ragMethod: ragCtx.retrievalMethod, ragKeywords: ragCtx.queryKeywords, ragRulesCount: ragCtx.totalFound } }; } var COMPLIANCE_ANALYSIS_SYSTEM_PROMPT = `\u4F60\u662FNAC\u516C\u94FE\u7684\u4E03\u5C42\u5408\u89C4\u9A8C\u8BC1\u5206\u6790\u4E13\u5BB6\u3002 \u4E03\u5C42\u5408\u89C4\u9A8C\u8BC1\u6846\u67B6\uFF1A L1: \u8EAB\u4EFD\u9A8C\u8BC1\uFF08KYC/AML\uFF09- \u57FA\u4E8EACC-20\u534F\u8BAE L2: \u8D44\u4EA7\u771F\u5B9E\u6027\u9A8C\u8BC1 - \u57FA\u4E8ECharter\u667A\u80FD\u5408\u7EA6 L3: \u53F8\u6CD5\u7BA1\u8F96\u5408\u89C4 - \u57FA\u4E8ECNNL\u795E\u7ECF\u7F51\u7EDC\u8BED\u8A00 L4: \u8D44\u4EA7\u4F30\u503C\u5408\u7406\u6027 - \u57FA\u4E8EXTZH\u7A33\u5B9A\u673A\u5236 L5: \u6CD5\u5F8B\u6587\u4EF6\u5B8C\u6574\u6027 - \u57FA\u4E8EGNACS\u5206\u7C7B\u7CFB\u7EDF L6: \u5BAA\u653F\u5408\u89C4\u5BA1\u67E5 - \u57FA\u4E8ECBPP\u5171\u8BC6\u534F\u8BAE L7: \u6700\u7EC8\u5BA1\u6279\u51B3\u7B56 - \u7BA1\u7406\u5458\u4EBA\u5DE5\u5BA1\u6279 \u4F60\u7684\u804C\u8D23\uFF1A 1. \u5206\u6790\u8D44\u4EA7\u4E0A\u94FE\u7533\u8BF7\u7684\u5408\u89C4\u98CE\u9669 2. \u8BC6\u522B\u7F3A\u5931\u7684\u5408\u89C4\u6750\u6599 3. \u8BC4\u4F30\u5404\u5C42\u9A8C\u8BC1\u7684\u901A\u8FC7\u53EF\u80FD\u6027 4. \u63D0\u4F9B\u5177\u4F53\u7684\u6539\u8FDB\u5EFA\u8BAE \u8F93\u51FA\u683C\u5F0F\uFF1A - \u5408\u89C4\u8BC4\u5206\uFF080-100\uFF09 - \u5404\u5C42\u72B6\u6001\uFF08\u901A\u8FC7/\u5F85\u5BA1/\u672A\u901A\u8FC7/\u4E0D\u9002\u7528\uFF09 - \u98CE\u9669\u70B9\u5217\u8868 - \u6539\u8FDB\u5EFA\u8BAE`; async function runComplianceAgent(userMessage, history, context) { const assetContext = context?.assetType ? ` \u5F85\u5206\u6790\u8D44\u4EA7\u7C7B\u578B\uFF1A${context.assetType} \u53F8\u6CD5\u7BA1\u8F96\u533A\uFF1A${context.jurisdiction || "\u672A\u6307\u5B9A"}` : ""; const messages = [ { role: "system", content: COMPLIANCE_ANALYSIS_SYSTEM_PROMPT + assetContext }, ...history.slice(-4), { role: "user", content: userMessage } ]; const reply = await callAgentLLM(messages, 1500, 0.3); return { agentType: "compliance", message: reply, confidence: 0.8, suggestions: [ "\u67E5\u770B\u5B8C\u6574\u7684\u4E03\u5C42\u5408\u89C4\u9A8C\u8BC1\u62A5\u544A", "\u4E0A\u4F20\u7F3A\u5931\u7684\u5408\u89C4\u6587\u4EF6", "\u8054\u7CFB\u5408\u89C4\u987E\u95EE" ] }; } var TRANSLATION_SYSTEM_PROMPT = `\u4F60\u662FNAC\u516C\u94FE\u7684\u4E13\u4E1A\u6CD5\u5F8B\u5408\u89C4\u7FFB\u8BD1\u4E13\u5BB6\u3002 \u652F\u6301\u8BED\u8A00\uFF1A\u4E2D\u6587\uFF08zh\uFF09\u3001\u82F1\u6587\uFF08en\uFF09\u3001\u963F\u62C9\u4F2F\u6587\uFF08ar\uFF09\u3001\u65E5\u6587\uFF08ja\uFF09\u3001\u97E9\u6587\uFF08ko\uFF09\u3001\u6CD5\u6587\uFF08fr\uFF09\u3001\u4FC4\u6587\uFF08ru\uFF09 \u7FFB\u8BD1\u8981\u6C42\uFF1A 1. \u4FDD\u6301\u6CD5\u5F8B\u672F\u8BED\u7684\u51C6\u786E\u6027\u548C\u4E13\u4E1A\u6027 2. \u4FDD\u7559\u4E13\u6709\u540D\u8BCD\uFF08NAC\u3001RWA\u3001Charter\u3001NVM\u3001CBPP\u3001CSNP\u3001CNNL\u3001ACC-20\u3001GNACS\u3001XTZH\uFF09\u4E0D\u7FFB\u8BD1 3. \u4FDD\u7559\u673A\u6784\u540D\u79F0\uFF08SEC\u3001SFC\u3001MAS\u3001ESMA\u3001DFSA\u3001DLD\uFF09\u4E0D\u7FFB\u8BD1 4. \u963F\u62C9\u4F2F\u8BED\u4F7F\u7528\u6807\u51C6\u73B0\u4EE3\u963F\u62C9\u4F2F\u8BED\uFF08MSA\uFF09\uFF0C\u6587\u672C\u65B9\u5411RTL 5. \u53EA\u8FD4\u56DE\u7FFB\u8BD1\u7ED3\u679C\uFF0C\u4E0D\u6DFB\u52A0\u89E3\u91CA`; async function runTranslationAgent(userMessage, history, context) { const langContext = context?.targetLang ? ` \u76EE\u6807\u8BED\u8A00\uFF1A${context.targetLang}` : ""; const messages = [ { role: "system", content: TRANSLATION_SYSTEM_PROMPT + langContext }, ...history.slice(-4), { role: "user", content: userMessage } ]; const reply = await callAgentLLM(messages, 2048, 0.2); return { agentType: "translation", message: reply, confidence: 0.9, metadata: { targetLang: context?.targetLang, isRTL: context?.targetLang === "ar" } }; } var APPROVAL_ASSIST_SYSTEM_PROMPT = `\u4F60\u662FNAC\u516C\u94FE\u5BA1\u6279\u5DE5\u4F5C\u6D41\u7684AI\u8F85\u52A9\u52A9\u624B\u3002 \u4F60\u7684\u804C\u8D23\uFF1A 1. \u5206\u6790\u5BA1\u6279\u6848\u4F8B\u7684\u5408\u89C4\u8BC4\u5206\u548C\u98CE\u9669\u70B9 2. \u6839\u636E\u4E03\u5C42\u5408\u89C4\u9A8C\u8BC1\u7ED3\u679C\u63D0\u4F9B\u5BA1\u6279\u5EFA\u8BAE 3. \u8BC6\u522B\u9AD8\u98CE\u9669\u6848\u4F8B\u5E76\u63D0\u9192\u5BA1\u6838\u5458\u5173\u6CE8 4. \u751F\u6210\u6807\u51C6\u5316\u7684\u5BA1\u6279\u610F\u89C1\u6A21\u677F \u5BA1\u6279\u51B3\u7B56\u4F9D\u636E\uFF1A - \u5408\u89C4\u8BC4\u5206 \u2265 90\uFF1A\u5EFA\u8BAE\u81EA\u52A8\u6279\u51C6\uFF08\u9700\u7BA1\u7406\u5458\u786E\u8BA4\uFF09 - \u5408\u89C4\u8BC4\u5206 70-89\uFF1A\u5EFA\u8BAE\u4EBA\u5DE5\u5BA1\u6838 - \u5408\u89C4\u8BC4\u5206 50-69\uFF1A\u5EFA\u8BAE\u8981\u6C42\u8865\u5145\u6750\u6599 - \u5408\u89C4\u8BC4\u5206 < 50\uFF1A\u5EFA\u8BAE\u62D2\u7EDD \u8F93\u51FA\u8981\u6C42\uFF1A - \u7ED9\u51FA\u660E\u786E\u7684\u5BA1\u6279\u5EFA\u8BAE\uFF08\u6279\u51C6/\u62D2\u7EDD/\u9700\u8865\u5145\u6750\u6599\uFF09 - \u5217\u51FA\u5173\u952E\u98CE\u9669\u70B9 - \u63D0\u4F9B\u6807\u51C6\u5316\u5BA1\u6279\u610F\u89C1\u6587\u672C`; async function runApprovalAssistAgent(userMessage, history, context) { const caseContext = context?.caseNumber ? ` \u6848\u4F8B\u7F16\u53F7\uFF1A${context.caseNumber} \u5408\u89C4\u8BC4\u5206\uFF1A${context.complianceScore || "\u672A\u77E5"} \u8D44\u4EA7\u7C7B\u578B\uFF1A${context.assetType || "\u672A\u77E5"} \u53F8\u6CD5\u7BA1\u8F96\u533A\uFF1A${context.jurisdiction || "\u672A\u77E5"}` : ""; const messages = [ { role: "system", content: APPROVAL_ASSIST_SYSTEM_PROMPT + caseContext }, ...history.slice(-4), { role: "user", content: userMessage } ]; const reply = await callAgentLLM(messages, 1200, 0.4); return { agentType: "approval_assist", message: reply, confidence: 0.75, suggestions: [ "\u67E5\u770B\u5B8C\u6574\u6848\u4F8B\u8BE6\u60C5", "\u6DFB\u52A0\u5BA1\u6838\u610F\u89C1", "\u66F4\u65B0\u5BA1\u6279\u72B6\u6001" ] }; } async function runAgent(request) { if (!isAgentConfigured()) { return { agentType: request.agentType, message: "AI\u667A\u80FD\u4F53\u670D\u52A1\u672A\u914D\u7F6E\u3002\u8BF7\u5728\u751F\u4EA7\u670D\u52A1\u5668 .env \u6587\u4EF6\u4E2D\u8BBE\u7F6E NAC_AI_API_URL \u548C NAC_AI_API_KEY\u3002\n\n\u63A8\u8350\u63A5\u5165\u963F\u91CC\u4E91\u901A\u4E49\u5343\u95EE\uFF08\u56FD\u5185\u8BBF\u95EE\u7A33\u5B9A\uFF09\uFF1A\n- NAC_AI_API_URL=https://dashscope.aliyuncs.com/compatible-mode\n- NAC_AI_API_KEY=sk-xxxxxxxx\n- NAC_AI_MODEL=qwen-plus", confidence: 0, suggestions: ["\u914D\u7F6EAI\u670D\u52A1\u540E\u91CD\u8BD5"] }; } const { agentType, userMessage, conversationHistory = [], context } = request; try { switch (agentType) { case "knowledge_qa": return await runKnowledgeQAAgent(userMessage, conversationHistory, context); case "compliance": return await runComplianceAgent(userMessage, conversationHistory, context); case "translation": return await runTranslationAgent(userMessage, conversationHistory, context); case "approval_assist": return await runApprovalAssistAgent(userMessage, conversationHistory, context); default: throw new Error(`\u672A\u77E5\u7684Agent\u7C7B\u578B: ${agentType}`); } } catch (error) { console.error(`[Agent:${agentType}] \u6267\u884C\u5931\u8D25:`, error.message); return { agentType, message: `Agent\u6267\u884C\u5931\u8D25: ${error.message}`, confidence: 0 }; } } var AGENT_REGISTRY = [ { type: "knowledge_qa", name: "\u77E5\u8BC6\u5E93\u95EE\u7B54\u52A9\u624B", nameEn: "Knowledge QA Agent", description: "\u57FA\u4E8ENAC\u5408\u89C4\u89C4\u5219\u5E93\u56DE\u7B54\u95EE\u9898\uFF0C\u652F\u6301\u4E03\u5927\u53F8\u6CD5\u7BA1\u8F96\u533A\u7684\u5408\u89C4\u67E5\u8BE2", icon: "BookOpen", capabilities: ["\u5408\u89C4\u89C4\u5219\u67E5\u8BE2", "\u53F8\u6CD5\u7BA1\u8F96\u533A\u89E3\u8BFB", "\u4E0A\u94FE\u6D41\u7A0B\u6307\u5BFC"], suggestedQuestions: [ "\u4E2D\u56FD\u5927\u9646\u623F\u5730\u4EA7\u4E0A\u94FE\u9700\u8981\u54EA\u4E9B\u6587\u4EF6\uFF1F", "\u9999\u6E2FRWA\u5408\u89C4\u8981\u6C42\u662F\u4EC0\u4E48\uFF1F", "\u4E03\u5C42\u5408\u89C4\u9A8C\u8BC1\u6846\u67B6\u662F\u4EC0\u4E48\uFF1F" ] }, { type: "compliance", name: "\u5408\u89C4\u5206\u6790\u4E13\u5BB6", nameEn: "Compliance Analysis Agent", description: "\u57FA\u4E8E\u4E03\u5C42\u5408\u89C4\u9A8C\u8BC1\u6846\u67B6\u5206\u6790\u8D44\u4EA7\u4E0A\u94FE\u7533\u8BF7\u7684\u5408\u89C4\u98CE\u9669", icon: "Shield", capabilities: ["\u98CE\u9669\u8BC4\u4F30", "\u5408\u89C4\u8BC4\u5206", "\u7F3A\u5931\u6750\u6599\u8BC6\u522B", "\u6539\u8FDB\u5EFA\u8BAE"], suggestedQuestions: [ "\u5206\u6790\u8FD9\u4E2A\u623F\u4EA7\u4E0A\u94FE\u7533\u8BF7\u7684\u5408\u89C4\u98CE\u9669", "\u6211\u7684\u8D44\u4EA7\u7F3A\u5C11\u54EA\u4E9B\u5408\u89C4\u6587\u4EF6\uFF1F", "\u5982\u4F55\u63D0\u9AD8\u5408\u89C4\u8BC4\u5206\uFF1F" ] }, { type: "translation", name: "\u591A\u8BED\u8A00\u7FFB\u8BD1\u4E13\u5BB6", nameEn: "Translation Agent", description: "\u4E13\u4E1A\u6CD5\u5F8B\u5408\u89C4\u6587\u672C\u7FFB\u8BD1\uFF0C\u652F\u6301\u4E03\u79CD\u8BED\u8A00\uFF0C\u4FDD\u7559\u4E13\u6709\u540D\u8BCD", icon: "Languages", capabilities: ["\u4E03\u8BED\u8A00\u7FFB\u8BD1", "\u6CD5\u5F8B\u672F\u8BED\u51C6\u786E", "\u4E13\u6709\u540D\u8BCD\u4FDD\u7559", "\u963F\u62C9\u4F2F\u8BEDRTL"], suggestedQuestions: [ "\u5C06\u8FD9\u6BB5\u5408\u89C4\u89C4\u5219\u7FFB\u8BD1\u6210\u82F1\u6587", "\u7FFB\u8BD1\u6210\u963F\u62C9\u4F2F\u8BED", "\u751F\u6210\u4E03\u79CD\u8BED\u8A00\u7684\u7FFB\u8BD1" ] }, { type: "approval_assist", name: "\u5BA1\u6279\u8F85\u52A9\u52A9\u624B", nameEn: "Approval Assist Agent", description: "\u8F85\u52A9\u5BA1\u6838\u5458\u5206\u6790\u6848\u4F8B\u3001\u751F\u6210\u5BA1\u6279\u610F\u89C1\u3001\u8BC6\u522B\u9AD8\u98CE\u9669\u6848\u4F8B", icon: "ClipboardCheck", capabilities: ["\u5BA1\u6279\u5EFA\u8BAE", "\u98CE\u9669\u8BC6\u522B", "\u610F\u89C1\u6A21\u677F", "\u6848\u4F8B\u5206\u6790"], suggestedQuestions: [ "\u5206\u6790\u8FD9\u4E2A\u6848\u4F8B\u5E94\u8BE5\u6279\u51C6\u8FD8\u662F\u62D2\u7EDD\uFF1F", "\u751F\u6210\u6807\u51C6\u5BA1\u6279\u610F\u89C1", "\u8FD9\u4E2A\u6848\u4F8B\u6709\u54EA\u4E9B\u98CE\u9669\u70B9\uFF1F" ] } ]; // server/agentConversations.ts import { ObjectId } from "mongodb"; function generateId() { return new ObjectId().toHexString(); } function extractTitle(message) { return message.slice(0, 50).replace(/\n/g, " ").trim() + (message.length > 50 ? "..." : ""); } async function createConversation(userId, userEmail, agentType, firstMessage) { const db2 = await getMongoDb(); if (!db2) throw new Error("MongoDB\u4E0D\u53EF\u7528"); const conversationId = generateId(); const now = /* @__PURE__ */ new Date(); const conversation = { conversationId, userId, userEmail, agentType, title: extractTitle(firstMessage), messageCount: 0, createdAt: now, updatedAt: now }; await db2.collection(COLLECTIONS.AGENT_CONVERSATIONS).insertOne(conversation); return conversationId; } async function listConversations(userId, agentType, limit = 20, skip = 0) { const db2 = await getMongoDb(); if (!db2) return { conversations: [], total: 0 }; const filter = { userId }; if (agentType) filter.agentType = agentType; const [conversations, total] = await Promise.all([ db2.collection(COLLECTIONS.AGENT_CONVERSATIONS).find(filter).sort({ updatedAt: -1 }).skip(skip).limit(limit).toArray(), db2.collection(COLLECTIONS.AGENT_CONVERSATIONS).countDocuments(filter) ]); return { conversations, total }; } async function getConversation(conversationId, userId) { const db2 = await getMongoDb(); if (!db2) return null; return db2.collection(COLLECTIONS.AGENT_CONVERSATIONS).findOne({ conversationId, userId }); } async function deleteConversation(conversationId, userId) { const db2 = await getMongoDb(); if (!db2) return false; const result = await db2.collection(COLLECTIONS.AGENT_CONVERSATIONS).deleteOne({ conversationId, userId }); if (result.deletedCount > 0) { await db2.collection(COLLECTIONS.AGENT_MESSAGES).deleteMany({ conversationId }); return true; } return false; } async function saveMessagePair(conversationId, userMessage, assistantMessage, confidence, sources, suggestions) { const db2 = await getMongoDb(); if (!db2) return; const now = /* @__PURE__ */ new Date(); const userRecord = { messageId: generateId(), conversationId, role: "user", content: userMessage, createdAt: now }; const assistantRecord = { messageId: generateId(), conversationId, role: "assistant", content: assistantMessage, confidence, sources, suggestions, createdAt: new Date(now.getTime() + 1) // 确保顺序 }; await db2.collection(COLLECTIONS.AGENT_MESSAGES).insertMany([userRecord, assistantRecord]); await db2.collection(COLLECTIONS.AGENT_CONVERSATIONS).updateOne( { conversationId }, { $inc: { messageCount: 2 }, $set: { updatedAt: /* @__PURE__ */ new Date() } } ); } async function loadConversationMessages(conversationId, userId, limit = 20) { const db2 = await getMongoDb(); if (!db2) return []; const conv = await getConversation(conversationId, userId); if (!conv) return []; return db2.collection(COLLECTIONS.AGENT_MESSAGES).find({ conversationId }).sort({ createdAt: 1 }).limit(limit).toArray(); } function messagesToAgentHistory(messages) { return messages.map((m) => ({ role: m.role, content: m.content })); } // server/archiveApprovalCases.ts init_notification(); var ARCHIVE_COLLECTION = "approval_cases_archive"; var SOURCE_COLLECTION = "approval_cases"; var ARCHIVE_THRESHOLD_DAYS = 365; async function runArchive(dryRun = false) { const startTime = Date.now(); const executedAt = (/* @__PURE__ */ new Date()).toISOString(); const errors = []; let archivedCount = 0; let failedCount = 0; console.log(`[Archive] \u5F00\u59CB\u6848\u4F8B\u5F52\u6863\u4EFB\u52A1 ${dryRun ? "(\u8BD5\u8FD0\u884C)" : ""} - ${executedAt}`); const db2 = await getMongoDb(); if (!db2) { const error = "MongoDB\u8FDE\u63A5\u5931\u8D25\uFF0C\u5F52\u6863\u4EFB\u52A1\u4E2D\u6B62"; console.error(`[Archive] ${error}`); await notifyOwner({ title: "\u6848\u4F8B\u5E93\u5F52\u6863\u5931\u8D25", content: error, level: "critical", module: "archive" }); return { success: false, archivedCount: 0, failedCount: 0, totalEligible: 0, executedAt, durationMs: Date.now() - startTime, errors: [error] }; } const cutoffDate = /* @__PURE__ */ new Date(); cutoffDate.setDate(cutoffDate.getDate() - ARCHIVE_THRESHOLD_DAYS); const eligibleFilter = { status: { $in: ["approved", "rejected"] }, updatedAt: { $lt: cutoffDate } }; const totalEligible = await db2.collection(SOURCE_COLLECTION).countDocuments(eligibleFilter); console.log(`[Archive] \u7B26\u5408\u5F52\u6863\u6761\u4EF6\u7684\u6848\u4F8B\u6570\u91CF\uFF1A${totalEligible}\uFF08\u622A\u6B62\u65E5\u671F\uFF1A${cutoffDate.toISOString()}\uFF09`); if (totalEligible === 0) { console.log("[Archive] \u65E0\u9700\u5F52\u6863\u7684\u6848\u4F8B\uFF0C\u4EFB\u52A1\u5B8C\u6210"); return { success: true, archivedCount: 0, failedCount: 0, totalEligible: 0, executedAt, durationMs: Date.now() - startTime, errors: [] }; } if (dryRun) { console.log(`[Archive] \u8BD5\u8FD0\u884C\u6A21\u5F0F\uFF1A\u5C06\u5F52\u6863 ${totalEligible} \u4E2A\u6848\u4F8B\uFF08\u672A\u5B9E\u9645\u6267\u884C\uFF09`); return { success: true, archivedCount: 0, failedCount: 0, totalEligible, executedAt, durationMs: Date.now() - startTime, errors: [] }; } try { await db2.collection(ARCHIVE_COLLECTION).createIndex({ caseNumber: 1 }, { unique: true, sparse: true }); await db2.collection(ARCHIVE_COLLECTION).createIndex({ archivedAt: 1 }); await db2.collection(ARCHIVE_COLLECTION).createIndex({ status: 1 }); await db2.collection(ARCHIVE_COLLECTION).createIndex({ jurisdiction: 1 }); } catch (e) { } const BATCH_SIZE = 50; let processed = 0; while (processed < totalEligible) { const batch = await db2.collection(SOURCE_COLLECTION).find(eligibleFilter).skip(processed).limit(BATCH_SIZE).toArray(); if (batch.length === 0) break; for (const caseDoc of batch) { try { const archiveDoc = { ...caseDoc, archivedAt: /* @__PURE__ */ new Date(), archiveReason: `\u81EA\u52A8\u5F52\u6863\uFF1A\u6848\u4F8B\u5B8C\u7ED3\u8D85\u8FC7${ARCHIVE_THRESHOLD_DAYS}\u5929`, originalCollection: SOURCE_COLLECTION }; await db2.collection(ARCHIVE_COLLECTION).insertOne(archiveDoc); await db2.collection(SOURCE_COLLECTION).deleteOne({ _id: caseDoc._id }); archivedCount++; } catch (e) { const errMsg = `\u6848\u4F8B ${caseDoc.caseNumber || caseDoc._id} \u5F52\u6863\u5931\u8D25: ${e.message}`; console.error(`[Archive] ${errMsg}`); errors.push(errMsg); failedCount++; } } processed += batch.length; console.log(`[Archive] \u8FDB\u5EA6\uFF1A${processed}/${totalEligible}\uFF08\u5DF2\u5F52\u6863\uFF1A${archivedCount}\uFF0C\u5931\u8D25\uFF1A${failedCount}\uFF09`); } const durationMs = Date.now() - startTime; const success = failedCount === 0; try { await db2.collection("archive_logs").insertOne({ type: "approval_cases", executedAt: new Date(executedAt), totalEligible, archivedCount, failedCount, durationMs, errors, success }); } catch (e) { console.warn("[Archive] \u5F52\u6863\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", e.message); } if (!success || archivedCount > 0) { await notifyOwner({ title: success ? `\u6848\u4F8B\u5E93\u5F52\u6863\u5B8C\u6210\uFF1A\u5F52\u6863${archivedCount}\u4E2A\u6848\u4F8B` : `\u6848\u4F8B\u5E93\u5F52\u6863\u90E8\u5206\u5931\u8D25`, content: success ? `\u672C\u6B21\u5F52\u6863\u4EFB\u52A1\u6210\u529F\u5B8C\u6210\u3002 **\u5F52\u6863\u6570\u91CF\uFF1A** ${archivedCount} **\u8017\u65F6\uFF1A** ${(durationMs / 1e3).toFixed(1)}\u79D2 **\u622A\u6B62\u65E5\u671F\uFF1A** ${cutoffDate.toLocaleDateString("zh-CN")}` : `\u5F52\u6863\u4EFB\u52A1\u5B8C\u6210\u4F46\u6709${failedCount}\u4E2A\u6848\u4F8B\u5931\u8D25\u3002 **\u6210\u529F\uFF1A** ${archivedCount} **\u5931\u8D25\uFF1A** ${failedCount} **\u9519\u8BEF\uFF1A** ${errors.slice(0, 3).join("\n")}`, level: success ? "info" : "warning", module: "archive" }); } console.log(`[Archive] \u5F52\u6863\u4EFB\u52A1\u5B8C\u6210 - \u5F52\u6863\uFF1A${archivedCount}\uFF0C\u5931\u8D25\uFF1A${failedCount}\uFF0C\u8017\u65F6\uFF1A${durationMs}ms`); return { success, archivedCount, failedCount, totalEligible, executedAt, durationMs, errors }; } async function getArchiveLogs(limit = 20) { const db2 = await getMongoDb(); if (!db2) return []; const logs = await db2.collection("archive_logs").find({ type: "approval_cases" }).sort({ executedAt: -1 }).limit(limit).toArray(); return logs.map((log) => ({ executedAt: log.executedAt?.toISOString() || "", archivedCount: log.archivedCount || 0, failedCount: log.failedCount || 0, totalEligible: log.totalEligible || 0, durationMs: log.durationMs || 0, success: log.success !== false })); } async function getArchivedCases(page = 1, pageSize = 20, filter) { const db2 = await getMongoDb(); if (!db2) return { items: [], total: 0 }; const query = {}; if (filter?.jurisdiction) query.jurisdiction = filter.jurisdiction; if (filter?.status) query.status = filter.status; const skip = (page - 1) * pageSize; const [items, total] = await Promise.all([ db2.collection(ARCHIVE_COLLECTION).find(query).sort({ archivedAt: -1 }).skip(skip).limit(pageSize).toArray(), db2.collection(ARCHIVE_COLLECTION).countDocuments(query) ]); return { items, total }; } // server/initMongoIndexes.ts async function initMongoIndexes() { const db2 = await getMongoDb(); const results = []; if (!db2) { return { success: false, results: [], summary: "MongoDB\u8FDE\u63A5\u5931\u8D25\uFF0C\u65E0\u6CD5\u521D\u59CB\u5316\u7D22\u5F15" }; } try { const rulesCol = db2.collection(COLLECTIONS.COMPLIANCE_RULES); const existingIndexes = await rulesCol.listIndexes().toArray(); const hasTextIndex = existingIndexes.some( (idx) => idx.key && Object.values(idx.key).includes("text") ); if (hasTextIndex) { results.push({ collection: COLLECTIONS.COMPLIANCE_RULES, indexName: "compliance_rules_fulltext", status: "already_exists" }); } else { await rulesCol.createIndex( { ruleName: "text", description: "text", content: "text", "translations.zh": "text", "translations.en": "text" }, { name: "compliance_rules_fulltext", weights: { ruleName: 10, // 规则名称权重最高 description: 5, // 描述次之 content: 3, // 内容再次 "translations.zh": 3, "translations.en": 2 }, default_language: "none" // 禁用语言分词,支持中文 } ); results.push({ collection: COLLECTIONS.COMPLIANCE_RULES, indexName: "compliance_rules_fulltext", status: "created" }); } } catch (e) { results.push({ collection: COLLECTIONS.COMPLIANCE_RULES, indexName: "compliance_rules_fulltext", status: "failed", error: e.message }); } const ruleIndexes = [ { key: { jurisdiction: 1 }, name: "idx_rules_jurisdiction" }, { key: { category: 1 }, name: "idx_rules_category" }, { key: { ruleId: 1 }, name: "idx_rules_ruleId", unique: true }, { key: { jurisdiction: 1, category: 1 }, name: "idx_rules_jur_cat" }, { key: { createdAt: -1 }, name: "idx_rules_created_desc" } ]; for (const idx of ruleIndexes) { try { const rulesCol = db2.collection(COLLECTIONS.COMPLIANCE_RULES); await rulesCol.createIndex(idx.key, { name: idx.name, unique: idx.unique || false, background: true }); results.push({ collection: COLLECTIONS.COMPLIANCE_RULES, indexName: idx.name, status: "created" }); } catch (e) { const errMsg = e.message; if (errMsg.includes("already exists") || errMsg.includes("IndexOptionsConflict")) { results.push({ collection: COLLECTIONS.COMPLIANCE_RULES, indexName: idx.name, status: "already_exists" }); } else { results.push({ collection: COLLECTIONS.COMPLIANCE_RULES, indexName: idx.name, status: "failed", error: errMsg }); } } } try { const convCol = db2.collection(COLLECTIONS.AGENT_CONVERSATIONS); const existingIndexes = await convCol.listIndexes().toArray(); const hasTTLIndex = existingIndexes.some( (idx) => idx.expireAfterSeconds !== void 0 ); if (hasTTLIndex) { results.push({ collection: COLLECTIONS.AGENT_CONVERSATIONS, indexName: "agent_conversations_ttl", status: "already_exists" }); } else { await convCol.createIndex( { updatedAt: 1 }, { name: "agent_conversations_ttl", expireAfterSeconds: 7776e3, // 90天 = 90 * 24 * 3600 background: true } ); results.push({ collection: COLLECTIONS.AGENT_CONVERSATIONS, indexName: "agent_conversations_ttl", status: "created" }); } } catch (e) { results.push({ collection: COLLECTIONS.AGENT_CONVERSATIONS, indexName: "agent_conversations_ttl", status: "failed", error: e.message }); } const convIndexes = [ { key: { userId: 1, agentType: 1 }, name: "idx_conv_user_agent" }, { key: { userId: 1, updatedAt: -1 }, name: "idx_conv_user_updated" }, { key: { conversationId: 1 }, name: "idx_conv_id", unique: true } ]; for (const idx of convIndexes) { try { const convCol = db2.collection(COLLECTIONS.AGENT_CONVERSATIONS); await convCol.createIndex(idx.key, { name: idx.name, unique: idx.unique || false, background: true }); results.push({ collection: COLLECTIONS.AGENT_CONVERSATIONS, indexName: idx.name, status: "created" }); } catch (e) { const errMsg = e.message; if (errMsg.includes("already exists") || errMsg.includes("IndexOptionsConflict")) { results.push({ collection: COLLECTIONS.AGENT_CONVERSATIONS, indexName: idx.name, status: "already_exists" }); } else { results.push({ collection: COLLECTIONS.AGENT_CONVERSATIONS, indexName: idx.name, status: "failed", error: errMsg }); } } } try { const kbCol = db2.collection("knowledge_base"); await kbCol.createIndex( { title: "text", content: "text", tags: "text" }, { name: "knowledge_base_fulltext", weights: { title: 10, tags: 5, content: 3 }, default_language: "none", background: true } ); results.push({ collection: "knowledge_base", indexName: "knowledge_base_fulltext", status: "created" }); } catch (e) { const errMsg = e.message; if (errMsg.includes("already exists") || errMsg.includes("IndexOptionsConflict")) { results.push({ collection: "knowledge_base", indexName: "knowledge_base_fulltext", status: "already_exists" }); } else { results.push({ collection: "knowledge_base", indexName: "knowledge_base_fulltext", status: "failed", error: errMsg }); } } const created = results.filter((r) => r.status === "created").length; const existing = results.filter((r) => r.status === "already_exists").length; const failed = results.filter((r) => r.status === "failed").length; return { success: failed === 0, results, summary: `\u7D22\u5F15\u521D\u59CB\u5316\u5B8C\u6210\uFF1A\u65B0\u5EFA ${created} \u4E2A\uFF0C\u5DF2\u5B58\u5728 ${existing} \u4E2A\uFF0C\u5931\u8D25 ${failed} \u4E2A` }; } // server/routers.ts init_notification(); var nacAuthProcedure = publicProcedure.use(async ({ ctx, next }) => { const token = ctx.req.cookies?.["nac_admin_token"] || ctx.req.headers["x-nac-token"]; if (!token) throw new TRPCError3({ code: "UNAUTHORIZED", message: "\u8BF7\u5148\u767B\u5F55" }); const payload = verifyNacToken(token); if (!payload) throw new TRPCError3({ code: "UNAUTHORIZED", message: "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" }); return next({ ctx: { ...ctx, nacUser: payload } }); }); var nacAdminProcedure = nacAuthProcedure.use(async ({ ctx, next }) => { if (ctx.nacUser?.role !== "admin") { throw new TRPCError3({ code: "FORBIDDEN", message: "\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650" }); } return next({ ctx }); }); async function writeAuditLog(action, userId, email, detail) { try { const db2 = await getMongoDb(); if (!db2) return; await db2.collection(COLLECTIONS.AUDIT_LOGS).insertOne({ action, userId, email, detail, timestamp: /* @__PURE__ */ new Date(), immutable: true }); } catch (e) { console.error("[AuditLog] Failed:", e.message); } } async function ensureKnowledgeBaseData() { const db2 = await getMongoDb(); if (!db2) return; const protocolCount = await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments(); if (protocolCount === 0) { await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).insertMany([ { name: "nac-charter-compiler", type: "contract_validation", version: "1.0.0", endpoint: "charter.newassetchain.io", trigger: "asset_type in ALL", status: "active", createdAt: /* @__PURE__ */ new Date() }, { name: "nac-cnnl-validator", type: "constitutional_check", version: "1.0.0", endpoint: "cnnl.newassetchain.io", trigger: "asset_type in ALL", status: "active", createdAt: /* @__PURE__ */ new Date() }, { name: "nac-acc20-engine", type: "compliance_approval", version: "1.0.0", endpoint: "acc20.newassetchain.io", trigger: "asset_type in ALL", status: "active", createdAt: /* @__PURE__ */ new Date() }, { name: "nac-gnacs-classifier", type: "asset_classification", version: "1.0.0", endpoint: "gnacs.newassetchain.io", trigger: "asset_type in ALL", status: "active", createdAt: /* @__PURE__ */ new Date() }, { name: "nac-valuation-ai", type: "valuation_model", version: "0.9.0", endpoint: "valuation.newassetchain.io", trigger: "asset_type is RealEstate", status: "pending", createdAt: /* @__PURE__ */ new Date() } ]); } const crawlerCount = await db2.collection(COLLECTIONS.CRAWLERS).countDocuments(); if (crawlerCount === 0) { await db2.collection(COLLECTIONS.CRAWLERS).insertMany([ { name: "CN-CSRC\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "CN", type: "external", source: "http://www.csrc.gov.cn", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }, { name: "HK-SFC\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "HK", type: "external", source: "https://www.sfc.hk", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }, { name: "US-SEC\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "US", type: "external", source: "https://www.sec.gov", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }, { name: "EU-ESMA\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "EU", type: "external", source: "https://www.esma.europa.eu", category: "regulation", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }, { name: "SG-MAS\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "SG", type: "external", source: "https://www.mas.gov.sg", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }, { name: "AE-DFSA\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "AE", type: "external", source: "https://www.dfsa.ae", category: "regulation", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }, { name: "CN-\u88C1\u5224\u6587\u4E66\u7F51\u91C7\u96C6\u5668", jurisdiction: "CN", type: "external", source: "https://wenshu.court.gov.cn", category: "credit", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }, { name: "\u5185\u90E8\u4E0A\u94FE\u6587\u4EF6\u91C7\u96C6\u5668", jurisdiction: "ALL", type: "internal", source: "internal://onboarding", category: "asset_document", frequency: "realtime", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() } ]); } const ruleCount = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(); if (ruleCount === 0) { await db2.collection(COLLECTIONS.COMPLIANCE_RULES).insertMany([ { jurisdiction: "CN", assetType: "RealEstate", ruleName: "\u4E0D\u52A8\u4EA7\u767B\u8BB0\u8BC1\u8981\u6C42", description: "\u4E2D\u56FD\u5883\u5185\u623F\u5730\u4EA7\u4E0A\u94FE\u5FC5\u987B\u63D0\u4F9B\u4E0D\u52A8\u4EA7\u767B\u8BB0\u8BC1", ruleNameI18n: { zh: "\u4E0D\u52A8\u4EA7\u767B\u8BB0\u8BC1\u8981\u6C42", en: "Real Estate Registration Certificate Requirement", ar: "\u0645\u062A\u0637\u0644\u0628\u0627\u062A \u0634\u0647\u0627\u062F\u0629 \u062A\u0633\u062C\u064A\u0644 \u0627\u0644\u0639\u0642\u0627\u0631\u0627\u062A", ja: "\u4E0D\u52D5\u7523\u767B\u8A18\u8A3C\u8981\u4EF6", ko: "\uBD80\uB3D9\uC0B0 \uB4F1\uAE30\uC99D \uC694\uAC74", fr: "Exigence de certificat d'enregistrement immobilier", ru: "\u0422\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u0443 \u043E \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043D\u0435\u0434\u0432\u0438\u0436\u0438\u043C\u043E\u0441\u0442\u0438" }, descriptionI18n: { zh: "\u4E2D\u56FD\u5883\u5185\u623F\u5730\u4EA7\u4E0A\u94FE\u5FC5\u987B\u63D0\u4F9B\u4E0D\u52A8\u4EA7\u767B\u8BB0\u8BC1", en: "Real estate assets on-chain in China must provide a real estate registration certificate", ar: "\u064A\u062C\u0628 \u0639\u0644\u0649 \u0627\u0644\u0623\u0635\u0648\u0644 \u0627\u0644\u0639\u0642\u0627\u0631\u064A\u0629 \u0627\u0644\u0645\u0633\u062C\u0644\u0629 \u0639\u0644\u0649 \u0627\u0644\u0633\u0644\u0633\u0644\u0629 \u0641\u064A \u0627\u0644\u0635\u064A\u0646 \u062A\u0642\u062F\u064A\u0645 \u0634\u0647\u0627\u062F\u0629 \u062A\u0633\u062C\u064A\u0644 \u0627\u0644\u0639\u0642\u0627\u0631\u0627\u062A", ja: "\u4E2D\u56FD\u56FD\u5185\u306E\u4E0D\u52D5\u7523\u30C1\u30A7\u30FC\u30F3\u767B\u9332\u306B\u306F\u4E0D\u52D5\u7523\u767B\u8A18\u8A3C\u306E\u63D0\u51FA\u304C\u5FC5\u8981", ko: "\uC911\uAD6D \uB0B4 \uBD80\uB3D9\uC0B0 \uC628\uCCB4\uC778 \uB4F1\uB85D \uC2DC \uBD80\uB3D9\uC0B0 \uB4F1\uAE30\uC99D \uC81C\uCD9C \uD544\uC218", fr: "Les actifs immobiliers enregistr\xE9s sur la cha\xEEne en Chine doivent fournir un certificat d'enregistrement immobilier", ru: "\u041D\u0435\u0434\u0432\u0438\u0436\u0438\u043C\u043E\u0441\u0442\u044C, \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043C\u0430\u044F \u0432 \u0431\u043B\u043E\u043A\u0447\u0435\u0439\u043D\u0435 \u0432 \u041A\u0438\u0442\u0430\u0435, \u0434\u043E\u043B\u0436\u043D\u0430 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E \u043E \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043D\u0435\u0434\u0432\u0438\u0436\u0438\u043C\u043E\u0441\u0442\u0438" }, required: true, status: "active", tags: ["CN", "RealEstate", "Document", "Required"], createdAt: /* @__PURE__ */ new Date() }, { jurisdiction: "HK", assetType: "Securities", ruleName: "SFC\u6301\u724C\u8981\u6C42", description: "\u9999\u6E2F\u8BC1\u5238\u7C7B\u8D44\u4EA7\u4E0A\u94FE\u987B\u7ECFSFC\u6301\u724C\u673A\u6784\u5BA1\u6838", ruleNameI18n: { zh: "SFC\u6301\u724C\u8981\u6C42", en: "SFC Licensing Requirement", ar: "\u0645\u062A\u0637\u0644\u0628\u0627\u062A \u062A\u0631\u062E\u064A\u0635 SFC", ja: "SFC\u30E9\u30A4\u30BB\u30F3\u30B9\u8981\u4EF6", ko: "SFC \uB77C\uC774\uC120\uC2A4 \uC694\uAC74", fr: "Exigence de licence SFC", ru: "\u0422\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u0438 SFC" }, descriptionI18n: { zh: "\u9999\u6E2F\u8BC1\u5238\u7C7B\u8D44\u4EA7\u4E0A\u94FE\u987B\u7ECFSFC\u6301\u724C\u673A\u6784\u5BA1\u6838", en: "Securities assets on-chain in Hong Kong must be reviewed by SFC-licensed institutions", ar: "\u064A\u062C\u0628 \u0645\u0631\u0627\u062C\u0639\u0629 \u0623\u0635\u0648\u0644 \u0627\u0644\u0623\u0648\u0631\u0627\u0642 \u0627\u0644\u0645\u0627\u0644\u064A\u0629 \u0627\u0644\u0645\u0633\u062C\u0644\u0629 \u0639\u0644\u0649 \u0627\u0644\u0633\u0644\u0633\u0644\u0629 \u0641\u064A \u0647\u0648\u0646\u063A \u0643\u0648\u0646\u063A \u0645\u0646 \u0642\u0628\u0644 \u0645\u0624\u0633\u0633\u0627\u062A \u0645\u0631\u062E\u0635\u0629 \u0645\u0646 SFC", ja: "\u9999\u6E2F\u306E\u8A3C\u5238\u8CC7\u7523\u306E\u30C1\u30A7\u30FC\u30F3\u767B\u9332\u306FSFC\u30E9\u30A4\u30BB\u30F3\u30B9\u6A5F\u95A2\u306E\u5BE9\u67FB\u304C\u5FC5\u8981", ko: "\uD64D\uCF69 \uC99D\uAD8C \uC790\uC0B0 \uC628\uCCB4\uC778 \uB4F1\uB85D \uC2DC SFC \uC778\uAC00 \uAE30\uAD00\uC758 \uC2EC\uC0AC \uD544\uC694", fr: "Les actifs en valeurs mobili\xE8res enregistr\xE9s sur la cha\xEEne \xE0 Hong Kong doivent \xEAtre examin\xE9s par des institutions agr\xE9\xE9es SFC", ru: "\u0426\u0435\u043D\u043D\u044B\u0435 \u0431\u0443\u043C\u0430\u0433\u0438, \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043C\u044B\u0435 \u0432 \u0431\u043B\u043E\u043A\u0447\u0435\u0439\u043D\u0435 \u0432 \u0413\u043E\u043D\u043A\u043E\u043D\u0433\u0435, \u0434\u043E\u043B\u0436\u043D\u044B \u043F\u0440\u043E\u0439\u0442\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443 \u0443\u0447\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F\u043C\u0438 \u0441 \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u0435\u0439 SFC" }, required: true, status: "active", tags: ["HK", "Securities", "License", "SFC"], createdAt: /* @__PURE__ */ new Date() }, { jurisdiction: "US", assetType: "Securities", ruleName: "Reg D\u8C41\u514D\u7533\u62A5", description: "\u7F8E\u56FD\u8BC1\u5238\u7C7B\u8D44\u4EA7\u987B\u6EE1\u8DB3Reg D/S\u8C41\u514D\u6761\u4EF6", ruleNameI18n: { zh: "Reg D\u8C41\u514D\u7533\u62A5", en: "Reg D Exemption Filing", ar: "\u062A\u0642\u062F\u064A\u0645 \u0625\u0639\u0641\u0627\u0621 Reg D", ja: "Reg D\u514D\u9664\u7533\u544A", ko: "Reg D \uBA74\uC81C \uC2E0\uACE0", fr: "D\xE9claration d'exemption Reg D", ru: "\u041F\u043E\u0434\u0430\u0447\u0430 \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u0438\u044F \u043E\u0431 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u0438\u0438 \u043F\u043E Reg D" }, descriptionI18n: { zh: "\u7F8E\u56FD\u8BC1\u5238\u7C7B\u8D44\u4EA7\u987B\u6EE1\u8DB3Reg D/S\u8C41\u514D\u6761\u4EF6", en: "US securities assets must meet Reg D/S exemption conditions", ar: "\u064A\u062C\u0628 \u0623\u0646 \u062A\u0633\u062A\u0648\u0641\u064A \u0623\u0635\u0648\u0644 \u0627\u0644\u0623\u0648\u0631\u0627\u0642 \u0627\u0644\u0645\u0627\u0644\u064A\u0629 \u0627\u0644\u0623\u0645\u0631\u064A\u0643\u064A\u0629 \u0634\u0631\u0648\u0637 \u0625\u0639\u0641\u0627\u0621 Reg D/S", ja: "\u7C73\u56FD\u8A3C\u5238\u8CC7\u7523\u306FReg D/S\u514D\u9664\u6761\u4EF6\u3092\u6E80\u305F\u3059\u5FC5\u8981\u304C\u3042\u308B", ko: "\uBBF8\uAD6D \uC99D\uAD8C \uC790\uC0B0\uC740 Reg D/S \uBA74\uC81C \uC870\uAC74\uC744 \uCDA9\uC871\uD574\uC57C \uD568", fr: "Les actifs en valeurs mobili\xE8res am\xE9ricains doivent satisfaire aux conditions d'exemption Reg D/S", ru: "\u0426\u0435\u043D\u043D\u044B\u0435 \u0431\u0443\u043C\u0430\u0433\u0438 \u0421\u0428\u0410 \u0434\u043E\u043B\u0436\u043D\u044B \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C \u0443\u0441\u043B\u043E\u0432\u0438\u044F\u043C \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u0438\u044F \u043F\u043E Reg D/S" }, required: true, status: "active", tags: ["US", "Securities", "RegD", "SEC"], createdAt: /* @__PURE__ */ new Date() }, { jurisdiction: "EU", assetType: "ALL", ruleName: "MiCA\u5408\u89C4\u8981\u6C42", description: "\u6B27\u76DF\u5883\u5185\u6240\u6709\u52A0\u5BC6\u8D44\u4EA7\u987B\u7B26\u5408MiCA\u6CD5\u89C4", ruleNameI18n: { zh: "MiCA\u5408\u89C4\u8981\u6C42", en: "MiCA Compliance Requirement", ar: "\u0645\u062A\u0637\u0644\u0628\u0627\u062A \u0627\u0644\u0627\u0645\u062A\u062B\u0627\u0644 MiCA", ja: "MiCA\u30B3\u30F3\u30D7\u30E9\u30A4\u30A2\u30F3\u30B9\u8981\u4EF6", ko: "MiCA \uC900\uC218 \uC694\uAC74", fr: "Exigence de conformit\xE9 MiCA", ru: "\u0422\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u044F MiCA" }, descriptionI18n: { zh: "\u6B27\u76DF\u5883\u5185\u6240\u6709\u52A0\u5BC6\u8D44\u4EA7\u987B\u7B26\u5408MiCA\u6CD5\u89C4", en: "All crypto assets within the EU must comply with MiCA regulations", ar: "\u064A\u062C\u0628 \u0623\u0646 \u062A\u0645\u062A\u062B\u0644 \u062C\u0645\u064A\u0639 \u0627\u0644\u0623\u0635\u0648\u0644 \u0627\u0644\u0645\u0634\u0641\u0631\u0629 \u062F\u0627\u062E\u0644 \u0627\u0644\u0627\u062A\u062D\u0627\u062F \u0627\u0644\u0623\u0648\u0631\u0648\u0628\u064A \u0644\u0644\u0648\u0627\u0626\u062D MiCA", ja: "EU\u57DF\u5185\u306E\u3059\u3079\u3066\u306E\u6697\u53F7\u8CC7\u7523\u306FMiCA\u898F\u5236\u306B\u6E96\u62E0\u3059\u308B\u5FC5\u8981\u304C\u3042\u308B", ko: "EU \uB0B4 \uBAA8\uB4E0 \uC554\uD638\uD654 \uC790\uC0B0\uC740 MiCA \uADDC\uC815\uC744 \uC900\uC218\uD574\uC57C \uD568", fr: "Tous les crypto-actifs au sein de l'UE doivent se conformer aux r\xE9glementations MiCA", ru: "\u0412\u0441\u0435 \u043A\u0440\u0438\u043F\u0442\u043E\u0430\u043A\u0442\u0438\u0432\u044B \u0432 \u0415\u0421 \u0434\u043E\u043B\u0436\u043D\u044B \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C \u0440\u0435\u0433\u043B\u0430\u043C\u0435\u043D\u0442\u0443 MiCA" }, required: true, status: "active", tags: ["EU", "ALL", "MiCA", "ESMA"], createdAt: /* @__PURE__ */ new Date() }, { jurisdiction: "SG", assetType: "DigitalToken", ruleName: "MAS\u6570\u5B57\u4EE3\u5E01\u670D\u52A1\u724C\u7167", description: "\u65B0\u52A0\u5761\u6570\u5B57\u4EE3\u5E01\u670D\u52A1\u987B\u6301MAS\u724C\u7167", ruleNameI18n: { zh: "MAS\u6570\u5B57\u4EE3\u5E01\u670D\u52A1\u724C\u7167", en: "MAS Digital Token Service License", ar: "\u062A\u0631\u062E\u064A\u0635 \u062E\u062F\u0645\u0629 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0631\u0642\u0645\u064A MAS", ja: "MAS\u30C7\u30B8\u30BF\u30EB\u30C8\u30FC\u30AF\u30F3\u30B5\u30FC\u30D3\u30B9\u30E9\u30A4\u30BB\u30F3\u30B9", ko: "MAS \uB514\uC9C0\uD138 \uD1A0\uD070 \uC11C\uBE44\uC2A4 \uB77C\uC774\uC120\uC2A4", fr: "Licence de service de jetons num\xE9riques MAS", ru: "\u041B\u0438\u0446\u0435\u043D\u0437\u0438\u044F \u043D\u0430 \u0443\u0441\u043B\u0443\u0433\u0438 \u0446\u0438\u0444\u0440\u043E\u0432\u044B\u0445 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 MAS" }, descriptionI18n: { zh: "\u65B0\u52A0\u5761\u6570\u5B57\u4EE3\u5E01\u670D\u52A1\u987B\u6301MAS\u724C\u7167", en: "Digital token services in Singapore must hold a MAS license", ar: "\u064A\u062C\u0628 \u0623\u0646 \u062A\u062D\u0645\u0644 \u062E\u062F\u0645\u0627\u062A \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0631\u0642\u0645\u064A \u0641\u064A \u0633\u0646\u063A\u0627\u0641\u0648\u0631\u0629 \u062A\u0631\u062E\u064A\u0635 MAS", ja: "\u30B7\u30F3\u30AC\u30DD\u30FC\u30EB\u306E\u30C7\u30B8\u30BF\u30EB\u30C8\u30FC\u30AF\u30F3\u30B5\u30FC\u30D3\u30B9\u306FMAS\u30E9\u30A4\u30BB\u30F3\u30B9\u304C\u5FC5\u8981", ko: "\uC2F1\uAC00\uD3EC\uB974 \uB514\uC9C0\uD138 \uD1A0\uD070 \uC11C\uBE44\uC2A4\uB294 MAS \uB77C\uC774\uC120\uC2A4 \uBCF4\uC720 \uD544\uC694", fr: "Les services de jetons num\xE9riques \xE0 Singapour doivent d\xE9tenir une licence MAS", ru: "\u0423\u0441\u043B\u0443\u0433\u0438 \u0446\u0438\u0444\u0440\u043E\u0432\u044B\u0445 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0432 \u0421\u0438\u043D\u0433\u0430\u043F\u0443\u0440\u0435 \u0434\u043E\u043B\u0436\u043D\u044B \u0438\u043C\u0435\u0442\u044C \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u044E MAS" }, required: true, status: "active", tags: ["SG", "DigitalToken", "MAS", "License"], createdAt: /* @__PURE__ */ new Date() }, { jurisdiction: "AE", assetType: "RealEstate", ruleName: "DLD\u4EA7\u6743\u8BC1\u4E66\u8981\u6C42", description: "\u8FEA\u62DC\u623F\u5730\u4EA7\u4E0A\u94FE\u987B\u63D0\u4F9BDLD\u9881\u53D1\u7684\u4EA7\u6743\u8BC1\u4E66", ruleNameI18n: { zh: "DLD\u4EA7\u6743\u8BC1\u4E66\u8981\u6C42", en: "DLD Title Deed Requirement", ar: "\u0645\u062A\u0637\u0644\u0628\u0627\u062A \u0633\u0646\u062F \u0627\u0644\u0645\u0644\u0643\u064A\u0629 DLD", ja: "DLD\u6240\u6709\u6A29\u8A3C\u66F8\u8981\u4EF6", ko: "DLD \uC18C\uC720\uAD8C \uC99D\uC11C \uC694\uAC74", fr: "Exigence de titre de propri\xE9t\xE9 DLD", ru: "\u0422\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u0443 \u043E \u043F\u0440\u0430\u0432\u0435 \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0441\u0442\u0438 DLD" }, descriptionI18n: { zh: "\u8FEA\u62DC\u623F\u5730\u4EA7\u4E0A\u94FE\u987B\u63D0\u4F9BDLD\u9881\u53D1\u7684\u4EA7\u6743\u8BC1\u4E66", en: "Dubai real estate on-chain must provide a title deed issued by DLD", ar: "\u064A\u062C\u0628 \u0623\u0646 \u062A\u0648\u0641\u0631 \u0627\u0644\u0639\u0642\u0627\u0631\u0627\u062A \u0627\u0644\u0645\u0633\u062C\u0644\u0629 \u0639\u0644\u0649 \u0627\u0644\u0633\u0644\u0633\u0644\u0629 \u0641\u064A \u062F\u0628\u064A \u0633\u0646\u062F \u0645\u0644\u0643\u064A\u0629 \u0635\u0627\u062F\u0631 \u0639\u0646 DLD", ja: "\u30C9\u30D0\u30A4\u306E\u4E0D\u52D5\u7523\u30C1\u30A7\u30FC\u30F3\u767B\u9332\u306B\u306FDLD\u767A\u884C\u306E\u6240\u6709\u6A29\u8A3C\u66F8\u304C\u5FC5\u8981", ko: "\uB450\uBC14\uC774 \uBD80\uB3D9\uC0B0 \uC628\uCCB4\uC778 \uB4F1\uB85D \uC2DC DLD \uBC1C\uAE09 \uC18C\uC720\uAD8C \uC99D\uC11C \uC81C\uCD9C \uD544\uC218", fr: "L'immobilier de Duba\xEF enregistr\xE9 sur la cha\xEEne doit fournir un titre de propri\xE9t\xE9 d\xE9livr\xE9 par DLD", ru: "\u041D\u0435\u0434\u0432\u0438\u0436\u0438\u043C\u043E\u0441\u0442\u044C \u0414\u0443\u0431\u0430\u044F, \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043C\u0430\u044F \u0432 \u0431\u043B\u043E\u043A\u0447\u0435\u0439\u043D\u0435, \u0434\u043E\u043B\u0436\u043D\u0430 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E \u043E \u043F\u0440\u0430\u0432\u0435 \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0441\u0442\u0438, \u0432\u044B\u0434\u0430\u043D\u043D\u043E\u0435 DLD" }, required: true, status: "active", tags: ["AE", "RealEstate", "DLD", "Document"], createdAt: /* @__PURE__ */ new Date() } ]); } } var appRouter = router({ system: systemRouter, // ─── NAC原生认证(不使用NAC_AI OAuth)──────────────────────────── nacAuth: router({ login: publicProcedure.input(z2.object({ email: z2.string().email(), password: z2.string().min(1) })).mutation(async ({ input, ctx }) => { try { const result = await loginWithNacCredentials(input.email, input.password); if (!result) throw new TRPCError3({ code: "UNAUTHORIZED", message: "\u90AE\u7BB1\u6216\u5BC6\u7801\u9519\u8BEF" }); const cookieOptions = getSessionCookieOptions(ctx.req); ctx.res.cookie("nac_admin_token", result.token, { ...cookieOptions, maxAge: 24 * 60 * 60 * 1e3 }); await writeAuditLog("LOGIN", result.user.id, result.user.email, { ip: ctx.req.ip }); return { success: true, user: { id: result.user.id, name: result.user.name, email: result.user.email, role: result.user.role, kyc_level: result.user.kyc_level } }; } catch (e) { if (e instanceof TRPCError3) throw e; throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR", message: "\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528" }); } }), logout: publicProcedure.mutation(({ ctx }) => { ctx.res.clearCookie("nac_admin_token"); return { success: true }; }), me: nacAuthProcedure.query(async ({ ctx }) => { const nacUser = ctx.nacUser; return { id: nacUser.id, email: nacUser.email, role: nacUser.role }; }) }), // ─── 全局态势感知仪表盘 ────────────────────────────────────────── dashboard: router({ stats: nacAuthProcedure.query(async () => { const db2 = await getMongoDb(); if (!db2) return { error: "\u6570\u636E\u5E93\u8FDE\u63A5\u5931\u8D25" }; await ensureKnowledgeBaseData(); const [ruleCount, crawlerCount, caseCount, protocolCount, userCount, auditCount] = await Promise.all([ db2.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(), db2.collection(COLLECTIONS.CRAWLERS).countDocuments(), db2.collection(COLLECTIONS.APPROVAL_CASES).countDocuments(), db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments(), getNacUserCount(), db2.collection(COLLECTIONS.AUDIT_LOGS).countDocuments() ]); const [activeCrawlers, pendingCases, approvedCases, activeProtocols] = await Promise.all([ db2.collection(COLLECTIONS.CRAWLERS).countDocuments({ status: "active" }), db2.collection(COLLECTIONS.APPROVAL_CASES).countDocuments({ status: "pending_review" }), db2.collection(COLLECTIONS.APPROVAL_CASES).countDocuments({ decision: "approved" }), db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments({ status: "active" }) ]); const jurisdictionStats = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([ { $group: { _id: "$jurisdiction", count: { $sum: 1 } } }, { $sort: { count: -1 } } ]).toArray(); return { knowledgeBase: { totalRules: ruleCount, activeProtocols, totalProtocols: protocolCount }, crawlers: { total: crawlerCount, active: activeCrawlers }, approvals: { total: caseCount, pending: pendingCases, approved: approvedCases, approvalRate: caseCount > 0 ? Math.round(approvedCases / caseCount * 100) : 0 }, users: { total: userCount }, audit: { total: auditCount }, jurisdictionCoverage: jurisdictionStats, systemStatus: { mongodb: "connected", mysql: "connected", timestamp: /* @__PURE__ */ new Date() } }; }), recentActivity: nacAuthProcedure.query(async () => { const db2 = await getMongoDb(); if (!db2) return []; return db2.collection(COLLECTIONS.AUDIT_LOGS).find({}).sort({ timestamp: -1 }).limit(20).toArray(); }) }), // ─── 知识库管理(含多语言支持)────────────────────────────────── knowledgeBase: router({ list: nacAuthProcedure.input(z2.object({ jurisdiction: z2.string().optional(), assetType: z2.string().optional(), status: z2.string().optional(), search: z2.string().optional(), // 全文搜索关键词(支持RAG来源引用跳转) page: z2.number().default(1), pageSize: z2.number().default(20), lang: z2.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).optional() })).query(async ({ input }) => { const db2 = await getMongoDb(); if (!db2) return { items: [], total: 0 }; const filter = {}; if (input.jurisdiction) filter.jurisdiction = input.jurisdiction; if (input.assetType) filter.assetType = input.assetType; if (input.status) filter.status = input.status; if (input.search) { const kw = input.search.trim(); try { filter["$text"] = { $search: kw }; } catch { delete filter["$text"]; const re = new RegExp(kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"); filter["$or"] = [ { ruleName: re }, { description: re }, { "ruleNameI18n.zh": re }, { "ruleNameI18n.en": re } ]; } } const skip = (input.page - 1) * input.pageSize; const sortOpt = input.search ? { score: -1, createdAt: -1 } : { createdAt: -1 }; const projection = input.search ? { score: { $meta: "textScore" } } : {}; const [items, total] = await Promise.all([ db2.collection(COLLECTIONS.COMPLIANCE_RULES).find(filter, { projection }).sort(sortOpt).skip(skip).limit(input.pageSize).toArray(), db2.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(filter) ]); const lang = input.lang || "zh"; const localizedItems = items.map((item) => ({ ...item, displayName: item.ruleNameI18n?.[lang] || item.ruleName, displayDescription: item.descriptionI18n?.[lang] || item.description })); return { items: localizedItems, total }; }), create: nacAdminProcedure.input(z2.object({ jurisdiction: z2.string(), assetType: z2.string(), ruleName: z2.string(), description: z2.string(), required: z2.boolean(), tags: z2.array(z2.string()), sourceLang: z2.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).optional(), autoTranslate: z2.boolean().optional() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); let ruleNameI18n = {}; let descriptionI18n = {}; if (input.autoTranslate !== false) { try { const translations = await generateRuleTranslations( input.ruleName, input.description, input.sourceLang || "zh" ); ruleNameI18n = translations.ruleNameI18n; descriptionI18n = translations.descriptionI18n; } catch (e) { console.error("[KnowledgeBase] Auto-translate failed:", e.message); const lang = input.sourceLang || "zh"; ruleNameI18n[lang] = input.ruleName; descriptionI18n[lang] = input.description; } } else { const lang = input.sourceLang || "zh"; ruleNameI18n[lang] = input.ruleName; descriptionI18n[lang] = input.description; } const result = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).insertOne({ jurisdiction: input.jurisdiction, assetType: input.assetType, ruleName: input.ruleName, description: input.description, ruleNameI18n, descriptionI18n, required: input.required, tags: input.tags, status: "active", createdAt: /* @__PURE__ */ new Date() }); await writeAuditLog("CREATE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: result.insertedId }); return { id: result.insertedId }; }), update: nacAdminProcedure.input(z2.object({ id: z2.string(), data: z2.object({ ruleName: z2.string().optional(), description: z2.string().optional(), status: z2.enum(["active", "disabled"]).optional(), tags: z2.array(z2.string()).optional() }), autoTranslate: z2.boolean().optional() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const updateData = { ...input.data, updatedAt: /* @__PURE__ */ new Date() }; if (input.autoTranslate !== false && (input.data.ruleName || input.data.description)) { const existing = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ _id: new ObjectId2(input.id) }); if (existing) { try { const translations = await generateRuleTranslations( input.data.ruleName || existing.ruleName, input.data.description || existing.description, "zh", { ruleNameI18n: existing.ruleNameI18n, descriptionI18n: existing.descriptionI18n } ); updateData.ruleNameI18n = translations.ruleNameI18n; updateData.descriptionI18n = translations.descriptionI18n; } catch (e) { console.error("[KnowledgeBase] Auto-translate on update failed:", e.message); } } } await db2.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne( { _id: new ObjectId2(input.id) }, { $set: updateData } ); await writeAuditLog("UPDATE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: input.id }); return { success: true }; }), toggleStatus: nacAdminProcedure.input(z2.object({ id: z2.string(), status: z2.enum(["active", "disabled"]) })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); await db2.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne({ _id: new ObjectId2(input.id) }, { $set: { status: input.status, updatedAt: /* @__PURE__ */ new Date() } }); await writeAuditLog("TOGGLE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: input.id, newStatus: input.status }); return { success: true }; }), delete: nacAdminProcedure.input(z2.object({ id: z2.string() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); await db2.collection(COLLECTIONS.COMPLIANCE_RULES).deleteOne({ _id: new ObjectId2(input.id) }); await writeAuditLog("DELETE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: input.id }); return { success: true }; }), // ─── AI辅助翻译接口 ────────────────────────────────────────── translateRule: nacAdminProcedure.input(z2.object({ id: z2.string(), targetLang: z2.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).optional() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const rule = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ _id: new ObjectId2(input.id) }); if (!rule) throw new TRPCError3({ code: "NOT_FOUND", message: "\u89C4\u5219\u4E0D\u5B58\u5728" }); const translations = await migrateRuleToMultiLang({ ruleName: rule.ruleName, description: rule.description, ruleNameI18n: rule.ruleNameI18n, descriptionI18n: rule.descriptionI18n }); await db2.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne( { _id: new ObjectId2(input.id) }, { $set: { ruleNameI18n: translations.ruleNameI18n, descriptionI18n: translations.descriptionI18n, updatedAt: /* @__PURE__ */ new Date() } } ); await writeAuditLog("TRANSLATE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: input.id }); return { success: true, translations }; }), // ─── 批量迁移现有规则到多语言格式 ──────────────────────────── migrateAllToMultiLang: nacAdminProcedure.mutation(async ({ ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const rules = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).find({ $or: [{ ruleNameI18n: { $exists: false } }, { "ruleNameI18n.en": { $exists: false } }] }).toArray(); let migrated = 0; for (const rule of rules) { try { const translations = await migrateRuleToMultiLang({ ruleName: rule.ruleName, description: rule.description, ruleNameI18n: rule.ruleNameI18n, descriptionI18n: rule.descriptionI18n }); await db2.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne( { _id: rule._id }, { $set: { ruleNameI18n: translations.ruleNameI18n, descriptionI18n: translations.descriptionI18n, updatedAt: /* @__PURE__ */ new Date() } } ); migrated++; } catch (e) { console.error(`[Migration] Failed for rule ${rule._id}:`, e.message); } } await writeAuditLog("MIGRATE_MULTILANG", ctx.nacUser.id, ctx.nacUser.email, { migratedCount: migrated }); return { success: true, migratedCount: migrated, totalRules: rules.length }; }), // // ─── 获取支持的语言列表 ────────────────────────────── getSupportedLanguages: nacAuthProcedure.query(() => { return SUPPORTED_LANGUAGES.map((lang) => ({ code: lang, name: { zh: "\u4E2D\u6587\uFF08\u7B80\u4F53\uFF09", en: "English", ar: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", ja: "\u65E5\u672C\u8A9E", ko: "\uD55C\uAD6D\uC5B4", fr: "Fran\xE7ais", ru: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" }[lang], isRTL: isRTL(lang) })); }), // ─── AI翻译服务状态检查 ─────────────────────────── aiStatus: nacAuthProcedure.query(() => { const configured = isAiTranslationConfigured(); return { configured, apiUrl: configured ? (process.env.NAC_AI_API_URL || "").replace(/\/+$/, "") : null, model: process.env.NAC_AI_MODEL || "gpt-3.5-turbo", message: configured ? "AI\u7FFB\u8BD1\u670D\u52A1\u5DF2\u914D\u7F6E\uFF0C\u53EF\u4EE5\u4F7F\u7528\u81EA\u52A8\u7FFB\u8BD1\u529F\u80FD" : "AI\u7FFB\u8BD1\u670D\u52A1\u672A\u914D\u7F6E\u3002\u8BF7\u5728\u670D\u52A1\u5668 .env \u4E2D\u8BBE\u7F6E NAC_AI_API_URL \u548C NAC_AI_API_KEY" }; }), // ─── 阿拉伯语RTL专项测试 ──────────────────────────── testArabicRTL: nacAdminProcedure.mutation(async () => { const report = await runArabicRTLTests(); return report; }), // ─── 批量导入合规规则 ───────────────────────────────── batchImport: nacAdminProcedure.input(z2.object({ rules: z2.array(z2.object({ jurisdiction: z2.string(), assetType: z2.string(), ruleName: z2.string(), description: z2.string(), required: z2.boolean().optional().default(true), tags: z2.array(z2.string()).optional().default([]), ruleNameI18n: z2.record(z2.string(), z2.string()).optional(), descriptionI18n: z2.record(z2.string(), z2.string()).optional() })), skipDuplicates: z2.boolean().optional().default(true) })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR", message: "\u6570\u636E\u5E93\u8FDE\u63A5\u5931\u8D25" }); let imported = 0; let skipped = 0; let failed = 0; const errors = []; for (const rule of input.rules) { try { if (input.skipDuplicates) { const exists = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ jurisdiction: rule.jurisdiction, ruleName: rule.ruleName }); if (exists) { skipped++; continue; } } let ruleNameI18n = rule.ruleNameI18n || {}; let descriptionI18n = rule.descriptionI18n || {}; if (!ruleNameI18n.en || !descriptionI18n.en) { try { const translations = await generateRuleTranslations( rule.ruleName, rule.description, "zh" ); ruleNameI18n = { ...translations.ruleNameI18n, ...ruleNameI18n }; descriptionI18n = { ...translations.descriptionI18n, ...descriptionI18n }; } catch { ruleNameI18n = { zh: rule.ruleName, ...ruleNameI18n }; descriptionI18n = { zh: rule.description, ...descriptionI18n }; } } await db2.collection(COLLECTIONS.COMPLIANCE_RULES).insertOne({ jurisdiction: rule.jurisdiction, assetType: rule.assetType, ruleName: rule.ruleName, description: rule.description, ruleNameI18n, descriptionI18n, required: rule.required ?? true, tags: rule.tags ?? [], status: "active", createdAt: /* @__PURE__ */ new Date() }); imported++; } catch (e) { failed++; errors.push(`[${rule.jurisdiction}] ${rule.ruleName}: ${e.message}`); } } await writeAuditLog("BATCH_IMPORT_RULES", ctx.nacUser.id, ctx.nacUser.email, { total: input.rules.length, imported, skipped, failed }); return { success: true, imported, skipped, failed, errors }; }), // ─── 获取知识库统计 ───────────────────────────────────── stats: nacAuthProcedure.query(async () => { const db2 = await getMongoDb(); if (!db2) return { total: 0, byJurisdiction: [], byAssetType: [], byStatus: [] }; const [total, byJurisdiction, byAssetType, byStatus] = await Promise.all([ db2.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(), db2.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([ { $group: { _id: "$jurisdiction", count: { $sum: 1 } } }, { $sort: { count: -1 } } ]).toArray(), db2.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([ { $group: { _id: "$assetType", count: { $sum: 1 } } }, { $sort: { count: -1 } } ]).toArray(), db2.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([ { $group: { _id: "$status", count: { $sum: 1 } } } ]).toArray() ]); return { total, byJurisdiction, byAssetType, byStatus }; }) }), // ─── 采集器监控与管理 ──────────────────────────────────────────── crawler: router({ list: nacAuthProcedure.query(async () => { const db2 = await getMongoDb(); if (!db2) return []; return db2.collection(COLLECTIONS.CRAWLERS).find({}).sort({ createdAt: -1 }).toArray(); }), logs: nacAuthProcedure.input(z2.object({ crawlerId: z2.string().optional(), limit: z2.number().default(50) })).query(async ({ input }) => { const db2 = await getMongoDb(); if (!db2) return []; const filter = {}; if (input.crawlerId) filter.crawlerId = input.crawlerId; return db2.collection(COLLECTIONS.CRAWLER_LOGS).find(filter).sort({ timestamp: -1 }).limit(input.limit).toArray(); }), trigger: nacAdminProcedure.input(z2.object({ crawlerId: z2.string() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const crawler = await db2.collection(COLLECTIONS.CRAWLERS).findOne({ _id: new ObjectId2(input.crawlerId) }); if (!crawler) throw new TRPCError3({ code: "NOT_FOUND", message: "\u91C7\u96C6\u5668\u4E0D\u5B58\u5728" }); await db2.collection(COLLECTIONS.CRAWLER_LOGS).insertOne({ crawlerId: input.crawlerId, crawlerName: crawler.name, action: "manual_trigger", status: "triggered", message: "\u7BA1\u7406\u5458\u624B\u52A8\u89E6\u53D1\u91C7\u96C6\u4EFB\u52A1", timestamp: /* @__PURE__ */ new Date() }); await db2.collection(COLLECTIONS.CRAWLERS).updateOne({ _id: new ObjectId2(input.crawlerId) }, { $set: { lastRun: /* @__PURE__ */ new Date() } }); await writeAuditLog("TRIGGER_CRAWLER", ctx.nacUser.id, ctx.nacUser.email, { crawlerId: input.crawlerId, crawlerName: crawler.name }); return { success: true, message: `\u91C7\u96C6\u5668 "${crawler.name}" \u5DF2\u89E6\u53D1` }; }), create: nacAdminProcedure.input(z2.object({ name: z2.string(), jurisdiction: z2.string(), type: z2.enum(["internal", "external"]), source: z2.string(), category: z2.string(), frequency: z2.string() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const result = await db2.collection(COLLECTIONS.CRAWLERS).insertOne({ ...input, status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }); await writeAuditLog("CREATE_CRAWLER", ctx.nacUser.id, ctx.nacUser.email, { crawlerName: input.name }); return { id: result.insertedId }; }) }), // ─── AI审批案例审查 ────────────────────────────────────────────── approvalCase: router({ list: nacAuthProcedure.input(z2.object({ status: z2.string().optional(), riskLevel: z2.string().optional(), page: z2.number().default(1), pageSize: z2.number().default(20) })).query(async ({ input }) => { const db2 = await getMongoDb(); if (!db2) return { items: [], total: 0 }; const filter = {}; if (input.status) filter.status = input.status; if (input.riskLevel) filter.riskLevel = input.riskLevel; const skip = (input.page - 1) * input.pageSize; const [items, total] = await Promise.all([ db2.collection(COLLECTIONS.APPROVAL_CASES).find(filter).sort({ createdAt: -1 }).skip(skip).limit(input.pageSize).toArray(), db2.collection(COLLECTIONS.APPROVAL_CASES).countDocuments(filter) ]); return { items, total }; }), review: nacAuthProcedure.input(z2.object({ id: z2.string(), decision: z2.enum(["approved", "rejected"]), comment: z2.string().optional() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const nacUser = ctx.nacUser; await db2.collection(COLLECTIONS.APPROVAL_CASES).updateOne( { _id: new ObjectId2(input.id) }, { $set: { status: "reviewed", decision: input.decision, reviewComment: input.comment, reviewedBy: nacUser.email, reviewedAt: /* @__PURE__ */ new Date() } } ); await writeAuditLog("REVIEW_CASE", nacUser.id, nacUser.email, { caseId: input.id, decision: input.decision }); return { success: true }; }) }), // ─── 标签与规则引擎治理 ────────────────────────────────────────── tagEngine: router({ listRules: nacAuthProcedure.query(async () => { const db2 = await getMongoDb(); if (!db2) return []; return db2.collection(COLLECTIONS.TAG_RULES).find({}).sort({ createdAt: -1 }).toArray(); }), correctTag: nacAuthProcedure.input(z2.object({ documentId: z2.string(), originalTags: z2.array(z2.string()), correctedTags: z2.array(z2.string()), reason: z2.string() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const nacUser = ctx.nacUser; await db2.collection(COLLECTIONS.TAG_RULES).insertOne({ type: "correction", documentId: input.documentId, originalTags: input.originalTags, correctedTags: input.correctedTags, reason: input.reason, correctedBy: nacUser.email, isTrainingData: true, createdAt: /* @__PURE__ */ new Date() }); await writeAuditLog("CORRECT_TAG", nacUser.id, nacUser.email, { documentId: input.documentId }); return { success: true }; }), createRule: nacAdminProcedure.input(z2.object({ keyword: z2.string(), tags: z2.array(z2.string()), dimension: z2.string(), description: z2.string() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const result = await db2.collection(COLLECTIONS.TAG_RULES).insertOne({ ...input, type: "rule", status: "active", createdAt: /* @__PURE__ */ new Date() }); await writeAuditLog("CREATE_TAG_RULE", ctx.nacUser.id, ctx.nacUser.email, { keyword: input.keyword }); return { id: result.insertedId }; }) }), // ─── 协议族注册表管理 ──────────────────────────────────────────── protocolRegistry: router({ list: nacAuthProcedure.query(async () => { const db2 = await getMongoDb(); if (!db2) return []; return db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).find({}).sort({ createdAt: -1 }).toArray(); }), register: nacAdminProcedure.input(z2.object({ name: z2.string(), type: z2.string(), version: z2.string(), endpoint: z2.string(), trigger: z2.string(), description: z2.string().optional() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const result = await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).insertOne({ ...input, status: "active", createdAt: /* @__PURE__ */ new Date() }); await writeAuditLog("REGISTER_PROTOCOL", ctx.nacUser.id, ctx.nacUser.email, { protocolName: input.name }); return { id: result.insertedId }; }), toggleStatus: nacAdminProcedure.input(z2.object({ id: z2.string(), status: z2.enum(["active", "disabled", "deprecated"]) })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).updateOne({ _id: new ObjectId2(input.id) }, { $set: { status: input.status, updatedAt: /* @__PURE__ */ new Date() } }); await writeAuditLog("TOGGLE_PROTOCOL", ctx.nacUser.id, ctx.nacUser.email, { protocolId: input.id, newStatus: input.status }); return { success: true }; }), updateVersion: nacAdminProcedure.input(z2.object({ id: z2.string(), version: z2.string(), trigger: z2.string().optional() })).mutation(async ({ input, ctx }) => { const db2 = await getMongoDb(); if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" }); const update = { version: input.version, updatedAt: /* @__PURE__ */ new Date() }; if (input.trigger) update.trigger = input.trigger; await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).updateOne({ _id: new ObjectId2(input.id) }, { $set: update }); await writeAuditLog("UPDATE_PROTOCOL_VERSION", ctx.nacUser.id, ctx.nacUser.email, { protocolId: input.id, version: input.version }); return { success: true }; }) }), // ─── AI智能体系统 ───────────────────────────────────────────────── aiAgent: router({ // 获取所有Agent列表 list: nacAuthProcedure.query(() => { return { agents: AGENT_REGISTRY, configured: isAgentConfigured(), configHint: isAgentConfigured() ? null : "\u8BF7\u5728\u670D\u52A1\u5668 .env \u4E2D\u914D\u7F6E NAC_AI_API_URL \u548C NAC_AI_API_KEY\uFF08\u63A8\u8350\uFF1A\u963F\u91CC\u4E91\u901A\u4E49\u5343\u95EE\uFF09" }; }), // 与Agent对话(支持会话持久化) chat: nacAuthProcedure.input(z2.object({ agentType: z2.enum(["knowledge_qa", "compliance", "translation", "approval_assist"]), userMessage: z2.string().min(1).max(4e3), conversationId: z2.string().optional(), // 传入则续接历史会话 conversationHistory: z2.array(z2.object({ role: z2.enum(["system", "user", "assistant"]), content: z2.string() })).optional().default([]), context: z2.record(z2.string(), z2.unknown()).optional(), persistHistory: z2.boolean().optional().default(true) // 是否持久化到MongoDB })).mutation(async ({ input, ctx }) => { const nacUser = ctx.nacUser; const userId = nacUser?.id || 0; const userEmail = nacUser?.email || "unknown"; let historyMessages = input.conversationHistory; let convId = input.conversationId; if (convId && input.persistHistory) { try { const dbMessages = await loadConversationMessages(convId, userId, 20); if (dbMessages.length > 0) { historyMessages = messagesToAgentHistory(dbMessages); } } catch (e) { console.warn("[aiAgent.chat] \u52A0\u8F7D\u5386\u53F2\u5931\u8D25:", e.message); } } const response = await runAgent({ agentType: input.agentType, userMessage: input.userMessage, conversationHistory: historyMessages, context: input.context }); if (input.persistHistory) { try { if (!convId) { convId = await createConversation( userId, userEmail, input.agentType, input.userMessage ); } await saveMessagePair( convId, input.userMessage, response.message, response.confidence, response.sources, response.suggestions ); } catch (e) { console.warn("[aiAgent.chat] \u5BF9\u8BDD\u5386\u53F2\u6301\u4E45\u5316\u5931\u8D25:", e.message); } } await writeAuditLog("AGENT_CHAT", userId, userEmail, { agentType: input.agentType, conversationId: convId, messageLength: input.userMessage.length, confidence: response.confidence }); return { ...response, conversationId: convId }; }), // 获取用户的会话列表 listConversations: nacAuthProcedure.input(z2.object({ agentType: z2.enum(["knowledge_qa", "compliance", "translation", "approval_assist"]).optional(), limit: z2.number().min(1).max(50).default(20), skip: z2.number().min(0).default(0) })).query(async ({ input, ctx }) => { const userId = ctx.nacUser?.id || 0; return await listConversations(userId, input.agentType, input.limit, input.skip); }), // 加载单个会话的历史消息 loadHistory: nacAuthProcedure.input(z2.object({ conversationId: z2.string(), limit: z2.number().min(1).max(100).default(50) })).query(async ({ input, ctx }) => { const userId = ctx.nacUser?.id || 0; const [conv, messages] = await Promise.all([ getConversation(input.conversationId, userId), loadConversationMessages(input.conversationId, userId, input.limit) ]); if (!conv) throw new TRPCError3({ code: "NOT_FOUND", message: "\u4F1A\u8BDD\u4E0D\u5B58\u5728" }); return { conversation: conv, messages }; }), // 删除会话 deleteConversation: nacAuthProcedure.input(z2.object({ conversationId: z2.string() })).mutation(async ({ input, ctx }) => { const userId = ctx.nacUser?.id || 0; const deleted = await deleteConversation(input.conversationId, userId); if (!deleted) throw new TRPCError3({ code: "NOT_FOUND", message: "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u9650\u5220\u9664" }); return { success: true }; }), // 检查AI服务状态 status: nacAuthProcedure.query(() => ({ configured: isAgentConfigured(), apiUrl: process.env.NAC_AI_API_URL ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E", model: process.env.NAC_AI_MODEL || "qwen-plus\uFF08\u9ED8\u8BA4\uFF09" })) }), // ─── 案例库归档管理 ─────────────────────────────────────────────── archive: router({ // 试运行归档(统计数量,不实际迁移) dryRun: nacAdminProcedure.query(async () => { return await runArchive(true); }), // 执行归档 run: nacAdminProcedure.mutation(async ({ ctx }) => { await writeAuditLog("RUN_ARCHIVE", ctx.nacUser.id, ctx.nacUser.email, { trigger: "manual" }); return await runArchive(false); }), // 归档历史记录 logs: nacAdminProcedure.input(z2.object({ limit: z2.number().default(20) })).query(async ({ input }) => { return await getArchiveLogs(input.limit); }), // 查询归档案例 listArchived: nacAdminProcedure.input(z2.object({ page: z2.number().default(1), pageSize: z2.number().default(20), jurisdiction: z2.string().optional(), status: z2.string().optional() })).query(async ({ input }) => { return await getArchivedCases(input.page, input.pageSize, { jurisdiction: input.jurisdiction, status: input.status }); }) }), // ─── 告警通知管理 ───────────────────────────────────────────────── notification: router({ // 获取Webhook配置状态 webhookStatus: nacAdminProcedure.query(() => getWebhookStatus()), // 发送测试通知 test: nacAdminProcedure.input(z2.object({ channel: z2.enum(["wecom", "dingtalk", "feishu", "generic"]), message: z2.string().optional() })).mutation(async ({ input, ctx }) => { const { notifyOwner: notifyOwner2 } = await Promise.resolve().then(() => (init_notification(), notification_exports)); const result = await notifyOwner2({ title: `NAC\u544A\u8B66\u6D4B\u8BD5 - ${input.channel}`, content: input.message || `\u8FD9\u662F\u6765\u81EANAC Knowledge Engine Admin\u7684\u6D4B\u8BD5\u901A\u77E5\u3002 \u53D1\u9001\u65F6\u95F4\uFF1A${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} \u53D1\u9001\u4EBA\uFF1A${ctx.nacUser?.email}`, level: "info", module: "test" }); await writeAuditLog("TEST_NOTIFICATION", ctx.nacUser.id, ctx.nacUser.email, { channel: input.channel }); return { success: result }; }), // 模拟采集器告警(测试用) testCrawlerAlert: nacAdminProcedure.mutation(async ({ ctx }) => { await notifyCrawlerError("CN-CSRC\u6CD5\u89C4\u91C7\u96C6\u5668", "\u8FDE\u63A5\u8D85\u65F6\uFF1Ahttp://www.csrc.gov.cn \u54CD\u5E94\u65F6\u95F4\u8D85\u8FC730\u79D2"); await writeAuditLog("TEST_CRAWLER_ALERT", ctx.nacUser.id, ctx.nacUser.email, {}); return { success: true }; }) }), // ─── 权限与审计管理 ────────────────────────────────────────────── rbac: router({ listUsers: nacAdminProcedure.input(z2.object({ page: z2.number().default(1), pageSize: z2.number().default(20) })).query(async ({ input }) => { const offset = (input.page - 1) * input.pageSize; const [users, total] = await Promise.all([listNacUsers(input.pageSize, offset), getNacUserCount()]); return { users, total }; }), auditLogs: nacAdminProcedure.input(z2.object({ action: z2.string().optional(), userId: z2.number().optional(), page: z2.number().default(1), pageSize: z2.number().default(50) })).query(async ({ input }) => { const db2 = await getMongoDb(); if (!db2) return { items: [], total: 0 }; const filter = {}; if (input.action) filter.action = input.action; if (input.userId) filter.userId = input.userId; const skip = (input.page - 1) * input.pageSize; const [items, total] = await Promise.all([ db2.collection(COLLECTIONS.AUDIT_LOGS).find(filter).sort({ timestamp: -1 }).skip(skip).limit(input.pageSize).toArray(), db2.collection(COLLECTIONS.AUDIT_LOGS).countDocuments(filter) ]); return { items, total }; }) }), // ─── 数据库管理 ──────────────────────────────────────────────── dbAdmin: router({ // 初始化MongoDB索引(全文索引+TTL索引) initIndexes: nacAdminProcedure.mutation(async ({ ctx }) => { const result = await initMongoIndexes(); await writeAuditLog("INIT_MONGO_INDEXES", ctx.nacUser.id, ctx.nacUser.email, { summary: result.summary }); return result; }), // 查询当前索引状态 indexStatus: nacAdminProcedure.query(async () => { const db2 = await getMongoDb(); if (!db2) return { collections: [] }; const collections = [ COLLECTIONS.COMPLIANCE_RULES, COLLECTIONS.AGENT_CONVERSATIONS, "knowledge_base" ]; const status = await Promise.all( collections.map(async (colName) => { try { const col = db2.collection(colName); const indexes = await col.listIndexes().toArray(); return { collection: colName, indexCount: indexes.length, hasTextIndex: indexes.some((idx) => Object.values(idx.key || {}).includes("text")), hasTTLIndex: indexes.some((idx) => idx.expireAfterSeconds !== void 0), indexes: indexes.map((idx) => ({ name: idx.name, key: idx.key, ttl: idx.expireAfterSeconds })) }; } catch { return { collection: colName, indexCount: 0, hasTextIndex: false, hasTTLIndex: false, indexes: [] }; } }) ); return { collections: status }; }) }) }); // server/_core/context.ts async function createContext(opts) { let user = null; try { const cookies = opts.req.cookies || {}; const token = cookies["nac_admin_token"] || opts.req.headers["x-nac-token"]; if (token) { const payload = verifyNacToken(token); if (payload) { const nacUser = await getNacUserById(payload.id); if (nacUser) { user = { id: nacUser.id, email: nacUser.email, role: nacUser.role, openId: nacUser.email // 兼容性字段 }; } } } } catch (error) { user = null; } return { req: opts.req, res: opts.res, user }; } // server/_core/index.ts function isPortAvailable(port) { return new Promise((resolve) => { const server = net.createServer(); server.listen(port, () => { server.close(() => resolve(true)); }); server.on("error", () => resolve(false)); }); } async function findAvailablePort(startPort = 3e3) { for (let port = startPort; port < startPort + 20; port++) { if (await isPortAvailable(port)) { return port; } } throw new Error(`No available port found starting from ${startPort}`); } async function startServer() { const app = express3(); const server = createServer(app); app.set("trust proxy", 1); app.use(cookieParser()); app.use(express3.json({ limit: "50mb" })); app.use(express3.urlencoded({ limit: "50mb", extended: true })); app.use( "/api/trpc", createExpressMiddleware({ router: appRouter, createContext }) ); if (process.env.NODE_ENV === "development") { const { setupVite: setupVite2 } = await Promise.resolve().then(() => (init_vite(), vite_exports)); await setupVite2(app, server); } else { const { serveStatic: serveStatic3 } = await Promise.resolve().then(() => (init_static(), static_exports)); serveStatic3(app); } const preferredPort = parseInt(process.env.PORT || "3000"); const port = await findAvailablePort(preferredPort); if (port !== preferredPort) { console.log(`Port ${preferredPort} is busy, using port ${port} instead`); } server.listen(port, () => { console.log(`[NAC Admin] Server running on http://localhost:${port}/`); }); } startServer().catch(console.error);