From e450695256bd0f7fe0283baa7603a59c741b19b3 Mon Sep 17 00:00:00 2001 From: nacadmin Date: Sun, 1 Mar 2026 16:21:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=E9=97=AE=E7=AD=94=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E5=9F=9F=E5=90=8D=20chat.newassetchain.io=EF=BC=8CCBPP?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E5=8E=9F=E5=88=99=EF=BC=8C=E7=99=BB=E5=BD=95?= =?UTF-8?q?/=E6=B3=A8=E5=86=8C=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nac-admin/server/routers.ts | 1386 +++++++++++++++++++++++++++++++++++ 1 file changed, 1386 insertions(+) create mode 100644 nac-admin/server/routers.ts diff --git a/nac-admin/server/routers.ts b/nac-admin/server/routers.ts new file mode 100644 index 0000000..f898482 --- /dev/null +++ b/nac-admin/server/routers.ts @@ -0,0 +1,1386 @@ +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"; +import { runFullCrawl as execFullCrawl, runTier1Crawl, getCrawlerStats } from "./regulatoryCrawler"; + +// ─── 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 }; + }), + // 公开接口:获取当前用户信息(未登录返回 null,不抛错) + whoami: publicProcedure.query(async ({ ctx }) => { + const nacUser = (ctx as any).nacUser; + if (!nacUser) return null; + 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 = {}; + 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 = 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 = {}; + let descriptionI18n: Record = {}; + + // 如果启用自动翻译,调用AI生成七语言翻译 + if (input.autoTranslate !== false) { + try { + const translations = await generateRuleTranslations( + input.ruleName, + input.description, + (input.sourceLang || "zh") as SupportedLanguage + ); + ruleNameI18n = translations.ruleNameI18n as Record; + descriptionI18n = translations.descriptionI18n as Record; + } 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 = { ...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, ...ruleNameI18n }; + descriptionI18n = { ...translations.descriptionI18n as Record, ...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 = {}; + 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 }; + }), + runFullCrawl: nacAdminProcedure + .input(z.object({ tier: z.number().optional().default(1), maxSources: z.number().optional().default(10) })) + .mutation(async ({ input }) => { + const results = await execFullCrawl({ tier: input.tier, maxSources: input.maxSources }); + const newRules = results.reduce((s: number, r: any) => s + (r.newRules || 0), 0); + const updatedRules = results.reduce((s: number, r: any) => s + (r.updatedRules || 0), 0); + const errors = results.filter((r: any) => r.error).length; + return { success: true, newRules, updatedRules, sourcesProcessed: results.length, errors }; + }), + }), + + // ─── 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 = {}; + 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 = { 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: publicProcedure + .query(() => { + return { + agents: AGENT_REGISTRY, + configured: isAgentConfigured(), + configHint: isAgentConfigured() + ? null + : "请在服务器 .env 中配置 NAC_AI_API_URL 和 NAC_AI_API_KEY(推荐:阿里云通义千问)", + }; + }), + + // 与Agent对话(支持会话持久化) + chat: publicProcedure + .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: publicProcedure + .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: publicProcedure + .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: publicProcedure + .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: publicProcedure + .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 = {}; + 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 }; + }), + }), + + // ─── 公开知识库问答 API(无需登录,面向所有 NAC 公链用户)───────────── + publicAsk: router({ + query: publicProcedure + .input(z.object({ + question: z.string().min(1).max(500), + language: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).default("zh"), + jurisdiction: z.string().optional(), + assetType: z.string().optional(), + })) + .mutation(async ({ input }) => { + try { + const { nacInfer } = await import("./nacInferenceEngine"); + const result = await nacInfer({ + question: input.question, + language: input.language, + jurisdiction: input.jurisdiction, + assetType: input.assetType, + }); + return { + success: true, + answer: result.answer, + confidence: result.confidence, + intent: result.intent, + sources: result.sources.slice(0, 5), + suggestions: result.suggestions.slice(0, 4), + engineVersion: result.engineVersion, + processingMs: result.processingMs, + }; + } catch (err) { + console.error("[publicAsk.query] 推理失败:", (err as Error).message); + return { + success: false, + answer: "抱歉,知识库暂时无法响应,请稍后再试。", + confidence: 0, + intent: "general_nac" as const, + sources: [], + suggestions: [], + engineVersion: "NAC-Inference-v1.0", + processingMs: 0, + }; + } + }), + + hotQuestions: publicProcedure + .input(z.object({ + language: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).default("zh"), + })) + .query(async ({ input }) => { + const questions: Record = { + zh: [ + "中国大陆房地产上链需要哪些文件?", + "香港 RWA 合规要求是什么?", + "七层合规验证框架是什么?", + "Charter 和 Solidity 有什么区别?", + "XTZH 稳定币如何锚定价值?", + "CBPP 共识协议与 PoS 有什么不同?", + "新加坡基金上链需要什么条件?", + "NVM 虚拟机与 EVM 有什么区别?", + ], + en: [ + "What documents are needed for China mainland real estate tokenization?", + "What are Hong Kong RWA compliance requirements?", + "What is the seven-layer compliance verification framework?", + "How does Charter differ from Solidity?", + "How does XTZH stablecoin maintain its value?", + "How is CBPP consensus different from PoS?", + "What are Singapore fund tokenization requirements?", + "How does NVM differ from EVM?", + ], + }; + return { questions: questions[input.language] || questions.zh }; + }), + + stats: publicProcedure + .query(async () => { + try { + const db = await getMongoDb(); + if (!db) return { ruleCount: 35, jurisdictionCount: 6, languages: 7 }; + const ruleCount = await db.collection("compliance_rules").countDocuments({ status: "active" }); + return { ruleCount, jurisdictionCount: 6, languages: 7, lastUpdated: new Date().toISOString() }; + } catch { + return { ruleCount: 35, jurisdictionCount: 6, languages: 7 }; + } + }), + }), +}); + +export type AppRouter = typeof appRouter; +