Commit 5919a2cf authored by David Yang's avatar David Yang

feat: 添加审计功能

parent 671bc70b
......@@ -8,6 +8,7 @@ export default defineConfig({
model: {},
initialState: {},
request: {},
esbuildMinifyIIFE: true,
reactQuery: {
devtool: false,
},
......
{
"name": "ai_chat",
"version": "1.0.1",
"version": "1.0.3",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
......
This diff is collapsed.
export const AUDIT_RISK_POINT_SUBJECT = 'audit_risk_point';
export const AUDIT_MATCH_THRESHOLD = 80;
export const AUDIT_PAGE_SIZE = 2000;
export const AUDIT_PROMPT_RECORD_LIMIT = 80;
export const AUDIT_FIELD_ALIASES = {
businessSegment: ['business_segment', '业务板块'],
level1Project: ['level1_project', '一级项目'],
level2Project: ['level2_project', '二级项目'],
riskDescription: ['risk_description', '具体风险点描述'],
regulationBasis: ['regulation_basis', '法规依据'],
riskSource: ['risk_source', '风险点来源'],
riskLevel: ['risk_level', '风险等级'],
keywords: ['keywords', '关键词'],
mdContent: ['md_content', 'Markdown 内容', 'markdown_content'],
enabled: ['enabled', '是否启用'],
sourceRow: ['source_row', '来源行号', '_source_row'],
};
export const AUDIT_EXCEL_FIELDS = [
{ key: 'businessSegment', label: '业务板块', alias: 'business_segment' },
{ key: 'level1Project', label: '一级项目', alias: 'level1_project' },
{ key: 'level2Project', label: '二级项目', alias: 'level2_project' },
{
key: 'riskDescription',
label: '具体风险点描述',
alias: 'risk_description',
required: true,
},
{ key: 'regulationBasis', label: '法规依据', alias: 'regulation_basis' },
{ key: 'riskSource', label: '风险点来源', alias: 'risk_source' },
{ key: 'riskLevel', label: '风险等级', alias: 'risk_level' },
{ key: 'keywords', label: '关键词', alias: 'keywords' },
{ key: 'mdContent', label: 'Markdown 内容', alias: 'md_content' },
{ key: 'enabled', label: '是否启用', alias: 'enabled' },
{ key: 'sourceRow', label: '来源行号', alias: 'source_row' },
];
export const AUDIT_STOP_WORDS = new Set([
'专项',
'审计',
'业务',
'风险',
'风险点',
'检查',
'检查点',
'项目',
'公司',
'基金',
'情况',
'相关',
'工作',
'制度',
'流程',
'内部',
'合规',
'生成',
'清单',
'查询',
'查找',
'内容',
'帮我',
]);
export const AUDIT_DOMAIN_KEYWORDS = {
采购: [
'采购',
'供应商',
'服务机构',
'外包',
'合同',
'招标',
'询价',
'验收',
'付款',
'廉洁',
'第三方',
'服务协议',
],
衍生品: [
'衍生品',
'衍生',
'期货',
'期权',
'互换',
'远期',
'杠杆',
'保证金',
'对手方',
],
反洗钱: [
'反洗钱',
'洗钱风险',
'可疑交易',
'大额交易',
'客户身份识别',
'身份尽职调查',
'客户风险等级',
'黑名单',
'制裁名单',
'交易监控',
'监管报送',
],
信息技术: [
'信息技术',
'系统',
'网络',
'数据',
'安全',
'灾备',
'运维',
'机房',
],
销售: ['销售', '客户', '投资者', '适当性', '宣传', '推介', '代销', '投诉'],
投研交易: [
'投资',
'研究',
'交易',
'关联交易',
'内幕',
'组合',
'授权',
'指令',
],
运营: ['运营', '估值', '核算', '清算', '注册登记', 'TA', '信息披露'],
员工行为: [
'员工行为',
'从业人员',
'投资行为',
'通讯工具',
'信息保密',
'廉洁从业',
'利益输送',
'人员资质',
'任免管理',
'薪酬管理',
'问责管理',
'员工培训',
],
公司治理: [
'公司治理',
'股东',
'董事会',
'监事',
'章程',
'高管',
'专业委员会',
],
合规管理: ['合规管理', '督察长', '监察稽核', '利益冲突'],
产品业务: ['产品', '公募', '资管', '备案', '注册', '展期', '清算'],
};
export const AUDIT_CHECKLIST_RULES = `
风险点、法规依据、风险点来源、风险等级必须来自审计风险点知识库,不得编造或改写原文。
审计内容必须按“业务板块 -> 一级项目 -> 二级项目 -> 风险点”组织,相同层级必须合并。
每条风险点必须保留匹配度、具体风险点描述、法规依据、风险点来源、风险等级。
如果知识库没有足够相关内容,必须明确说明知识库覆盖不足。
`;
This diff is collapsed.
import * as XLSX from 'xlsx';
import {
AUDIT_EXCEL_FIELDS,
AUDIT_FIELD_ALIASES,
AUDIT_RISK_POINT_SUBJECT,
} from './constants';
import { auditRecordIdentity, buildRiskPointMarkdown } from './engine';
const HIERARCHY_FIELDS = ['businessSegment', 'level1Project', 'level2Project'];
function normalizeHeader(value) {
return String(value || '')
.replace(/[\s\n\r**::]/g, '')
.toLowerCase();
}
function normalizeCell(value) {
if (value === undefined || value === null) return '';
if (value instanceof Date) return value.toISOString().slice(0, 10);
return String(value).trim();
}
function fieldHeaderAliases(field) {
return [
field.label,
field.alias,
...(AUDIT_FIELD_ALIASES[field.key] || []),
].map(normalizeHeader);
}
function detectHeaderRow(rows) {
let best = { index: -1, score: 0, columns: {} };
rows.slice(0, 30).forEach((row, rowIndex) => {
const columns = {};
row.forEach((cell, columnIndex) => {
const header = normalizeHeader(cell);
AUDIT_EXCEL_FIELDS.forEach((field) => {
if (
columns[field.key] === undefined &&
fieldHeaderAliases(field).includes(header)
) {
columns[field.key] = columnIndex;
}
});
});
const score = Object.keys(columns).length;
if (score > best.score) best = { index: rowIndex, score, columns };
});
return best;
}
function parseKeywords(value, record) {
const explicit = normalizeCell(value)
.split(/[,,、;;\n\s]+/)
.map((item) => item.trim())
.filter((item) => item.length >= 2);
if (explicit.length) return Array.from(new Set(explicit));
return Array.from(
new Set(
[record.businessSegment, record.level1Project, record.level2Project]
.filter(Boolean)
.flatMap((item) => item.split(/[、/()()\s]+/))
.filter((item) => item.length >= 2),
),
);
}
function parseEnabled(value) {
const text = normalizeCell(value).toLowerCase();
if (!text) return true;
return !['false', '0', '否', '停用', '禁用'].includes(text);
}
export async function parseAuditExcel(file) {
const workbook = XLSX.read(await file.arrayBuffer(), {
type: 'array',
cellDates: true,
});
const sheetResults = [];
const allRecords = [];
const errors = [];
workbook.SheetNames.forEach((sheetName) => {
const sheet = workbook.Sheets[sheetName];
const rows = XLSX.utils.sheet_to_json(sheet, {
header: 1,
defval: '',
raw: false,
});
const header = detectHeaderRow(rows);
if (header.score < 3 || header.columns.riskDescription === undefined) {
sheetResults.push({
sheetName,
imported: 0,
skipped: Math.max(0, rows.length - 1),
reason: '未识别到“具体风险点描述”及至少两个业务字段',
});
return;
}
const previousHierarchy = {};
let imported = 0;
let skipped = 0;
rows.slice(header.index + 1).forEach((row, offset) => {
const sourceRow = header.index + offset + 2;
const record = {};
AUDIT_EXCEL_FIELDS.forEach((field) => {
const columnIndex = header.columns[field.key];
record[field.key] =
columnIndex === undefined ? '' : normalizeCell(row[columnIndex]);
});
HIERARCHY_FIELDS.forEach((key) => {
if (record[key]) previousHierarchy[key] = record[key];
else record[key] = previousHierarchy[key] || '';
});
if (!record.riskDescription) {
const hasOtherContent = Object.values(record).some(Boolean);
if (hasOtherContent) {
errors.push({ sheetName, sourceRow, message: '缺少具体风险点描述' });
}
skipped += 1;
return;
}
record.sourceRow = record.sourceRow || sourceRow;
record.enabled = parseEnabled(record.enabled);
record.keywords = parseKeywords(record.keywords, record);
record.sheetName = sheetName;
record.mdContent = record.mdContent || buildRiskPointMarkdown(record);
record.searchText = [
record.businessSegment,
record.level1Project,
record.level2Project,
record.riskDescription,
record.regulationBasis,
record.riskSource,
record.riskLevel,
record.keywords.join(' '),
]
.filter(Boolean)
.join(' ');
allRecords.push(record);
imported += 1;
});
sheetResults.push({ sheetName, imported, skipped });
});
const uniqueRecords = [];
const identities = new Set();
allRecords.forEach((record) => {
const identity = auditRecordIdentity(record, true);
if (!identities.has(identity)) {
identities.add(identity);
uniqueRecords.push(record);
}
});
return {
fileName: file.name,
records: uniqueRecords,
duplicateRows: allRecords.length - uniqueRecords.length,
errors,
sheets: sheetResults,
markdown: uniqueRecords
.map((record, index) => buildRiskPointMarkdown(record, index + 1))
.join('\n\n'),
};
}
export function createAuditRecordModel(record) {
const hierarchy = [
record.businessSegment,
record.level1Project,
record.level2Project,
].filter(Boolean);
return {
subject: AUDIT_RISK_POINT_SUBJECT,
title: hierarchy.join(' / ') || `风险点 ${record.sourceRow || ''}`,
metadata: {
business_segment: record.businessSegment || '',
level1_project: record.level1Project || '',
level2_project: record.level2Project || '',
risk_description: record.riskDescription || '',
regulation_basis: record.regulationBasis || '',
risk_source: record.riskSource || '',
risk_level: record.riskLevel || '',
keywords: Array.isArray(record.keywords)
? record.keywords
: parseKeywords(record.keywords, record),
md_content: record.mdContent || buildRiskPointMarkdown(record),
enabled: record.enabled !== false,
source_row: Number(record.sourceRow) || record.sourceRow || '',
},
};
}
function sameRecordContent(left, right) {
return auditRecordIdentity(left, true) === auditRecordIdentity(right, true);
}
export async function importAuditRecords({
records,
existingRecords,
createRecord,
updateRecord,
onProgress,
}) {
const existingByIdentity = new Map(
existingRecords.map((record) => [auditRecordIdentity(record), record]),
);
const jobs = [];
const result = { created: 0, updated: 0, skipped: 0, failed: 0, errors: [] };
records.forEach((record) => {
const existing = existingByIdentity.get(auditRecordIdentity(record));
if (existing && sameRecordContent(existing, record)) {
result.skipped += 1;
return;
}
jobs.push({ type: existing?.id ? 'update' : 'create', record, existing });
});
let completed = result.skipped;
onProgress?.({ completed, total: records.length, ...result });
let cursor = 0;
const worker = async () => {
while (cursor < jobs.length) {
const job = jobs[cursor];
cursor += 1;
try {
const model = createAuditRecordModel(job.record);
if (job.type === 'update') {
await updateRecord(job.existing.id, {
title: model.title,
metadata: model.metadata,
});
result.updated += 1;
} else {
await createRecord(model);
result.created += 1;
}
} catch (error) {
result.failed += 1;
result.errors.push({
sourceRow: job.record.sourceRow,
message: error?.message || '写入失败',
});
} finally {
completed += 1;
onProgress?.({ completed, total: records.length, ...result });
}
}
};
await Promise.all(
Array.from({ length: Math.min(4, Math.max(1, jobs.length)) }, worker),
);
return result;
}
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment