1387 lines
71 KiB
TypeScript
1387 lines
71 KiB
TypeScript
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<string, unknown> = {};
|
||
if (input.jurisdiction) filter.jurisdiction = input.jurisdiction;
|
||
if (input.assetType) filter.assetType = input.assetType;
|
||
if (input.status) filter.status = input.status;
|
||
// 全文搜索:优先使用$text索引,降级到正则匹配
|
||
if (input.search) {
|
||
const kw = input.search.trim();
|
||
try {
|
||
// 尝试全文索引搜索
|
||
filter["$text"] = { $search: kw };
|
||
} catch {
|
||
// 降级到正则匹配
|
||
delete filter["$text"];
|
||
const re = new RegExp(kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
||
filter["$or"] = [
|
||
{ ruleName: re },
|
||
{ description: re },
|
||
{ "ruleNameI18n.zh": re },
|
||
{ "ruleNameI18n.en": re },
|
||
];
|
||
}
|
||
}
|
||
const skip = (input.page - 1) * input.pageSize;
|
||
// 全文搜索时按相关性排序,否则按创建时间降序
|
||
const sortOpt: Record<string, 1 | -1> = input.search
|
||
? { score: -1, createdAt: -1 }
|
||
: { createdAt: -1 };
|
||
const projection = input.search ? { score: { $meta: "textScore" } } : {};
|
||
const [items, total] = await Promise.all([
|
||
db.collection(COLLECTIONS.COMPLIANCE_RULES).find(filter, { projection }).sort(sortOpt).skip(skip).limit(input.pageSize).toArray(),
|
||
db.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(filter),
|
||
]);
|
||
// 根据请求语言返回对应翻译
|
||
const lang = input.lang || "zh";
|
||
const localizedItems = items.map((item: any) => ({
|
||
...item,
|
||
displayName: item.ruleNameI18n?.[lang] || item.ruleName,
|
||
displayDescription: item.descriptionI18n?.[lang] || item.description,
|
||
}));
|
||
return { items: localizedItems, total };
|
||
}),
|
||
|
||
create: nacAdminProcedure
|
||
.input(z.object({
|
||
jurisdiction: z.string(),
|
||
assetType: z.string(),
|
||
ruleName: z.string(),
|
||
description: z.string(),
|
||
required: z.boolean(),
|
||
tags: z.array(z.string()),
|
||
sourceLang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).optional(),
|
||
autoTranslate: z.boolean().optional(),
|
||
}))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
|
||
let ruleNameI18n: Record<string, string> = {};
|
||
let descriptionI18n: Record<string, string> = {};
|
||
|
||
// 如果启用自动翻译,调用AI生成七语言翻译
|
||
if (input.autoTranslate !== false) {
|
||
try {
|
||
const translations = await generateRuleTranslations(
|
||
input.ruleName,
|
||
input.description,
|
||
(input.sourceLang || "zh") as SupportedLanguage
|
||
);
|
||
ruleNameI18n = translations.ruleNameI18n as Record<string, string>;
|
||
descriptionI18n = translations.descriptionI18n as Record<string, string>;
|
||
} catch (e) {
|
||
console.error("[KnowledgeBase] Auto-translate failed:", (e as Error).message);
|
||
// 降级:只存源语言
|
||
const lang = input.sourceLang || "zh";
|
||
ruleNameI18n[lang] = input.ruleName;
|
||
descriptionI18n[lang] = input.description;
|
||
}
|
||
} else {
|
||
const lang = input.sourceLang || "zh";
|
||
ruleNameI18n[lang] = input.ruleName;
|
||
descriptionI18n[lang] = input.description;
|
||
}
|
||
|
||
const result = await db.collection(COLLECTIONS.COMPLIANCE_RULES).insertOne({
|
||
jurisdiction: input.jurisdiction,
|
||
assetType: input.assetType,
|
||
ruleName: input.ruleName,
|
||
description: input.description,
|
||
ruleNameI18n,
|
||
descriptionI18n,
|
||
required: input.required,
|
||
tags: input.tags,
|
||
status: "active",
|
||
createdAt: new Date(),
|
||
});
|
||
await writeAuditLog("CREATE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: result.insertedId });
|
||
return { id: result.insertedId };
|
||
}),
|
||
|
||
update: nacAdminProcedure
|
||
.input(z.object({
|
||
id: z.string(),
|
||
data: z.object({
|
||
ruleName: z.string().optional(),
|
||
description: z.string().optional(),
|
||
status: z.enum(["active", "disabled"]).optional(),
|
||
tags: z.array(z.string()).optional(),
|
||
}),
|
||
autoTranslate: z.boolean().optional(),
|
||
}))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
|
||
const updateData: Record<string, unknown> = { ...input.data, updatedAt: new Date() };
|
||
|
||
// 如果更新了名称或描述,重新生成翻译
|
||
if (input.autoTranslate !== false && (input.data.ruleName || input.data.description)) {
|
||
const existing = await db.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ _id: new ObjectId(input.id) }) as any;
|
||
if (existing) {
|
||
try {
|
||
const translations = await generateRuleTranslations(
|
||
input.data.ruleName || existing.ruleName,
|
||
input.data.description || existing.description,
|
||
"zh",
|
||
{ ruleNameI18n: existing.ruleNameI18n, descriptionI18n: existing.descriptionI18n }
|
||
);
|
||
updateData.ruleNameI18n = translations.ruleNameI18n;
|
||
updateData.descriptionI18n = translations.descriptionI18n;
|
||
} catch (e) {
|
||
console.error("[KnowledgeBase] Auto-translate on update failed:", (e as Error).message);
|
||
}
|
||
}
|
||
}
|
||
|
||
// v15: 保存版本快照(更新前先读取旧数据)
|
||
const oldRule = await db.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ _id: new ObjectId(input.id) }) as any;
|
||
if (oldRule) {
|
||
await saveRuleVersion(
|
||
input.id,
|
||
oldRule,
|
||
updateData,
|
||
(ctx as any).nacUser.id,
|
||
(ctx as any).nacUser.email
|
||
).catch(e => console.error("[v15] saveRuleVersion failed:", e.message));
|
||
}
|
||
|
||
await db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne(
|
||
{ _id: new ObjectId(input.id) },
|
||
{ $set: updateData }
|
||
);
|
||
await writeAuditLog("UPDATE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: input.id });
|
||
return { success: true };
|
||
}),
|
||
|
||
toggleStatus: nacAdminProcedure
|
||
.input(z.object({ id: z.string(), status: z.enum(["active", "disabled"]) }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
await db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne({ _id: new ObjectId(input.id) }, { $set: { status: input.status, updatedAt: new Date() } });
|
||
await writeAuditLog("TOGGLE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: input.id, newStatus: input.status });
|
||
return { success: true };
|
||
}),
|
||
|
||
delete: nacAdminProcedure
|
||
.input(z.object({ id: z.string() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
await db.collection(COLLECTIONS.COMPLIANCE_RULES).deleteOne({ _id: new ObjectId(input.id) });
|
||
await writeAuditLog("DELETE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: input.id });
|
||
return { success: true };
|
||
}),
|
||
|
||
// ─── AI辅助翻译接口 ──────────────────────────────────────────
|
||
translateRule: nacAdminProcedure
|
||
.input(z.object({
|
||
id: z.string(),
|
||
targetLang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).optional(),
|
||
}))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
|
||
const rule = await db.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({ _id: new ObjectId(input.id) }) as any;
|
||
if (!rule) throw new TRPCError({ code: "NOT_FOUND", message: "规则不存在" });
|
||
|
||
const translations = await migrateRuleToMultiLang({
|
||
ruleName: rule.ruleName,
|
||
description: rule.description,
|
||
ruleNameI18n: rule.ruleNameI18n,
|
||
descriptionI18n: rule.descriptionI18n,
|
||
});
|
||
|
||
await db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne(
|
||
{ _id: new ObjectId(input.id) },
|
||
{ $set: { ruleNameI18n: translations.ruleNameI18n, descriptionI18n: translations.descriptionI18n, updatedAt: new Date() } }
|
||
);
|
||
|
||
await writeAuditLog("TRANSLATE_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { ruleId: input.id });
|
||
return { success: true, translations };
|
||
}),
|
||
|
||
// ─── 批量迁移现有规则到多语言格式 ────────────────────────────
|
||
migrateAllToMultiLang: nacAdminProcedure
|
||
.mutation(async ({ ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
|
||
// 只迁移没有多语言字段的规则
|
||
const rules = await db.collection(COLLECTIONS.COMPLIANCE_RULES).find({
|
||
$or: [{ ruleNameI18n: { $exists: false } }, { "ruleNameI18n.en": { $exists: false } }]
|
||
}).toArray() as any[];
|
||
|
||
let migrated = 0;
|
||
for (const rule of rules) {
|
||
try {
|
||
const translations = await migrateRuleToMultiLang({
|
||
ruleName: rule.ruleName,
|
||
description: rule.description,
|
||
ruleNameI18n: rule.ruleNameI18n,
|
||
descriptionI18n: rule.descriptionI18n,
|
||
});
|
||
await db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne(
|
||
{ _id: rule._id },
|
||
{ $set: { ruleNameI18n: translations.ruleNameI18n, descriptionI18n: translations.descriptionI18n, updatedAt: new Date() } }
|
||
);
|
||
migrated++;
|
||
} catch (e) {
|
||
console.error(`[Migration] Failed for rule ${rule._id}:`, (e as Error).message);
|
||
}
|
||
}
|
||
|
||
await writeAuditLog("MIGRATE_MULTILANG", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { migratedCount: migrated });
|
||
return { success: true, migratedCount: migrated, totalRules: rules.length };
|
||
}),
|
||
|
||
// // ─── 获取支持的语言列表 ──────────────────────────────
|
||
getSupportedLanguages: nacAuthProcedure.query(() => {
|
||
return SUPPORTED_LANGUAGES.map(lang => ({
|
||
code: lang,
|
||
name: { zh: "中文(简体)", en: "English", ar: "العربية", ja: "日本語", ko: "한국어", fr: "Français", ru: "Русский" }[lang],
|
||
isRTL: isRTL(lang as SupportedLanguage),
|
||
}));
|
||
}),
|
||
|
||
// ─── AI翻译服务状态检查 ───────────────────────────
|
||
aiStatus: nacAuthProcedure.query(() => {
|
||
const configured = isAiTranslationConfigured();
|
||
return {
|
||
configured,
|
||
apiUrl: configured ? (process.env.NAC_AI_API_URL || "").replace(/\/+$/, "") : null,
|
||
model: process.env.NAC_AI_MODEL || "gpt-3.5-turbo",
|
||
message: configured
|
||
? "AI翻译服务已配置,可以使用自动翻译功能"
|
||
: "AI翻译服务未配置。请在服务器 .env 中设置 NAC_AI_API_URL 和 NAC_AI_API_KEY",
|
||
};
|
||
}),
|
||
|
||
// ─── 阿拉伯语RTL专项测试 ────────────────────────────
|
||
testArabicRTL: nacAdminProcedure
|
||
.mutation(async () => {
|
||
const report = await runArabicRTLTests();
|
||
return report;
|
||
}),
|
||
|
||
// ─── 批量导入合规规则 ─────────────────────────────────
|
||
batchImport: nacAdminProcedure
|
||
.input(z.object({
|
||
rules: z.array(z.object({
|
||
jurisdiction: z.string(),
|
||
assetType: z.string(),
|
||
ruleName: z.string(),
|
||
description: z.string(),
|
||
required: z.boolean().optional().default(true),
|
||
tags: z.array(z.string()).optional().default([]),
|
||
ruleNameI18n: z.record(z.string(), z.string()).optional(),
|
||
descriptionI18n: z.record(z.string(), z.string()).optional(),
|
||
})),
|
||
skipDuplicates: z.boolean().optional().default(true),
|
||
}))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "数据库连接失败" });
|
||
|
||
let imported = 0;
|
||
let skipped = 0;
|
||
let failed = 0;
|
||
const errors: string[] = [];
|
||
|
||
for (const rule of input.rules) {
|
||
try {
|
||
// 去重检查
|
||
if (input.skipDuplicates) {
|
||
const exists = await db.collection(COLLECTIONS.COMPLIANCE_RULES).findOne({
|
||
jurisdiction: rule.jurisdiction,
|
||
ruleName: rule.ruleName,
|
||
});
|
||
if (exists) {
|
||
skipped++;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 如果没有提供多语言字段,尝试AI翻译
|
||
let ruleNameI18n = rule.ruleNameI18n || {};
|
||
let descriptionI18n = rule.descriptionI18n || {};
|
||
|
||
if (!ruleNameI18n.en || !descriptionI18n.en) {
|
||
try {
|
||
const translations = await generateRuleTranslations(
|
||
rule.ruleName,
|
||
rule.description,
|
||
"zh"
|
||
);
|
||
ruleNameI18n = { ...translations.ruleNameI18n as Record<string, string>, ...ruleNameI18n };
|
||
descriptionI18n = { ...translations.descriptionI18n as Record<string, string>, ...descriptionI18n };
|
||
} catch {
|
||
// AI翻译失败,使用源语言
|
||
ruleNameI18n = { zh: rule.ruleName, ...ruleNameI18n };
|
||
descriptionI18n = { zh: rule.description, ...descriptionI18n };
|
||
}
|
||
}
|
||
|
||
await db.collection(COLLECTIONS.COMPLIANCE_RULES).insertOne({
|
||
jurisdiction: rule.jurisdiction,
|
||
assetType: rule.assetType,
|
||
ruleName: rule.ruleName,
|
||
description: rule.description,
|
||
ruleNameI18n,
|
||
descriptionI18n,
|
||
required: rule.required ?? true,
|
||
tags: rule.tags ?? [],
|
||
status: "active",
|
||
createdAt: new Date(),
|
||
});
|
||
imported++;
|
||
} catch (e) {
|
||
failed++;
|
||
errors.push(`[${rule.jurisdiction}] ${rule.ruleName}: ${(e as Error).message}`);
|
||
}
|
||
}
|
||
|
||
await writeAuditLog("BATCH_IMPORT_RULES", (ctx as any).nacUser.id, (ctx as any).nacUser.email, {
|
||
total: input.rules.length, imported, skipped, failed,
|
||
});
|
||
|
||
return { success: true, imported, skipped, failed, errors };
|
||
}),
|
||
|
||
// ─── v14: AI语义检索 ──────────────────────────────────────────
|
||
semanticSearch: nacAuthProcedure
|
||
.input(z.object({
|
||
query: z.string().min(1).max(500),
|
||
jurisdiction: z.string().optional(),
|
||
assetType: z.string().optional(),
|
||
limit: z.number().min(1).max(50).default(10),
|
||
lang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).default("zh"),
|
||
minScore: z.number().min(0).max(1).default(0.45),
|
||
}))
|
||
.query(async ({ input }) => {
|
||
const result = await semanticSearch(input.query, {
|
||
jurisdiction: input.jurisdiction,
|
||
assetType: input.assetType,
|
||
limit: input.limit,
|
||
lang: input.lang,
|
||
minScore: input.minScore,
|
||
});
|
||
return result;
|
||
}),
|
||
|
||
// ─── v15: 规则版本历史 ──────────────────────────────────────────
|
||
getVersionHistory: nacAuthProcedure
|
||
.input(z.object({
|
||
ruleId: z.string(),
|
||
limit: z.number().default(20),
|
||
}))
|
||
.query(async ({ input }) => {
|
||
const versions = await getRuleVersionHistory(input.ruleId, input.limit);
|
||
return versions;
|
||
}),
|
||
|
||
compareVersions: nacAuthProcedure
|
||
.input(z.object({
|
||
ruleId: z.string(),
|
||
versionA: z.number(),
|
||
versionB: z.number(),
|
||
}))
|
||
.query(async ({ input }) => {
|
||
return compareRuleVersions(input.ruleId, input.versionA, input.versionB);
|
||
}),
|
||
|
||
rollbackVersion: nacAdminProcedure
|
||
.input(z.object({
|
||
ruleId: z.string(),
|
||
targetVersion: z.number(),
|
||
}))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const result = await rollbackRuleToVersion(
|
||
input.ruleId,
|
||
input.targetVersion,
|
||
(ctx as any).nacUser.id,
|
||
(ctx as any).nacUser.email
|
||
);
|
||
if (result.success) {
|
||
await writeAuditLog("ROLLBACK_RULE_VERSION", (ctx as any).nacUser.id, (ctx as any).nacUser.email, {
|
||
ruleId: input.ruleId, targetVersion: input.targetVersion, restoredFields: result.restoredFields,
|
||
});
|
||
}
|
||
return result;
|
||
}),
|
||
|
||
// ─── v16: PDF导出报告 ──────────────────────────────────────────
|
||
exportReport: nacAuthProcedure
|
||
.input(z.object({
|
||
jurisdiction: z.string().optional(),
|
||
assetType: z.string().optional(),
|
||
lang: z.enum(["zh", "en", "ar", "ja", "ko", "fr", "ru"]).default("zh"),
|
||
title: z.string().default("NAC合规规则报告"),
|
||
includeDisabled: z.boolean().optional().default(false),
|
||
excludeJurisdictions: z.array(z.string()).optional().default([]),
|
||
}))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const result = await generateComplianceReport({
|
||
jurisdiction: input.jurisdiction,
|
||
assetType: input.assetType,
|
||
lang: input.lang,
|
||
title: input.title,
|
||
includeDisabled: input.includeDisabled,
|
||
excludeJurisdictions: input.excludeJurisdictions,
|
||
});
|
||
await writeAuditLog("EXPORT_REPORT", (ctx as any).nacUser.id, (ctx as any).nacUser.email, {
|
||
jurisdiction: input.jurisdiction, assetType: input.assetType, lang: input.lang,
|
||
ruleCount: result.ruleCount, fileSize: result.fileSize,
|
||
});
|
||
return result;
|
||
}),
|
||
|
||
// ─── v14: 预计算向量缓存(管理员触发) ───────────────────────
|
||
precomputeEmbeddings: nacAdminProcedure
|
||
.mutation(async ({ ctx }) => {
|
||
const result = await precomputeEmbeddings("zh");
|
||
await writeAuditLog("PRECOMPUTE_EMBEDDINGS", (ctx as any).nacUser.id, (ctx as any).nacUser.email, result);
|
||
return result;
|
||
}),
|
||
|
||
// // ─── v17: 监管动态获取 ────────────────────────────
|
||
getRegulatoryUpdates: nacAuthProcedure
|
||
.input(z.object({
|
||
jurisdiction: z.string().optional(),
|
||
status: z.string().optional(),
|
||
limit: z.number().optional().default(50),
|
||
}))
|
||
.query(async ({ input }) => {
|
||
return getRegulatoryUpdates(input);
|
||
}),
|
||
|
||
// ─── v17: 触发监管数据抓取 ──────────────────────────
|
||
fetchRegulatoryUpdates: nacAdminProcedure
|
||
.input(z.object({ jurisdiction: z.string().optional() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const result = await fetchRegulatoryUpdates(input.jurisdiction);
|
||
await writeAuditLog("FETCH_REGULATORY_UPDATES", (ctx as any).nacUser.id, (ctx as any).nacUser.email, result);
|
||
return result;
|
||
}),
|
||
|
||
// ─── v17: 应用规则更新建议 ──────────────────────────
|
||
applyRuleUpdateSuggestion: nacAdminProcedure
|
||
.input(z.object({
|
||
updateId: z.string(),
|
||
suggestionIndex: z.number(),
|
||
}))
|
||
.mutation(async ({ input, ctx }) => {
|
||
return applyRuleUpdateSuggestion(input.updateId, input.suggestionIndex, (ctx as any).nacUser.email);
|
||
}),
|
||
|
||
// ─── v17: 忽略监管更新 ───────────────────────────────
|
||
dismissRegulatoryUpdate: nacAdminProcedure
|
||
.input(z.object({ updateId: z.string() }))
|
||
.mutation(async ({ input }) => {
|
||
await dismissRegulatoryUpdate(input.updateId);
|
||
return { success: true };
|
||
}),
|
||
|
||
|
||
// ─── v19: 冲突检测 ──────────────────────────────────────────────
|
||
getConflicts: nacAuthProcedure
|
||
.input(z.object({
|
||
assetType: z.string().optional(),
|
||
severity: z.string().optional(),
|
||
jurisdiction: z.string().optional(),
|
||
}))
|
||
.query(async ({ input }) => {
|
||
return getDetectedConflicts({
|
||
assetType: input.assetType,
|
||
severity: input.severity,
|
||
jurisdictionA: input.jurisdiction,
|
||
});
|
||
}),
|
||
|
||
runConflictDetection: nacAdminProcedure
|
||
.input(z.object({ assetType: z.string().optional() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const conflicts = await detectConflicts(input.assetType);
|
||
await writeAuditLog("RUN_CONFLICT_DETECTION", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { count: conflicts.length });
|
||
return { count: conflicts.length, conflicts };
|
||
}),
|
||
|
||
getConflictReport: nacAuthProcedure
|
||
.input(z.object({ assetType: z.string().optional() }))
|
||
.query(async ({ input }) => {
|
||
return generateConflictReport(input.assetType);
|
||
}),
|
||
|
||
// ─── v20: 一键上链前置合规验证 ──────────────────────────────────
|
||
preChainValidation: nacAuthProcedure
|
||
.input(z.object({
|
||
assetType: z.string(),
|
||
jurisdiction: z.string(),
|
||
walletAddress: z.string().optional(),
|
||
assetValue: z.number().optional(),
|
||
assetName: z.string(),
|
||
}))
|
||
.mutation(async ({ input }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
|
||
const rules = await db.collection(COLLECTIONS.COMPLIANCE_RULES)
|
||
.find({ jurisdiction: input.jurisdiction, assetType: input.assetType, status: "active" })
|
||
.toArray();
|
||
|
||
const conflicts = await getDetectedConflicts({ assetType: input.assetType });
|
||
const relevantConflicts = conflicts.filter(
|
||
c => c.jurisdictionA === input.jurisdiction || c.jurisdictionB === input.jurisdiction
|
||
);
|
||
|
||
let score = 100;
|
||
const issues: string[] = [];
|
||
const warnings: string[] = [];
|
||
const checklist: Array<{ item: string; status: "pass" | "warn" | "fail"; detail: string }> = [];
|
||
|
||
if (rules.length === 0) {
|
||
score -= 30;
|
||
issues.push(`该辖区(${input.jurisdiction})尚无${input.assetType}类资产的合规规则,建议先建立合规框架`);
|
||
checklist.push({ item: "合规规则覆盖", status: "fail", detail: "无对应辖区规则" });
|
||
} else {
|
||
checklist.push({ item: "合规规则覆盖", status: "pass", detail: `已找到 ${rules.length} 条适用规则` });
|
||
}
|
||
|
||
const criticalConflicts = relevantConflicts.filter(c => c.severity === "critical");
|
||
if (criticalConflicts.length > 0) {
|
||
score -= 40;
|
||
issues.push(`存在 ${criticalConflicts.length} 个紧急跨辖区冲突,需要法务审查`);
|
||
checklist.push({ item: "冲突检查", status: "fail", detail: `${criticalConflicts.length} 个紧急冲突` });
|
||
} else if (relevantConflicts.length > 0) {
|
||
score -= 15;
|
||
warnings.push(`存在 ${relevantConflicts.length} 个一般冲突,建议关注`);
|
||
checklist.push({ item: "冲突检查", status: "warn", detail: `${relevantConflicts.length} 个一般冲突` });
|
||
} else {
|
||
checklist.push({ item: "冲突检查", status: "pass", detail: "无已知冲突" });
|
||
}
|
||
|
||
if (input.walletAddress) {
|
||
const isValidNacAddress = /^NAC[A-Za-z0-9]{40,}$/.test(input.walletAddress);
|
||
if (!isValidNacAddress) {
|
||
score -= 10;
|
||
warnings.push("钱包地址格式不符合NAC标准,建议使用NAC原生钱包");
|
||
checklist.push({ item: "NAC钱包地址", status: "warn", detail: "地址格式待验证" });
|
||
} else {
|
||
checklist.push({ item: "NAC钱包地址", status: "pass", detail: "地址格式正确" });
|
||
}
|
||
} else {
|
||
warnings.push("未提供钱包地址,上链后无法接收代币权益");
|
||
checklist.push({ item: "NAC钱包地址", status: "warn", detail: "未提供钱包地址" });
|
||
}
|
||
|
||
if (input.assetValue && input.assetValue > 0) {
|
||
checklist.push({ item: "资产价值评估", status: "pass", detail: `资产价值: $${input.assetValue.toLocaleString()}` });
|
||
} else {
|
||
warnings.push("未提供资产价值,建议先完成AI估值再上链");
|
||
checklist.push({ item: "资产价值评估", status: "warn", detail: "建议先完成AI估值" });
|
||
}
|
||
|
||
const exchangeChecklist = [
|
||
{ exchange: "NAC DEX", requirements: ["合规评分≥ 70", "KYC已完成"], eligible: score >= 70 },
|
||
{ exchange: "NAC合规交易所", requirements: ["合规评分≥ 85", "KYC已完成", "无紧急冲突"], eligible: score >= 85 && criticalConflicts.length === 0 },
|
||
{ exchange: "主流CEX上市", requirements: ["合规评分≥ 95", "全部冲突已解决", "法务意见书"], eligible: score >= 95 && relevantConflicts.length === 0 },
|
||
];
|
||
|
||
return {
|
||
score: Math.max(0, score),
|
||
status: score >= 85 ? "ready" : score >= 60 ? "warning" : "blocked",
|
||
rules: rules.map((r: any) => ({ id: r._id?.toString(), name: r.ruleName, assetType: r.assetType })),
|
||
conflicts: relevantConflicts,
|
||
checklist,
|
||
issues,
|
||
warnings,
|
||
exchangeChecklist,
|
||
recommendation: score >= 85 ? "合规评分达标,可以进行上链" : score >= 60 ? "存在风险项,建议先解决警告再上链" : "存在严重合规问题,不建议上链",
|
||
};
|
||
}),
|
||
|
||
// ─── 获取知识库统计 ─────────────────────────────
|
||
stats: nacAuthProcedure
|
||
.query(async () => {
|
||
const db = await getMongoDb();
|
||
if (!db) return { total: 0, byJurisdiction: [], byAssetType: [], byStatus: [] };
|
||
|
||
const [total, byJurisdiction, byAssetType, byStatus] = await Promise.all([
|
||
db.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(),
|
||
db.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([
|
||
{ $group: { _id: "$jurisdiction", count: { $sum: 1 } } },
|
||
{ $sort: { count: -1 } },
|
||
]).toArray(),
|
||
db.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([
|
||
{ $group: { _id: "$assetType", count: { $sum: 1 } } },
|
||
{ $sort: { count: -1 } },
|
||
]).toArray(),
|
||
db.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([
|
||
{ $group: { _id: "$status", count: { $sum: 1 } } },
|
||
]).toArray(),
|
||
]);
|
||
|
||
return { total, byJurisdiction, byAssetType, byStatus };
|
||
}),
|
||
}),
|
||
|
||
// ─── 采集器监控与管理 ────────────────────────────────────────────
|
||
crawler: router({
|
||
list: nacAuthProcedure.query(async () => {
|
||
const db = await getMongoDb();
|
||
if (!db) return [];
|
||
return db.collection(COLLECTIONS.CRAWLERS).find({}).sort({ createdAt: -1 }).toArray();
|
||
}),
|
||
|
||
logs: nacAuthProcedure
|
||
.input(z.object({ crawlerId: z.string().optional(), limit: z.number().default(50) }))
|
||
.query(async ({ input }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) return [];
|
||
const filter: Record<string, unknown> = {};
|
||
if (input.crawlerId) filter.crawlerId = input.crawlerId;
|
||
return db.collection(COLLECTIONS.CRAWLER_LOGS).find(filter).sort({ timestamp: -1 }).limit(input.limit).toArray();
|
||
}),
|
||
|
||
trigger: nacAdminProcedure
|
||
.input(z.object({ crawlerId: z.string() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
const crawler = await db.collection(COLLECTIONS.CRAWLERS).findOne({ _id: new ObjectId(input.crawlerId) });
|
||
if (!crawler) throw new TRPCError({ code: "NOT_FOUND", message: "采集器不存在" });
|
||
await db.collection(COLLECTIONS.CRAWLER_LOGS).insertOne({ crawlerId: input.crawlerId, crawlerName: crawler.name, action: "manual_trigger", status: "triggered", message: "管理员手动触发采集任务", timestamp: new Date() });
|
||
await db.collection(COLLECTIONS.CRAWLERS).updateOne({ _id: new ObjectId(input.crawlerId) }, { $set: { lastRun: new Date() } });
|
||
await writeAuditLog("TRIGGER_CRAWLER", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { crawlerId: input.crawlerId, crawlerName: crawler.name });
|
||
return { success: true, message: `采集器 "${crawler.name}" 已触发` };
|
||
}),
|
||
|
||
create: nacAdminProcedure
|
||
.input(z.object({ name: z.string(), jurisdiction: z.string(), type: z.enum(["internal", "external"]), source: z.string(), category: z.string(), frequency: z.string() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
const result = await db.collection(COLLECTIONS.CRAWLERS).insertOne({ ...input, status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: new Date() });
|
||
await writeAuditLog("CREATE_CRAWLER", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { crawlerName: input.name });
|
||
return { id: result.insertedId };
|
||
}),
|
||
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<string, unknown> = {};
|
||
if (input.status) filter.status = input.status;
|
||
if (input.riskLevel) filter.riskLevel = input.riskLevel;
|
||
const skip = (input.page - 1) * input.pageSize;
|
||
const [items, total] = await Promise.all([
|
||
db.collection(COLLECTIONS.APPROVAL_CASES).find(filter).sort({ createdAt: -1 }).skip(skip).limit(input.pageSize).toArray(),
|
||
db.collection(COLLECTIONS.APPROVAL_CASES).countDocuments(filter),
|
||
]);
|
||
return { items, total };
|
||
}),
|
||
|
||
review: nacAuthProcedure
|
||
.input(z.object({ id: z.string(), decision: z.enum(["approved", "rejected"]), comment: z.string().optional() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
const nacUser = (ctx as any).nacUser;
|
||
await db.collection(COLLECTIONS.APPROVAL_CASES).updateOne(
|
||
{ _id: new ObjectId(input.id) },
|
||
{ $set: { status: "reviewed", decision: input.decision, reviewComment: input.comment, reviewedBy: nacUser.email, reviewedAt: new Date() } }
|
||
);
|
||
await writeAuditLog("REVIEW_CASE", nacUser.id, nacUser.email, { caseId: input.id, decision: input.decision });
|
||
return { success: true };
|
||
}),
|
||
}),
|
||
|
||
// ─── 标签与规则引擎治理 ──────────────────────────────────────────
|
||
tagEngine: router({
|
||
listRules: nacAuthProcedure.query(async () => {
|
||
const db = await getMongoDb();
|
||
if (!db) return [];
|
||
return db.collection(COLLECTIONS.TAG_RULES).find({}).sort({ createdAt: -1 }).toArray();
|
||
}),
|
||
|
||
correctTag: nacAuthProcedure
|
||
.input(z.object({ documentId: z.string(), originalTags: z.array(z.string()), correctedTags: z.array(z.string()), reason: z.string() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
const nacUser = (ctx as any).nacUser;
|
||
await db.collection(COLLECTIONS.TAG_RULES).insertOne({ type: "correction", documentId: input.documentId, originalTags: input.originalTags, correctedTags: input.correctedTags, reason: input.reason, correctedBy: nacUser.email, isTrainingData: true, createdAt: new Date() });
|
||
await writeAuditLog("CORRECT_TAG", nacUser.id, nacUser.email, { documentId: input.documentId });
|
||
return { success: true };
|
||
}),
|
||
|
||
createRule: nacAdminProcedure
|
||
.input(z.object({ keyword: z.string(), tags: z.array(z.string()), dimension: z.string(), description: z.string() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
const result = await db.collection(COLLECTIONS.TAG_RULES).insertOne({ ...input, type: "rule", status: "active", createdAt: new Date() });
|
||
await writeAuditLog("CREATE_TAG_RULE", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { keyword: input.keyword });
|
||
return { id: result.insertedId };
|
||
}),
|
||
}),
|
||
|
||
// ─── 协议族注册表管理 ────────────────────────────────────────────
|
||
protocolRegistry: router({
|
||
list: nacAuthProcedure.query(async () => {
|
||
const db = await getMongoDb();
|
||
if (!db) return [];
|
||
return db.collection(COLLECTIONS.PROTOCOL_REGISTRY).find({}).sort({ createdAt: -1 }).toArray();
|
||
}),
|
||
|
||
register: nacAdminProcedure
|
||
.input(z.object({ name: z.string(), type: z.string(), version: z.string(), endpoint: z.string(), trigger: z.string(), description: z.string().optional() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
const result = await db.collection(COLLECTIONS.PROTOCOL_REGISTRY).insertOne({ ...input, status: "active", createdAt: new Date() });
|
||
await writeAuditLog("REGISTER_PROTOCOL", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { protocolName: input.name });
|
||
return { id: result.insertedId };
|
||
}),
|
||
|
||
toggleStatus: nacAdminProcedure
|
||
.input(z.object({ id: z.string(), status: z.enum(["active", "disabled", "deprecated"]) }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
await db.collection(COLLECTIONS.PROTOCOL_REGISTRY).updateOne({ _id: new ObjectId(input.id) }, { $set: { status: input.status, updatedAt: new Date() } });
|
||
await writeAuditLog("TOGGLE_PROTOCOL", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { protocolId: input.id, newStatus: input.status });
|
||
return { success: true };
|
||
}),
|
||
|
||
updateVersion: nacAdminProcedure
|
||
.input(z.object({ id: z.string(), version: z.string(), trigger: z.string().optional() }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
const db = await getMongoDb();
|
||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||
const update: Record<string, unknown> = { version: input.version, updatedAt: new Date() };
|
||
if (input.trigger) update.trigger = input.trigger;
|
||
await db.collection(COLLECTIONS.PROTOCOL_REGISTRY).updateOne({ _id: new ObjectId(input.id) }, { $set: update });
|
||
await writeAuditLog("UPDATE_PROTOCOL_VERSION", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { protocolId: input.id, version: input.version });
|
||
return { success: true };
|
||
}),
|
||
}),
|
||
|
||
// ─── AI智能体系统 ─────────────────────────────────────────────────
|
||
aiAgent: router({
|
||
// 获取所有Agent列表
|
||
list: 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<string, unknown> = {};
|
||
if (input.action) filter.action = input.action;
|
||
if (input.userId) filter.userId = input.userId;
|
||
const skip = (input.page - 1) * input.pageSize;
|
||
const [items, total] = await Promise.all([
|
||
db.collection(COLLECTIONS.AUDIT_LOGS).find(filter).sort({ timestamp: -1 }).skip(skip).limit(input.pageSize).toArray(),
|
||
db.collection(COLLECTIONS.AUDIT_LOGS).countDocuments(filter),
|
||
]);
|
||
return { items, total };
|
||
}),
|
||
}),
|
||
|
||
// ─── 数据库管理 ────────────────────────────────────────────────
|
||
dbAdmin: router({
|
||
// 初始化MongoDB索引(全文索引+TTL索引)
|
||
initIndexes: nacAdminProcedure
|
||
.mutation(async ({ ctx }) => {
|
||
const result = await initMongoIndexes();
|
||
await writeAuditLog("INIT_MONGO_INDEXES", (ctx as any).nacUser.id, (ctx as any).nacUser.email, { summary: result.summary });
|
||
return result;
|
||
}),
|
||
// 查询当前索引状态
|
||
indexStatus: nacAdminProcedure
|
||
.query(async () => {
|
||
const db = await getMongoDb();
|
||
if (!db) return { collections: [] };
|
||
const collections = [
|
||
COLLECTIONS.COMPLIANCE_RULES,
|
||
COLLECTIONS.AGENT_CONVERSATIONS,
|
||
"knowledge_base",
|
||
];
|
||
const status = await Promise.all(
|
||
collections.map(async (colName) => {
|
||
try {
|
||
const col = db.collection(colName);
|
||
const indexes = await col.listIndexes().toArray();
|
||
return {
|
||
collection: colName,
|
||
indexCount: indexes.length,
|
||
hasTextIndex: indexes.some(idx => Object.values(idx.key || {}).includes("text")),
|
||
hasTTLIndex: indexes.some(idx => idx.expireAfterSeconds !== undefined),
|
||
indexes: indexes.map(idx => ({
|
||
name: idx.name,
|
||
key: idx.key,
|
||
ttl: idx.expireAfterSeconds,
|
||
})),
|
||
};
|
||
} catch {
|
||
return { collection: colName, indexCount: 0, hasTextIndex: false, hasTTLIndex: false, indexes: [] };
|
||
}
|
||
})
|
||
);
|
||
return { collections: status };
|
||
}),
|
||
}),
|
||
|
||
// ─── 公开知识库问答 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<string, string[]> = {
|
||
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;
|
||
|