NAC_Blockchain/services/nac-admin/server/routers.ts

1285 lines
67 KiB
TypeScript
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.

import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
import { systemRouter } from "./_core/systemRouter";
import { loginWithNacCredentials, verifyNacToken, listNacUsers, getNacUserCount } from "./nacAuth";
import { getMongoDb, COLLECTIONS } from "./mongodb";
import { ObjectId } from "mongodb";
import { getSessionCookieOptions } from "./_core/cookies";
import { generateRuleTranslations, migrateRuleToMultiLang, SUPPORTED_LANGUAGES, isAiTranslationConfigured, runArabicRTLTests, isRTL, type SupportedLanguage } from "./i18nTranslation";
import { runAgent, isAgentConfigured, AGENT_REGISTRY, type AgentType, type AgentMessage } from "./aiAgents";
import {
createConversation,
listConversations,
getConversation,
deleteConversation,
saveMessagePair,
loadConversationMessages,
messagesToAgentHistory,
} from "./agentConversations";
import { runArchive, getArchiveLogs, getArchivedCases } from "./archiveApprovalCases";
import { semanticSearch, precomputeEmbeddings } from "./semanticSearch";
import { saveRuleVersion, getRuleVersionHistory, compareRuleVersions, rollbackRuleToVersion } from "./ruleVersions";
import { generateComplianceReport } from "./reportGenerator";
import { getRegulatoryUpdates, fetchRegulatoryUpdates, applyRuleUpdateSuggestion, dismissRegulatoryUpdate } from "./regulatoryMonitor";
import { detectConflicts, getDetectedConflicts, generateConflictReport } from "./conflictDetector";
import { initMongoIndexes } from "./initMongoIndexes";
import { getWebhookStatus, notifyCrawlerError } from "./_core/notification";
// ─── NAC JWT 认证中间件 ───────────────────────────────────────────
const nacAuthProcedure = publicProcedure.use(async ({ ctx, next }) => {
const token = (ctx.req as any).cookies?.["nac_admin_token"] || ctx.req.headers["x-nac-token"] as string;
if (!token) throw new TRPCError({ code: "UNAUTHORIZED", message: "请先登录" });
const payload = verifyNacToken(token);
if (!payload) throw new TRPCError({ code: "UNAUTHORIZED", message: "登录已过期,请重新登录" });
return next({ ctx: { ...ctx, nacUser: payload } });
});
const nacAdminProcedure = nacAuthProcedure.use(async ({ ctx, next }) => {
if ((ctx as any).nacUser?.role !== "admin") {
throw new TRPCError({ code: "FORBIDDEN", message: "需要管理员权限" });
}
return next({ ctx });
});
// ─── 审计日志写入 ─────────────────────────────────────────────────
async function writeAuditLog(action: string, userId: number, email: string, detail: object) {
try {
const db = await getMongoDb();
if (!db) return;
await db.collection(COLLECTIONS.AUDIT_LOGS).insertOne({
action, userId, email, detail,
timestamp: new Date(), immutable: true,
});
} catch (e) {
console.error("[AuditLog] Failed:", (e as Error).message);
}
}
// ─── 初始化知识库基础数据 ─────────────────────────────────────────
async function ensureKnowledgeBaseData() {
const db = await getMongoDb();
if (!db) return;
const protocolCount = await db.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments();
if (protocolCount === 0) {
await db.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: 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: 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: 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: 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: new Date() },
]);
}
const crawlerCount = await db.collection(COLLECTIONS.CRAWLERS).countDocuments();
if (crawlerCount === 0) {
await db.collection(COLLECTIONS.CRAWLERS).insertMany([
{ name: "CN-CSRC法规采集器", jurisdiction: "CN", type: "external", source: "http://www.csrc.gov.cn", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() },
{ name: "HK-SFC法规采集器", jurisdiction: "HK", type: "external", source: "https://www.sfc.hk", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() },
{ name: "US-SEC法规采集器", jurisdiction: "US", type: "external", source: "https://www.sec.gov", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() },
{ name: "EU-ESMA法规采集器", jurisdiction: "EU", type: "external", source: "https://www.esma.europa.eu", category: "regulation", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() },
{ name: "SG-MAS法规采集器", jurisdiction: "SG", type: "external", source: "https://www.mas.gov.sg", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() },
{ name: "AE-DFSA法规采集器", jurisdiction: "AE", type: "external", source: "https://www.dfsa.ae", category: "regulation", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() },
{ name: "CN-裁判文书网采集器", jurisdiction: "CN", type: "external", source: "https://wenshu.court.gov.cn", category: "credit", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() },
{ name: "内部上链文件采集器", jurisdiction: "ALL", type: "internal", source: "internal://onboarding", category: "asset_document", frequency: "realtime", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() },
]);
}
const ruleCount = await db.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments();
if (ruleCount === 0) {
await db.collection(COLLECTIONS.COMPLIANCE_RULES).insertMany([
{
jurisdiction: "CN", assetType: "RealEstate", ruleName: "不动产登记证要求",
description: "中国境内房地产上链必须提供不动产登记证",
ruleNameI18n: { zh: "不动产登记证要求", en: "Real Estate Registration Certificate Requirement", ar: "متطلبات شهادة تسجيل العقارات", ja: "不動産登記証要件", ko: "부동산 등기증 요건", fr: "Exigence de certificat d'enregistrement immobilier", ru: "Требование к свидетельству о регистрации недвижимости" },
descriptionI18n: { zh: "中国境内房地产上链必须提供不动产登记证", en: "Real estate assets on-chain in China must provide a real estate registration certificate", ar: "يجب على الأصول العقارية المسجلة على السلسلة في الصين تقديم شهادة تسجيل العقارات", ja: "中国国内の不動産チェーン登録には不動産登記証の提出が必要", ko: "중국 내 부동산 온체인 등록 시 부동산 등기증 제출 필수", fr: "Les actifs immobiliers enregistrés sur la chaîne en Chine doivent fournir un certificat d'enregistrement immobilier", ru: "Недвижимость, регистрируемая в блокчейне в Китае, должна предоставить свидетельство о регистрации недвижимости" },
required: true, status: "active", tags: ["CN", "RealEstate", "Document", "Required"], createdAt: new Date()
},
{
jurisdiction: "HK", assetType: "Securities", ruleName: "SFC持牌要求",
description: "香港证券类资产上链须经SFC持牌机构审核",
ruleNameI18n: { zh: "SFC持牌要求", en: "SFC Licensing Requirement", ar: "متطلبات ترخيص SFC", ja: "SFCライセンス要件", ko: "SFC 라이선스 요건", fr: "Exigence de licence SFC", ru: "Требование лицензии SFC" },
descriptionI18n: { zh: "香港证券类资产上链须经SFC持牌机构审核", en: "Securities assets on-chain in Hong Kong must be reviewed by SFC-licensed institutions", ar: "يجب مراجعة أصول الأوراق المالية المسجلة على السلسلة في هونغ كونغ من قبل مؤسسات مرخصة من SFC", ja: "香港の証券資産のチェーン登録はSFCライセンス機関の審査が必要", ko: "홍콩 증권 자산 온체인 등록 시 SFC 인가 기관의 심사 필요", fr: "Les actifs en valeurs mobilières enregistrés sur la chaîne à Hong Kong doivent être examinés par des institutions agréées SFC", ru: "Ценные бумаги, регистрируемые в блокчейне в Гонконге, должны пройти проверку учреждениями с лицензией SFC" },
required: true, status: "active", tags: ["HK", "Securities", "License", "SFC"], createdAt: new Date()
},
{
jurisdiction: "US", assetType: "Securities", ruleName: "Reg D豁免申报",
description: "美国证券类资产须满足Reg D/S豁免条件",
ruleNameI18n: { zh: "Reg D豁免申报", en: "Reg D Exemption Filing", ar: "تقديم إعفاء Reg D", ja: "Reg D免除申告", ko: "Reg D 면제 신고", fr: "Déclaration d'exemption Reg D", ru: "Подача заявления об освобождении по Reg D" },
descriptionI18n: { zh: "美国证券类资产须满足Reg D/S豁免条件", en: "US securities assets must meet Reg D/S exemption conditions", ar: "يجب أن تستوفي أصول الأوراق المالية الأمريكية شروط إعفاء Reg D/S", ja: "米国証券資産はReg D/S免除条件を満たす必要がある", ko: "미국 증권 자산은 Reg D/S 면제 조건을 충족해야 함", fr: "Les actifs en valeurs mobilières américains doivent satisfaire aux conditions d'exemption Reg D/S", ru: "Ценные бумаги США должны соответствовать условиям освобождения по Reg D/S" },
required: true, status: "active", tags: ["US", "Securities", "RegD", "SEC"], createdAt: new Date()
},
{
jurisdiction: "EU", assetType: "ALL", ruleName: "MiCA合规要求",
description: "欧盟境内所有加密资产须符合MiCA法规",
ruleNameI18n: { zh: "MiCA合规要求", en: "MiCA Compliance Requirement", ar: "متطلبات الامتثال MiCA", ja: "MiCAコンプライアンス要件", ko: "MiCA 준수 요건", fr: "Exigence de conformité MiCA", ru: "Требование соответствия MiCA" },
descriptionI18n: { zh: "欧盟境内所有加密资产须符合MiCA法规", en: "All crypto assets within the EU must comply with MiCA regulations", ar: "يجب أن تمتثل جميع الأصول المشفرة داخل الاتحاد الأوروبي للوائح MiCA", ja: "EU域内のすべての暗号資産はMiCA規制に準拠する必要がある", ko: "EU 내 모든 암호화 자산은 MiCA 규정을 준수해야 함", fr: "Tous les crypto-actifs au sein de l'UE doivent se conformer aux réglementations MiCA", ru: "Все криптоактивы в ЕС должны соответствовать регламенту MiCA" },
required: true, status: "active", tags: ["EU", "ALL", "MiCA", "ESMA"], createdAt: new Date()
},
{
jurisdiction: "SG", assetType: "DigitalToken", ruleName: "MAS数字代币服务牌照",
description: "新加坡数字代币服务须持MAS牌照",
ruleNameI18n: { zh: "MAS数字代币服务牌照", en: "MAS Digital Token Service License", ar: "ترخيص خدمة الرمز الرقمي MAS", ja: "MASデジタルトークンサービスライセンス", ko: "MAS 디지털 토큰 서비스 라이선스", fr: "Licence de service de jetons numériques MAS", ru: "Лицензия на услуги цифровых токенов MAS" },
descriptionI18n: { zh: "新加坡数字代币服务须持MAS牌照", en: "Digital token services in Singapore must hold a MAS license", ar: "يجب أن تحمل خدمات الرمز الرقمي في سنغافورة ترخيص MAS", ja: "シンガポールのデジタルトークンサービスはMASライセンスが必要", ko: "싱가포르 디지털 토큰 서비스는 MAS 라이선스 보유 필요", fr: "Les services de jetons numériques à Singapour doivent détenir une licence MAS", ru: "Услуги цифровых токенов в Сингапуре должны иметь лицензию MAS" },
required: true, status: "active", tags: ["SG", "DigitalToken", "MAS", "License"], createdAt: new Date()
},
{
jurisdiction: "AE", assetType: "RealEstate", ruleName: "DLD产权证书要求",
description: "迪拜房地产上链须提供DLD颁发的产权证书",
ruleNameI18n: { zh: "DLD产权证书要求", en: "DLD Title Deed Requirement", ar: "متطلبات سند الملكية DLD", ja: "DLD所有権証書要件", ko: "DLD 소유권 증서 요건", fr: "Exigence de titre de propriété DLD", ru: "Требование к свидетельству о праве собственности DLD" },
descriptionI18n: { zh: "迪拜房地产上链须提供DLD颁发的产权证书", en: "Dubai real estate on-chain must provide a title deed issued by DLD", ar: "يجب أن توفر العقارات المسجلة على السلسلة في دبي سند ملكية صادر عن DLD", ja: "ドバイの不動産チェーン登録にはDLD発行の所有権証書が必要", ko: "두바이 부동산 온체인 등록 시 DLD 발급 소유권 증서 제출 필수", fr: "L'immobilier de Dubaï enregistré sur la chaîne doit fournir un titre de propriété délivré par DLD", ru: "Недвижимость Дубая, регистрируемая в блокчейне, должна предоставить свидетельство о праве собственности, выданное DLD" },
required: true, status: "active", tags: ["AE", "RealEstate", "DLD", "Document"], createdAt: new Date()
},
]);
}
}
// ─── 主路由 ───────────────────────────────────────────────────────
export const appRouter = router({
system: systemRouter,
// ─── NAC原生认证不使用Manus OAuth────────────────────────────
nacAuth: router({
login: publicProcedure
.input(z.object({ email: z.string().email(), password: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
try {
const result = await loginWithNacCredentials(input.email, input.password);
if (!result) throw new TRPCError({ code: "UNAUTHORIZED", message: "邮箱或密码错误" });
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.cookie("nac_admin_token", result.token, { ...cookieOptions, maxAge: 24 * 60 * 60 * 1000 });
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 TRPCError) throw e;
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "登录服务暂时不可用" });
}
}),
logout: publicProcedure.mutation(({ ctx }) => {
ctx.res.clearCookie("nac_admin_token");
return { success: true };
}),
me: nacAuthProcedure.query(async ({ ctx }) => {
const nacUser = (ctx as any).nacUser;
return { id: nacUser.id, email: nacUser.email, role: nacUser.role };
}),
}),
// ─── 全局态势感知仪表盘 ──────────────────────────────────────────
dashboard: router({
stats: nacAuthProcedure.query(async () => {
const db = await getMongoDb();
if (!db) return { error: "数据库连接失败" };
await ensureKnowledgeBaseData();
const [ruleCount, crawlerCount, caseCount, protocolCount, userCount, auditCount] = await Promise.all([
db.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(),
db.collection(COLLECTIONS.CRAWLERS).countDocuments(),
db.collection(COLLECTIONS.APPROVAL_CASES).countDocuments(),
db.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments(),
getNacUserCount(),
db.collection(COLLECTIONS.AUDIT_LOGS).countDocuments(),
]);
const [activeCrawlers, pendingCases, approvedCases, activeProtocols] = await Promise.all([
db.collection(COLLECTIONS.CRAWLERS).countDocuments({ status: "active" }),
db.collection(COLLECTIONS.APPROVAL_CASES).countDocuments({ status: "pending_review" }),
db.collection(COLLECTIONS.APPROVAL_CASES).countDocuments({ decision: "approved" }),
db.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments({ status: "active" }),
]);
const jurisdictionStats = await db.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: new Date() },
};
}),
recentActivity: nacAuthProcedure.query(async () => {
const db = await getMongoDb();
if (!db) return [];
return db.collection(COLLECTIONS.AUDIT_LOGS).find({}).sort({ timestamp: -1 }).limit(20).toArray();
}),
}),
// ─── 知识库管理(含多语言支持)──────────────────────────────────
knowledgeBase: router({
list: nacAuthProcedure
.input(z.object({
jurisdiction: z.string().optional(),
assetType: z.string().optional(),
status: z.string().optional(),
search: z.string().optional(), // 全文搜索关键词支持RAG来源引用跳转
page: z.number().default(1),
pageSize: z.number().default(20),
lang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).optional(),
}))
.query(async ({ input }) => {
const db = await getMongoDb();
if (!db) return { items: [], total: 0 };
const filter: Record<string, unknown> = {};
if (input.jurisdiction) filter.jurisdiction = input.jurisdiction;
if (input.assetType) filter.assetType = input.assetType;
if (input.status) filter.status = input.status;
// 全文搜索:优先使用$text索引降级到正则匹配
if (input.search) {
const kw = input.search.trim();
try {
// 尝试全文索引搜索
filter["$text"] = { $search: kw };
} catch {
// 降级到正则匹配
delete filter["$text"];
const re = new RegExp(kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
filter["$or"] = [
{ ruleName: re },
{ description: re },
{ "ruleNameI18n.zh": re },
{ "ruleNameI18n.en": re },
];
}
}
const skip = (input.page - 1) * input.pageSize;
// 全文搜索时按相关性排序,否则按创建时间降序
const sortOpt: Record<string, 1 | -1> = input.search
? { score: -1, createdAt: -1 }
: { createdAt: -1 };
const projection = input.search ? { score: { $meta: "textScore" } } : {};
const [items, total] = await Promise.all([
db.collection(COLLECTIONS.COMPLIANCE_RULES).find(filter, { projection }).sort(sortOpt).skip(skip).limit(input.pageSize).toArray(),
db.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(filter),
]);
// 根据请求语言返回对应翻译
const lang = input.lang || "zh";
const localizedItems = items.map((item: any) => ({
...item,
displayName: item.ruleNameI18n?.[lang] || item.ruleName,
displayDescription: item.descriptionI18n?.[lang] || item.description,
}));
return { items: localizedItems, total };
}),
create: nacAdminProcedure
.input(z.object({
jurisdiction: z.string(),
assetType: z.string(),
ruleName: z.string(),
description: z.string(),
required: z.boolean(),
tags: z.array(z.string()),
sourceLang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).optional(),
autoTranslate: z.boolean().optional(),
}))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
let ruleNameI18n: Record<string, string> = {};
let descriptionI18n: Record<string, string> = {};
// 如果启用自动翻译调用AI生成七语言翻译
if (input.autoTranslate !== false) {
try {
const translations = await generateRuleTranslations(
input.ruleName,
input.description,
(input.sourceLang || "zh") as SupportedLanguage
);
ruleNameI18n = translations.ruleNameI18n as Record<string, string>;
descriptionI18n = translations.descriptionI18n as Record<string, string>;
} catch (e) {
console.error("[KnowledgeBase] Auto-translate failed:", (e as Error).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 db.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: new Date(),
});
await writeAuditLog("CREATE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: result.insertedId });
return { id: result.insertedId };
}),
update: nacAdminProcedure
.input(z.object({
id: z.string(),
data: z.object({
ruleName: z.string().optional(),
description: z.string().optional(),
status: z.enum(["active", "disabled"]).optional(),
tags: z.array(z.string()).optional(),
}),
autoTranslate: z.boolean().optional(),
}))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const updateData: Record<string, unknown> = { ...input.data, updatedAt: new Date() };
// 如果更新了名称或描述,重新生成翻译
if (input.autoTranslate !== false && (input.data.ruleName || input.data.description)) {
const existing = await db.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ _id: new ObjectId(input.id) }) as any;
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 as Error).message);
}
}
}
// v15: 保存版本快照(更新前先读取旧数据)
const oldRule = await db.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ _id: new ObjectId(input.id) }) as any;
if (oldRule) {
await saveRuleVersion(
input.id,
oldRule,
updateData,
(ctx as any).nacUser.id,
(ctx as any).nacUser.email
).catch(e => console.error("[v15] saveRuleVersion failed:", e.message));
}
await db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne(
{ _id: new ObjectId(input.id) },
{ $set: updateData }
);
await writeAuditLog("UPDATE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: input.id });
return { success: true };
}),
toggleStatus: nacAdminProcedure
.input(z.object({ id: z.string(), status: z.enum(["active", "disabled"]) }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
await db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne({ _id: new ObjectId(input.id) }, { $set: { status: input.status, updatedAt: new Date() } });
await writeAuditLog("TOGGLE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: input.id, newStatus: input.status });
return { success: true };
}),
delete: nacAdminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
await db.collection(COLLECTIONS.COMPLIANCE_RULES).deleteOne({ _id: new ObjectId(input.id) });
await writeAuditLog("DELETE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: input.id });
return { success: true };
}),
// ─── AI辅助翻译接口 ──────────────────────────────────────────
translateRule: nacAdminProcedure
.input(z.object({
id: z.string(),
targetLang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).optional(),
}))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const rule = await db.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ _id: new ObjectId(input.id) }) as any;
if (!rule) throw new TRPCError({ code: "NOT_FOUND", message: "规则不存在" });
const translations = await migrateRuleToMultiLang({
ruleName: rule.ruleName,
description: rule.description,
ruleNameI18n: rule.ruleNameI18n,
descriptionI18n: rule.descriptionI18n,
});
await db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne(
{ _id: new ObjectId(input.id) },
{ $set: { ruleNameI18n: translations.ruleNameI18n, descriptionI18n: translations.descriptionI18n, updatedAt: new Date() } }
);
await writeAuditLog("TRANSLATE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: input.id });
return { success: true, translations };
}),
// ─── 批量迁移现有规则到多语言格式 ────────────────────────────
migrateAllToMultiLang: nacAdminProcedure
.mutation(async ({ ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
// 只迁移没有多语言字段的规则
const rules = await db.collection(COLLECTIONS.COMPLIANCE_RULES).find({
$or: [{ ruleNameI18n: { $exists: false } }, { "ruleNameI18n.en": { $exists: false } }]
}).toArray() as any[];
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 db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne(
{ _id: rule._id },
{ $set: { ruleNameI18n: translations.ruleNameI18n, descriptionI18n: translations.descriptionI18n, updatedAt: new Date() } }
);
migrated++;
} catch (e) {
console.error(`[Migration] Failed for rule ${rule._id}:`, (e as Error).message);
}
}
await writeAuditLog("MIGRATE_MULTILANG", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { migratedCount: migrated });
return { success: true, migratedCount: migrated, totalRules: rules.length };
}),
// // ─── 获取支持的语言列表 ──────────────────────────────
getSupportedLanguages: nacAuthProcedure.query(() => {
return SUPPORTED_LANGUAGES.map(lang => ({
code: lang,
name: { zh: "中文(简体)", en: "English", ar: "العربية", ja: "日本語", ko: "한국어", fr: "Français", ru: "Русский" }[lang],
isRTL: isRTL(lang as SupportedLanguage),
}));
}),
// ─── 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翻译服务已配置可以使用自动翻译功能"
: "AI翻译服务未配置。请在服务器 .env 中设置 NAC_AI_API_URL 和 NAC_AI_API_KEY",
};
}),
// ─── 阿拉伯语RTL专项测试 ────────────────────────────
testArabicRTL: nacAdminProcedure
.mutation(async () => {
const report = await runArabicRTLTests();
return report;
}),
// ─── 批量导入合规规则 ─────────────────────────────────
batchImport: nacAdminProcedure
.input(z.object({
rules: z.array(z.object({
jurisdiction: z.string(),
assetType: z.string(),
ruleName: z.string(),
description: z.string(),
required: z.boolean().optional().default(true),
tags: z.array(z.string()).optional().default([]),
ruleNameI18n: z.record(z.string(), z.string()).optional(),
descriptionI18n: z.record(z.string(), z.string()).optional(),
})),
skipDuplicates: z.boolean().optional().default(true),
}))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "数据库连接失败" });
let imported = 0;
let skipped = 0;
let failed = 0;
const errors: string[] = [];
for (const rule of input.rules) {
try {
// 去重检查
if (input.skipDuplicates) {
const exists = await db.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({
jurisdiction: rule.jurisdiction,
ruleName: rule.ruleName,
});
if (exists) {
skipped++;
continue;
}
}
// 如果没有提供多语言字段尝试AI翻译
let ruleNameI18n = rule.ruleNameI18n || {};
let descriptionI18n = rule.descriptionI18n || {};
if (!ruleNameI18n.en || !descriptionI18n.en) {
try {
const translations = await generateRuleTranslations(
rule.ruleName,
rule.description,
"zh"
);
ruleNameI18n = { ...translations.ruleNameI18n as Record<string, string>, ...ruleNameI18n };
descriptionI18n = { ...translations.descriptionI18n as Record<string, string>, ...descriptionI18n };
} catch {
// AI翻译失败使用源语言
ruleNameI18n = { zh: rule.ruleName, ...ruleNameI18n };
descriptionI18n = { zh: rule.description, ...descriptionI18n };
}
}
await db.collection(COLLECTIONS.COMPLIANCE_RULES).insertOne({
jurisdiction: rule.jurisdiction,
assetType: rule.assetType,
ruleName: rule.ruleName,
description: rule.description,
ruleNameI18n,
descriptionI18n,
required: rule.required ?? true,
tags: rule.tags ?? [],
status: "active",
createdAt: new Date(),
});
imported++;
} catch (e) {
failed++;
errors.push(`[${rule.jurisdiction}] ${rule.ruleName}: ${(e as Error).message}`);
}
}
await writeAuditLog("BATCH_IMPORT_RULES", (ctx as any).nacUser.id, (ctx as any).nacUser.email, {
total: input.rules.length, imported, skipped, failed,
});
return { success: true, imported, skipped, failed, errors };
}),
// ─── v14: AI语义检索 ──────────────────────────────────────────
semanticSearch: nacAuthProcedure
.input(z.object({
query: z.string().min(1).max(500),
jurisdiction: z.string().optional(),
assetType: z.string().optional(),
limit: z.number().min(1).max(50).default(10),
lang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).default("zh"),
minScore: z.number().min(0).max(1).default(0.45),
}))
.query(async ({ input }) => {
const result = await semanticSearch(input.query, {
jurisdiction: input.jurisdiction,
assetType: input.assetType,
limit: input.limit,
lang: input.lang,
minScore: input.minScore,
});
return result;
}),
// ─── v15: 规则版本历史 ──────────────────────────────────────────
getVersionHistory: nacAuthProcedure
.input(z.object({
ruleId: z.string(),
limit: z.number().default(20),
}))
.query(async ({ input }) => {
const versions = await getRuleVersionHistory(input.ruleId, input.limit);
return versions;
}),
compareVersions: nacAuthProcedure
.input(z.object({
ruleId: z.string(),
versionA: z.number(),
versionB: z.number(),
}))
.query(async ({ input }) => {
return compareRuleVersions(input.ruleId, input.versionA, input.versionB);
}),
rollbackVersion: nacAdminProcedure
.input(z.object({
ruleId: z.string(),
targetVersion: z.number(),
}))
.mutation(async ({ input, ctx }) => {
const result = await rollbackRuleToVersion(
input.ruleId,
input.targetVersion,
(ctx as any).nacUser.id,
(ctx as any).nacUser.email
);
if (result.success) {
await writeAuditLog("ROLLBACK_RULE_VERSION", (ctx as any).nacUser.id, (ctx as any).nacUser.email, {
ruleId: input.ruleId, targetVersion: input.targetVersion, restoredFields: result.restoredFields,
});
}
return result;
}),
// ─── v16: PDF导出报告 ──────────────────────────────────────────
exportReport: nacAuthProcedure
.input(z.object({
jurisdiction: z.string().optional(),
assetType: z.string().optional(),
lang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).default("zh"),
title: z.string().default("NAC合规规则报告"),
includeDisabled: z.boolean().optional().default(false),
excludeJurisdictions: z.array(z.string()).optional().default([]),
}))
.mutation(async ({ input, ctx }) => {
const result = await generateComplianceReport({
jurisdiction: input.jurisdiction,
assetType: input.assetType,
lang: input.lang,
title: input.title,
includeDisabled: input.includeDisabled,
excludeJurisdictions: input.excludeJurisdictions,
});
await writeAuditLog("EXPORT_REPORT", (ctx as any).nacUser.id, (ctx as any).nacUser.email, {
jurisdiction: input.jurisdiction, assetType: input.assetType, lang: input.lang,
ruleCount: result.ruleCount, fileSize: result.fileSize,
});
return result;
}),
// ─── v14: 预计算向量缓存(管理员触发) ───────────────────────
precomputeEmbeddings: nacAdminProcedure
.mutation(async ({ ctx }) => {
const result = await precomputeEmbeddings("zh");
await writeAuditLog("PRECOMPUTE_EMBEDDINGS", (ctx as any).nacUser.id, (ctx as any).nacUser.email, result);
return result;
}),
// // ─── v17: 监管动态获取 ────────────────────────────
getRegulatoryUpdates: nacAuthProcedure
.input(z.object({
jurisdiction: z.string().optional(),
status: z.string().optional(),
limit: z.number().optional().default(50),
}))
.query(async ({ input }) => {
return getRegulatoryUpdates(input);
}),
// ─── v17: 触发监管数据抓取 ──────────────────────────
fetchRegulatoryUpdates: nacAdminProcedure
.input(z.object({ jurisdiction: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
const result = await fetchRegulatoryUpdates(input.jurisdiction);
await writeAuditLog("FETCH_REGULATORY_UPDATES", (ctx as any).nacUser.id, (ctx as any).nacUser.email, result);
return result;
}),
// ─── v17: 应用规则更新建议 ──────────────────────────
applyRuleUpdateSuggestion: nacAdminProcedure
.input(z.object({
updateId: z.string(),
suggestionIndex: z.number(),
}))
.mutation(async ({ input, ctx }) => {
return applyRuleUpdateSuggestion(input.updateId, input.suggestionIndex, (ctx as any).nacUser.email);
}),
// ─── v17: 忽略监管更新 ───────────────────────────────
dismissRegulatoryUpdate: nacAdminProcedure
.input(z.object({ updateId: z.string() }))
.mutation(async ({ input }) => {
await dismissRegulatoryUpdate(input.updateId);
return { success: true };
}),
// ─── v19: 冲突检测 ──────────────────────────────────────────────
getConflicts: nacAuthProcedure
.input(z.object({
assetType: z.string().optional(),
severity: z.string().optional(),
jurisdiction: z.string().optional(),
}))
.query(async ({ input }) => {
return getDetectedConflicts({
assetType: input.assetType,
severity: input.severity,
jurisdictionA: input.jurisdiction,
});
}),
runConflictDetection: nacAdminProcedure
.input(z.object({ assetType: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
const conflicts = await detectConflicts(input.assetType);
await writeAuditLog("RUN_CONFLICT_DETECTION", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { count: conflicts.length });
return { count: conflicts.length, conflicts };
}),
getConflictReport: nacAuthProcedure
.input(z.object({ assetType: z.string().optional() }))
.query(async ({ input }) => {
return generateConflictReport(input.assetType);
}),
// ─── v20: 一键上链前置合规验证 ──────────────────────────────────
preChainValidation: nacAuthProcedure
.input(z.object({
assetType: z.string(),
jurisdiction: z.string(),
walletAddress: z.string().optional(),
assetValue: z.number().optional(),
assetName: z.string(),
}))
.mutation(async ({ input }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const rules = await db.collection(COLLECTIONS.COMPLIANCE_RULES)
.find({ jurisdiction: input.jurisdiction, assetType: input.assetType, status: "active" })
.toArray();
const conflicts = await getDetectedConflicts({ assetType: input.assetType });
const relevantConflicts = conflicts.filter(
c => c.jurisdictionA === input.jurisdiction || c.jurisdictionB === input.jurisdiction
);
let score = 100;
const issues: string[] = [];
const warnings: string[] = [];
const checklist: Array<{ item: string; status: "pass" | "warn" | "fail"; detail: string }> = [];
if (rules.length === 0) {
score -= 30;
issues.push(`该辖区(${input.jurisdiction})尚无${input.assetType}类资产的合规规则,建议先建立合规框架`);
checklist.push({ item: "合规规则覆盖", status: "fail", detail: "无对应辖区规则" });
} else {
checklist.push({ item: "合规规则覆盖", status: "pass", detail: `已找到 ${rules.length} 条适用规则` });
}
const criticalConflicts = relevantConflicts.filter(c => c.severity === "critical");
if (criticalConflicts.length > 0) {
score -= 40;
issues.push(`存在 ${criticalConflicts.length} 个紧急跨辖区冲突,需要法务审查`);
checklist.push({ item: "冲突检查", status: "fail", detail: `${criticalConflicts.length} 个紧急冲突` });
} else if (relevantConflicts.length > 0) {
score -= 15;
warnings.push(`存在 ${relevantConflicts.length} 个一般冲突,建议关注`);
checklist.push({ item: "冲突检查", status: "warn", detail: `${relevantConflicts.length} 个一般冲突` });
} else {
checklist.push({ item: "冲突检查", status: "pass", detail: "无已知冲突" });
}
if (input.walletAddress) {
const isValidNacAddress = /^NAC[A-Za-z0-9]{40,}$/.test(input.walletAddress);
if (!isValidNacAddress) {
score -= 10;
warnings.push("钱包地址格式不符合NAC标准建议使用NAC原生钱包");
checklist.push({ item: "NAC钱包地址", status: "warn", detail: "地址格式待验证" });
} else {
checklist.push({ item: "NAC钱包地址", status: "pass", detail: "地址格式正确" });
}
} else {
warnings.push("未提供钱包地址,上链后无法接收代币权益");
checklist.push({ item: "NAC钱包地址", status: "warn", detail: "未提供钱包地址" });
}
if (input.assetValue && input.assetValue > 0) {
checklist.push({ item: "资产价值评估", status: "pass", detail: `资产价值: $${input.assetValue.toLocaleString()}` });
} else {
warnings.push("未提供资产价值建议先完成AI估值再上链");
checklist.push({ item: "资产价值评估", status: "warn", detail: "建议先完成AI估值" });
}
const exchangeChecklist = [
{ exchange: "NAC DEX", requirements: ["合规评分≥ 70", "KYC已完成"], eligible: score >= 70 },
{ exchange: "NAC合规交易所", requirements: ["合规评分≥ 85", "KYC已完成", "无紧急冲突"], eligible: score >= 85 && criticalConflicts.length === 0 },
{ exchange: "主流CEX上市", requirements: ["合规评分≥ 95", "全部冲突已解决", "法务意见书"], eligible: score >= 95 && relevantConflicts.length === 0 },
];
return {
score: Math.max(0, score),
status: score >= 85 ? "ready" : score >= 60 ? "warning" : "blocked",
rules: rules.map((r: any) => ({ id: r._id?.toString(), name: r.ruleName, assetType: r.assetType })),
conflicts: relevantConflicts,
checklist,
issues,
warnings,
exchangeChecklist,
recommendation: score >= 85 ? "合规评分达标,可以进行上链" : score >= 60 ? "存在风险项,建议先解决警告再上链" : "存在严重合规问题,不建议上链",
};
}),
// ─── 获取知识库统计 ─────────────────────────────
stats: nacAuthProcedure
.query(async () => {
const db = await getMongoDb();
if (!db) return { total: 0, byJurisdiction: [], byAssetType: [], byStatus: [] };
const [total, byJurisdiction, byAssetType, byStatus] = await Promise.all([
db.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(),
db.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([
{ $group: { _id: "$jurisdiction", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
]).toArray(),
db.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([
{ $group: { _id: "$assetType", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
]).toArray(),
db.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([
{ $group: { _id: "$status", count: { $sum: 1 } } },
]).toArray(),
]);
return { total, byJurisdiction, byAssetType, byStatus };
}),
}),
// ─── 采集器监控与管理 ────────────────────────────────────────────
crawler: router({
list: nacAuthProcedure.query(async () => {
const db = await getMongoDb();
if (!db) return [];
return db.collection(COLLECTIONS.CRAWLERS).find({}).sort({ createdAt: -1 }).toArray();
}),
logs: nacAuthProcedure
.input(z.object({ crawlerId: z.string().optional(), limit: z.number().default(50) }))
.query(async ({ input }) => {
const db = await getMongoDb();
if (!db) return [];
const filter: Record<string, unknown> = {};
if (input.crawlerId) filter.crawlerId = input.crawlerId;
return db.collection(COLLECTIONS.CRAWLER_LOGS).find(filter).sort({ timestamp: -1 }).limit(input.limit).toArray();
}),
trigger: nacAdminProcedure
.input(z.object({ crawlerId: z.string() }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const crawler = await db.collection(COLLECTIONS.CRAWLERS).findOne({ _id: new ObjectId(input.crawlerId) });
if (!crawler) throw new TRPCError({ code: "NOT_FOUND", message: "采集器不存在" });
await db.collection(COLLECTIONS.CRAWLER_LOGS).insertOne({ crawlerId: input.crawlerId, crawlerName: crawler.name, action: "manual_trigger", status: "triggered", message: "管理员手动触发采集任务", timestamp: new Date() });
await db.collection(COLLECTIONS.CRAWLERS).updateOne({ _id: new ObjectId(input.crawlerId) }, { $set: { lastRun: new Date() } });
await writeAuditLog("TRIGGER_CRAWLER", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { crawlerId: input.crawlerId, crawlerName: crawler.name });
return { success: true, message: `采集器 "${crawler.name}" 已触发` };
}),
create: nacAdminProcedure
.input(z.object({ name: z.string(), jurisdiction: z.string(), type: z.enum(["internal", "external"]), source: z.string(), category: z.string(), frequency: z.string() }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const result = await db.collection(COLLECTIONS.CRAWLERS).insertOne({ ...input, status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() });
await writeAuditLog("CREATE_CRAWLER", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { crawlerName: input.name });
return { id: result.insertedId };
}),
}),
// ─── AI审批案例审查 ──────────────────────────────────────────────
approvalCase: router({
list: nacAuthProcedure
.input(z.object({ status: z.string().optional(), riskLevel: z.string().optional(), page: z.number().default(1), pageSize: z.number().default(20) }))
.query(async ({ input }) => {
const db = await getMongoDb();
if (!db) return { items: [], total: 0 };
const filter: Record<string, unknown> = {};
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([
db.collection(COLLECTIONS.APPROVAL_CASES).find(filter).sort({ createdAt: -1 }).skip(skip).limit(input.pageSize).toArray(),
db.collection(COLLECTIONS.APPROVAL_CASES).countDocuments(filter),
]);
return { items, total };
}),
review: nacAuthProcedure
.input(z.object({ id: z.string(), decision: z.enum(["approved", "rejected"]), comment: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const nacUser = (ctx as any).nacUser;
await db.collection(COLLECTIONS.APPROVAL_CASES).updateOne(
{ _id: new ObjectId(input.id) },
{ $set: { status: "reviewed", decision: input.decision, reviewComment: input.comment, reviewedBy: nacUser.email, reviewedAt: 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 db = await getMongoDb();
if (!db) return [];
return db.collection(COLLECTIONS.TAG_RULES).find({}).sort({ createdAt: -1 }).toArray();
}),
correctTag: nacAuthProcedure
.input(z.object({ documentId: z.string(), originalTags: z.array(z.string()), correctedTags: z.array(z.string()), reason: z.string() }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const nacUser = (ctx as any).nacUser;
await db.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: new Date() });
await writeAuditLog("CORRECT_TAG", nacUser.id, nacUser.email, { documentId: input.documentId });
return { success: true };
}),
createRule: nacAdminProcedure
.input(z.object({ keyword: z.string(), tags: z.array(z.string()), dimension: z.string(), description: z.string() }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const result = await db.collection(COLLECTIONS.TAG_RULES).insertOne({ ...input, type: "rule", status: "active", createdAt: new Date() });
await writeAuditLog("CREATE_TAG_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { keyword: input.keyword });
return { id: result.insertedId };
}),
}),
// ─── 协议族注册表管理 ────────────────────────────────────────────
protocolRegistry: router({
list: nacAuthProcedure.query(async () => {
const db = await getMongoDb();
if (!db) return [];
return db.collection(COLLECTIONS.PROTOCOL_REGISTRY).find({}).sort({ createdAt: -1 }).toArray();
}),
register: nacAdminProcedure
.input(z.object({ name: z.string(), type: z.string(), version: z.string(), endpoint: z.string(), trigger: z.string(), description: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const result = await db.collection(COLLECTIONS.PROTOCOL_REGISTRY).insertOne({ ...input, status: "active", createdAt: new Date() });
await writeAuditLog("REGISTER_PROTOCOL", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { protocolName: input.name });
return { id: result.insertedId };
}),
toggleStatus: nacAdminProcedure
.input(z.object({ id: z.string(), status: z.enum(["active", "disabled", "deprecated"]) }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
await db.collection(COLLECTIONS.PROTOCOL_REGISTRY).updateOne({ _id: new ObjectId(input.id) }, { $set: { status: input.status, updatedAt: new Date() } });
await writeAuditLog("TOGGLE_PROTOCOL", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { protocolId: input.id, newStatus: input.status });
return { success: true };
}),
updateVersion: nacAdminProcedure
.input(z.object({ id: z.string(), version: z.string(), trigger: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
const db = await getMongoDb();
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
const update: Record<string, unknown> = { version: input.version, updatedAt: new Date() };
if (input.trigger) update.trigger = input.trigger;
await db.collection(COLLECTIONS.PROTOCOL_REGISTRY).updateOne({ _id: new ObjectId(input.id) }, { $set: update });
await writeAuditLog("UPDATE_PROTOCOL_VERSION", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { protocolId: input.id, version: input.version });
return { success: true };
}),
}),
// ─── AI智能体系统 ─────────────────────────────────────────────────
aiAgent: router({
// 获取所有Agent列表
list: nacAuthProcedure
.query(() => {
return {
agents: AGENT_REGISTRY,
configured: isAgentConfigured(),
configHint: isAgentConfigured()
? null
: "请在服务器 .env 中配置 NAC_AI_API_URL 和 NAC_AI_API_KEY推荐阿里云通义千问",
};
}),
// 与Agent对话支持会话持久化
chat: nacAuthProcedure
.input(z.object({
agentType: z.enum(["knowledge_qa", "compliance", "translation", "approval_assist"]),
userMessage: z.string().min(1).max(4000),
conversationId: z.string().optional(), // 传入则续接历史会话
conversationHistory: z.array(z.object({
role: z.enum(["system", "user", "assistant"]),
content: z.string(),
})).optional().default([]),
context: z.record(z.string(), z.unknown()).optional(),
persistHistory: z.boolean().optional().default(true), // 是否持久化到MongoDB
}))
.mutation(async ({ input, ctx }) => {
const nacUser = (ctx as any).nacUser;
const userId = nacUser?.id || 0;
const userEmail = nacUser?.email || "unknown";
// 如果传入了conversationId从数据库加载历史消息
let historyMessages: AgentMessage[] = input.conversationHistory as AgentMessage[];
let convId = input.conversationId;
if (convId && input.persistHistory) {
try {
const dbMessages = await loadConversationMessages(convId, userId, 20);
if (dbMessages.length > 0) {
historyMessages = messagesToAgentHistory(dbMessages) as AgentMessage[];
}
} catch (e) {
console.warn("[aiAgent.chat] 加载历史失败:", (e as Error).message);
}
}
// 运行Agent
const response = await runAgent({
agentType: input.agentType as AgentType,
userMessage: input.userMessage,
conversationHistory: historyMessages,
context: input.context,
});
// 持久化对话历史到MongoDB
if (input.persistHistory) {
try {
// 如果没有会话,创建新会话
if (!convId) {
convId = await createConversation(
userId,
userEmail,
input.agentType as AgentType,
input.userMessage
);
}
// 保存消息对
await saveMessagePair(
convId,
input.userMessage,
response.message,
response.confidence,
response.sources,
response.suggestions
);
} catch (e) {
console.warn("[aiAgent.chat] 对话历史持久化失败:", (e as Error).message);
}
}
await writeAuditLog("AGENT_CHAT", userId, userEmail, {
agentType: input.agentType,
conversationId: convId,
messageLength: input.userMessage.length,
confidence: response.confidence,
});
return { ...response, conversationId: convId };
}),
// 获取用户的会话列表
listConversations: nacAuthProcedure
.input(z.object({
agentType: z.enum(["knowledge_qa", "compliance", "translation", "approval_assist"]).optional(),
limit: z.number().min(1).max(50).default(20),
skip: z.number().min(0).default(0),
}))
.query(async ({ input, ctx }) => {
const userId = (ctx as any).nacUser?.id || 0;
return await listConversations(userId, input.agentType as AgentType | undefined, input.limit, input.skip);
}),
// 加载单个会话的历史消息
loadHistory: nacAuthProcedure
.input(z.object({
conversationId: z.string(),
limit: z.number().min(1).max(100).default(50),
}))
.query(async ({ input, ctx }) => {
const userId = (ctx as any).nacUser?.id || 0;
const [conv, messages] = await Promise.all([
getConversation(input.conversationId, userId),
loadConversationMessages(input.conversationId, userId, input.limit),
]);
if (!conv) throw new TRPCError({ code: "NOT_FOUND", message: "会话不存在" });
return { conversation: conv, messages };
}),
// 删除会话
deleteConversation: nacAuthProcedure
.input(z.object({ conversationId: z.string() }))
.mutation(async ({ input, ctx }) => {
const userId = (ctx as any).nacUser?.id || 0;
const deleted = await deleteConversation(input.conversationId, userId);
if (!deleted) throw new TRPCError({ code: "NOT_FOUND", message: "会话不存在或无权限删除" });
return { success: true };
}),
// 检查AI服务状态
status: nacAuthProcedure
.query(() => ({
configured: isAgentConfigured(),
apiUrl: process.env.NAC_AI_API_URL ? "已配置" : "未配置",
model: process.env.NAC_AI_MODEL || "qwen-plus默认",
})),
}),
// ─── 案例库归档管理 ───────────────────────────────────────────────
archive: router({
// 试运行归档(统计数量,不实际迁移)
dryRun: nacAdminProcedure
.query(async () => {
return await runArchive(true);
}),
// 执行归档
run: nacAdminProcedure
.mutation(async ({ ctx }) => {
await writeAuditLog("RUN_ARCHIVE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { trigger: "manual" });
return await runArchive(false);
}),
// 归档历史记录
logs: nacAdminProcedure
.input(z.object({ limit: z.number().default(20) }))
.query(async ({ input }) => {
return await getArchiveLogs(input.limit);
}),
// 查询归档案例
listArchived: nacAdminProcedure
.input(z.object({
page: z.number().default(1),
pageSize: z.number().default(20),
jurisdiction: z.string().optional(),
status: z.string().optional(),
}))
.query(async ({ input }) => {
return await getArchivedCases(input.page, input.pageSize, {
jurisdiction: input.jurisdiction,
status: input.status,
});
}),
}),
// ─── 告警通知管理 ─────────────────────────────────────────────────
notification: router({
// 获取Webhook配置状态
webhookStatus: nacAdminProcedure
.query(() => getWebhookStatus()),
// 发送测试通知
test: nacAdminProcedure
.input(z.object({
channel: z.enum(["wecom", "dingtalk", "feishu", "generic"]),
message: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
const { notifyOwner } = await import("./_core/notification");
const result = await notifyOwner({
title: `NAC告警测试 - ${input.channel}`,
content: input.message || `这是来自NAC Knowledge Engine Admin的测试通知。\n\n发送时间${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}\n发送人${(ctx as any).nacUser?.email}`,
level: "info",
module: "test",
});
await writeAuditLog("TEST_NOTIFICATION", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { channel: input.channel });
return { success: result };
}),
// 模拟采集器告警(测试用)
testCrawlerAlert: nacAdminProcedure
.mutation(async ({ ctx }) => {
await notifyCrawlerError("CN-CSRC法规采集器", "连接超时http://www.csrc.gov.cn 响应时间超过30秒");
await writeAuditLog("TEST_CRAWLER_ALERT", (ctx as any).nacUser.id, (ctx as any).nacUser.email, {});
return { success: true };
}),
}),
// ─── 权限与审计管理 ──────────────────────────────────────────────
rbac: router({
listUsers: nacAdminProcedure
.input(z.object({ page: z.number().default(1), pageSize: z.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(z.object({ action: z.string().optional(), userId: z.number().optional(), page: z.number().default(1), pageSize: z.number().default(50) }))
.query(async ({ input }) => {
const db = await getMongoDb();
if (!db) return { items: [], total: 0 };
const filter: Record<string, unknown> = {};
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([
db.collection(COLLECTIONS.AUDIT_LOGS).find(filter).sort({ timestamp: -1 }).skip(skip).limit(input.pageSize).toArray(),
db.collection(COLLECTIONS.AUDIT_LOGS).countDocuments(filter),
]);
return { items, total };
}),
}),
// ─── 数据库管理 ────────────────────────────────────────────────
dbAdmin: router({
// 初始化MongoDB索引全文索引+TTL索引
initIndexes: nacAdminProcedure
.mutation(async ({ ctx }) => {
const result = await initMongoIndexes();
await writeAuditLog("INIT_MONGO_INDEXES", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { summary: result.summary });
return result;
}),
// 查询当前索引状态
indexStatus: nacAdminProcedure
.query(async () => {
const db = await getMongoDb();
if (!db) return { collections: [] };
const collections = [
COLLECTIONS.COMPLIANCE_RULES,
COLLECTIONS.AGENT_CONVERSATIONS,
"knowledge_base",
];
const status = await Promise.all(
collections.map(async (colName) => {
try {
const col = db.collection(colName);
const indexes = await col.listIndexes().toArray();
return {
collection: colName,
indexCount: indexes.length,
hasTextIndex: indexes.some(idx => Object.values(idx.key || {}).includes("text")),
hasTTLIndex: indexes.some(idx => idx.expireAfterSeconds !== undefined),
indexes: indexes.map(idx => ({
name: idx.name,
key: idx.key,
ttl: idx.expireAfterSeconds,
})),
};
} catch {
return { collection: colName, indexCount: 0, hasTextIndex: false, hasTTLIndex: false, indexes: [] };
}
})
);
return { collections: status };
}),
}),
});
export type AppRouter = typeof appRouter;