2602 lines
118 KiB
JavaScript
2602 lines
118 KiB
JavaScript
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);
|