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

1212 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// server/_core/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
import { z } from "zod";
// server/_core/notification.ts
import { TRPCError as TRPCError2 } from "@trpc/server";
var TITLE_MAX_LENGTH = 1200;
var CONTENT_MAX_LENGTH = 2e4;
var trimValue = (value) => value.trim();
var isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
var 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 };
};
async function notifyOwner(payload) {
const { title, content } = validatePayload(payload);
const webhookUrl = process.env.NAC_NOTIFY_WEBHOOK_URL;
if (webhookUrl) {
try {
const response = await fetch(webhookUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ title, content, timestamp: (/* @__PURE__ */ new Date()).toISOString() })
});
if (response.ok) {
console.log(`[Notification] \u901A\u77E5\u5DF2\u53D1\u9001: ${title}`);
return true;
}
console.warn(`[Notification] Webhook\u53D1\u9001\u5931\u8D25 (${response.status}): ${title}`);
} catch (error) {
console.warn("[Notification] Webhook\u8C03\u7528\u5931\u8D25:", error);
}
}
console.log(`[Notification] \u7CFB\u7EDF\u901A\u77E5 - ${title}: ${content.slice(0, 200)}`);
return true;
}
// server/_core/systemRouter.ts
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"
};
// server/routers.ts
import { ObjectId } 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/routers.ts
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(),
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;
const skip = (input.page - 1) * input.pageSize;
const [items, total] = await Promise.all([
db2.collection(COLLECTIONS.COMPLIANCE_RULES).find(filter).sort({ createdAt: -1 }).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 ObjectId(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 ObjectId(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 ObjectId(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 ObjectId(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 ObjectId(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 ObjectId(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 ObjectId(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 ObjectId(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 ObjectId(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 ObjectId(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 ObjectId(input.id) }, { $set: update });
await writeAuditLog("UPDATE_PROTOCOL_VERSION", ctx.nacUser.id, ctx.nacUser.email, { protocolId: input.id, version: input.version });
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 };
})
})
});
// 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);