NAC_Blockchain/ops/nac-admin/dist_backup_v13_20260226_16.../index.js

2602 lines
118 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<font color="${color}">${level.toUpperCase()}</font>`
}
};
}
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;
})
}),
// ─── 采集器监控与管理 ────────────────────────────────────────────
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);