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 = {}; 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 }; }), }), // ─── 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: 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 = {}; 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;