1212 lines
66 KiB
JavaScript
1212 lines
66 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/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);
|