Commit dd45ed90 authored by David Yang's avatar David Yang

fix(audit): refine existing checklists and confirm skill style persistence

parent f99e8f26
...@@ -13,11 +13,12 @@ ...@@ -13,11 +13,12 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.3.2", "@ant-design/icons": "^6.3.2",
"@fontsource/inter": "^5.3.0", "@fontsource/inter": "^5.3.0",
"@heroui-pro/react": "1.0.0-beta.8", "@heroui-pro/react": "1.0.0-beta.9",
"@heroui/react": "^3.2.4", "@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4", "@heroui/styles": "^3.2.4",
"@modelcontextprotocol/sdk": "^1.17.5", "@modelcontextprotocol/sdk": "^1.17.5",
"@number-flow/react": "^0.6.2", "@number-flow/react": "^0.6.2",
"@react-aria/interactions": "^3.28.1",
"@react-aria/utils": "^3.34.1", "@react-aria/utils": "^3.34.1",
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.3",
"@tiptap/core": "^3.30.2", "@tiptap/core": "^3.30.2",
...@@ -43,10 +44,12 @@ ...@@ -43,10 +44,12 @@
"marked-katex-extension": "^5.1.5", "marked-katex-extension": "^5.1.5",
"motion": "^13.1.0", "motion": "^13.1.0",
"react": "^19.2.8", "react": "^19.2.8",
"react-aria": "^3.52.1",
"react-aria-components": "^1.20.0", "react-aria-components": "^1.20.0",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^4.12.3", "react-resizable-panels": "^4.12.3",
"react-stately": "^3.50.0",
"recharts": "^3.10.1", "recharts": "^3.10.1",
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
{ {
"name": "ai_chat", "name": "ai_chat",
"version": "1.0.4", "version": "1.0.3",
"build": "abc", "build": "abc",
"description": "merge master", "description": "ai 应用",
"date": "2025-07-14 19:27:20", "date": "2025-07-14 19:27:20",
"author": "yang" "author": "yang"
} }
...@@ -99,6 +99,11 @@ function AssistantMessage({ ...@@ -99,6 +99,11 @@ function AssistantMessage({
label={content ? '思考完成' : '正在思考...'} label={content ? '思考完成' : '正在思考...'}
value={item.message?.think} value={item.message?.think}
/> />
{isStreaming && item.message?.auditProgress ? (
<p className="text-sm text-muted" role="status">
{item.message.auditProgress}…
</p>
) : null}
{item.message?.tools?.map((tool, index) => ( {item.message?.tools?.map((tool, index) => (
<ThinkingDisclosure <ThinkingDisclosure
key={`${tool.function?.name || 'tool'}-${index}`} key={`${tool.function?.name || 'tool'}-${index}`}
......
import { DeleteOutlined, UploadOutlined } from '@ant-design/icons'; import {
DeleteOutlined,
InfoCircleOutlined,
UploadOutlined,
} from '@ant-design/icons';
import { DataGrid } from '@heroui-pro/react/data-grid'; import { DataGrid } from '@heroui-pro/react/data-grid';
import { DropZone } from '@heroui-pro/react/drop-zone'; import { DropZone } from '@heroui-pro/react/drop-zone';
import { EmptyState } from '@heroui-pro/react/empty-state'; import { EmptyState } from '@heroui-pro/react/empty-state';
...@@ -15,6 +19,7 @@ import { ...@@ -15,6 +19,7 @@ import {
Surface, Surface,
TextArea, TextArea,
TextField, TextField,
Tooltip,
toast, toast,
} from '@heroui/react'; } from '@heroui/react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
...@@ -227,7 +232,18 @@ export function AuditPromptConfigDialog({ ...@@ -227,7 +232,18 @@ export function AuditPromptConfigDialog({
value={prompt} value={prompt}
onChange={setPrompt} onChange={setPrompt}
> >
<Label>主提示词</Label> <Label>
<span className="inline-flex items-center gap-1">
助手定位提示词
<Tooltip delay={0}>
<InfoCircleOutlined aria-hidden="true" />
<Tooltip.Content className="max-w-72">
定义审计助手是谁、负责什么任务。具体专项的业务过滤规则请保存到公共
Skill 文件。
</Tooltip.Content>
</Tooltip>
</span>
</Label>
<TextArea maxLength={6000} rows={7} variant="secondary" /> <TextArea maxLength={6000} rows={7} variant="secondary" />
</TextField> </TextField>
<TextField <TextField
...@@ -236,7 +252,18 @@ export function AuditPromptConfigDialog({ ...@@ -236,7 +252,18 @@ export function AuditPromptConfigDialog({
value={context} value={context}
onChange={setContext} onChange={setContext}
> >
<Label>业务规则与约束</Label> <Label>
<span className="inline-flex items-center gap-1">
通用回答约束
<Tooltip delay={0}>
<InfoCircleOutlined aria-hidden="true" />
<Tooltip.Content className="max-w-72">
设置所有回答必须遵守的通用边界,例如只能依据知识库原文、依据不足时明确说明。具体业务过滤规则请保存到公共
Skill 文件。
</Tooltip.Content>
</Tooltip>
</span>
</Label>
<TextArea maxLength={10000} rows={9} variant="secondary" /> <TextArea maxLength={10000} rows={9} variant="secondary" />
</TextField> </TextField>
</Modal.Body> </Modal.Body>
...@@ -321,11 +348,15 @@ export function AuditChecklistResult({ ...@@ -321,11 +348,15 @@ export function AuditChecklistResult({
aiContent = '', aiContent = '',
allowWordExport = false, allowWordExport = false,
onSaveSkillRule, onSaveSkillRule,
onResolveSkillRule,
onResolveSkillStyle,
}) { }) {
const [threshold, setThreshold] = useState( const [threshold, setThreshold] = useState(
Number(result.threshold || AUDIT_MATCH_THRESHOLD), Number(result.threshold || AUDIT_MATCH_THRESHOLD),
); );
const [wordLoading, setWordLoading] = useState(false); const [wordLoading, setWordLoading] = useState(false);
const [ruleSaving, setRuleSaving] = useState(false);
const reportContent = result.summaryContent ?? aiContent;
const [excludedRiskIds, setExcludedRiskIds] = useState(new Set()); const [excludedRiskIds, setExcludedRiskIds] = useState(new Set());
useEffect(() => { useEffect(() => {
...@@ -352,8 +383,8 @@ export function AuditChecklistResult({ ...@@ -352,8 +383,8 @@ export function AuditChecklistResult({
); );
const isQa = result.kind === 'qa'; const isQa = result.kind === 'qa';
const summary = useMemo( const summary = useMemo(
() => parseAuditSummary(aiContent, result.project), () => parseAuditSummary(reportContent, result.project),
[aiContent, result.project], [reportContent, result.project],
); );
const handleWordExport = async () => { const handleWordExport = async () => {
...@@ -361,7 +392,7 @@ export function AuditChecklistResult({ ...@@ -361,7 +392,7 @@ export function AuditChecklistResult({
try { try {
await exportAuditWord( await exportAuditWord(
effectiveResult, effectiveResult,
aiContent, reportContent,
threshold, threshold,
effectiveResult.reportStyle, effectiveResult.reportStyle,
); );
...@@ -401,6 +432,79 @@ export function AuditChecklistResult({ ...@@ -401,6 +432,79 @@ export function AuditChecklistResult({
return ( return (
<section aria-label="审计风险点结果" className="grid gap-6"> <section aria-label="审计风险点结果" className="grid gap-6">
{result.adjustmentNote ? (
<p role="status">{result.adjustmentNote}</p>
) : null}
{result.pendingSkillStyle && onResolveSkillStyle ? (
<Surface className="grid gap-3 p-4" variant="tertiary">
<p>是否将本次 Word 样式保存到公共 Skill,作为以后报告的默认样式?</p>
<p>{result.pendingSkillStyle.changes.join(';')}</p>
<div className="flex gap-2">
{[true, false].map((save) => (
<Button
key={String(save)}
isDisabled={ruleSaving}
variant={save ? 'primary' : 'secondary'}
onPress={async () => {
setRuleSaving(true);
try {
await onResolveSkillStyle(save);
toast.success(
save
? 'Word 样式已保存到公共 Skill'
: 'Word 样式仅用于本次报告',
);
} catch (error) {
toast.danger(
`操作失败,可重试:${error.message || '未知错误'}`,
);
} finally {
setRuleSaving(false);
}
}}
>
{save ? '保存到公共 Skill' : '仅本次使用'}
</Button>
))}
</div>
</Surface>
) : null}
{result.pendingSkillRule && onResolveSkillRule ? (
<Surface className="grid gap-3 p-4" variant="tertiary">
<p>
本次已应用“{result.pendingSkillRule.label}”。是否保存到公共
Skill,供以后同类审计使用?
</p>
<div className="flex gap-2">
{[true, false].map((save) => (
<Button
key={String(save)}
isDisabled={ruleSaving}
variant={save ? 'primary' : 'secondary'}
onPress={async () => {
setRuleSaving(true);
try {
await onResolveSkillRule(save);
toast.success(
save
? '过滤规则已保存到公共 Skill'
: '过滤规则仅用于本次清单',
);
} catch (error) {
toast.danger(
`操作失败,可重试:${error.message || '未知错误'}`,
);
} finally {
setRuleSaving(false);
}
}}
>
{save ? '保存到公共 Skill' : '仅本次使用'}
</Button>
))}
</div>
</Surface>
) : null}
{!isQa ? ( {!isQa ? (
<> <>
<div className="grid gap-1"> <div className="grid gap-1">
...@@ -560,7 +664,7 @@ export function AuditChecklistResult({ ...@@ -560,7 +664,7 @@ export function AuditChecklistResult({
<Button <Button
variant="secondary" variant="secondary"
onPress={() => onPress={() =>
exportAuditMarkdown(effectiveResult, aiContent, threshold) exportAuditMarkdown(effectiveResult, reportContent, threshold)
} }
> >
Markdown Markdown
...@@ -568,7 +672,7 @@ export function AuditChecklistResult({ ...@@ -568,7 +672,7 @@ export function AuditChecklistResult({
<Button <Button
variant="secondary" variant="secondary"
onPress={() => onPress={() =>
exportAuditExcel(effectiveResult, aiContent, threshold) exportAuditExcel(effectiveResult, reportContent, threshold)
} }
> >
Excel Excel
......
...@@ -42,6 +42,10 @@ const QA_PATTERNS = [ ...@@ -42,6 +42,10 @@ const QA_PATTERNS = [
const REFINE_PATTERNS = [ const REFINE_PATTERNS = [
'去掉', '去掉',
'过滤',
'移除',
'剔除',
'不纳入',
'删除', '删除',
'不要', '不要',
'排除', '排除',
...@@ -149,6 +153,26 @@ function compactAuditRecord(record) { ...@@ -149,6 +153,26 @@ function compactAuditRecord(record) {
}; };
} }
// 按 compact 后的字符体积打包批次:mdContent/searchText 等派生字段
// 不进入模型上下文,也不应撑大批次数量
export function packAuditRecords(records = [], charBudget = 18000) {
const batches = [];
let current = [];
let currentSize = 0;
records.forEach((record) => {
const recordSize = JSON.stringify(compactAuditRecord(record)).length + 2;
if (current.length && currentSize + recordSize > charBudget) {
batches.push(current);
current = [];
currentSize = 0;
}
current.push(record);
currentSize += recordSize;
});
if (current.length) batches.push(current);
return batches.length ? batches : [[]];
}
export function buildAuditAnalysisPrompt({ export function buildAuditAnalysisPrompt({
query = '', query = '',
records = [], records = [],
...@@ -158,6 +182,8 @@ export function buildAuditAnalysisPrompt({ ...@@ -158,6 +182,8 @@ export function buildAuditAnalysisPrompt({
analysisMode = 'candidate', analysisMode = 'candidate',
batchNumber = 1, batchNumber = 1,
batchCount = 1, batchCount = 1,
totalRecords = 0,
candidatePoolSize = 0,
}) { }) {
const skill = collectAuditSkillInstructions(skillObject, query); const skill = collectAuditSkillInstructions(skillObject, query);
const context = records.slice(0, maxRecords).map(compactAuditRecord); const context = records.slice(0, maxRecords).map(compactAuditRecord);
...@@ -195,6 +221,9 @@ export function buildAuditAnalysisPrompt({ ...@@ -195,6 +221,9 @@ export function buildAuditAnalysisPrompt({
: 'Skill 业务过滤规则:无', : 'Skill 业务过滤规则:无',
'', '',
`当前批次:${batchNumber}/${batchCount};批次记录数:${context.length};本轮总记录数:${records.length}`, `当前批次:${batchNumber}/${batchCount};批次记录数:${context.length};本轮总记录数:${records.length}`,
candidatePoolSize > 0 && totalRecords > candidatePoolSize
? `知识库共 ${totalRecords} 条记录,已按本地相关度预筛出 ${candidatePoolSize} 条候选进入本轮分析。`
: '',
analysisMode === 'candidate' analysisMode === 'candidate'
? '候选阶段输出要求:返回本批次可能相关的风险点 ID,selectedRiskPointIds 可以适度偏宽。' ? '候选阶段输出要求:返回本批次可能相关的风险点 ID,selectedRiskPointIds 可以适度偏宽。'
: '收敛阶段输出要求:返回本批次最终建议保留的风险点 ID,selectedRiskPointIds 应尽量去重且严格。', : '收敛阶段输出要求:返回本批次最终建议保留的风险点 ID,selectedRiskPointIds 应尽量去重且严格。',
...@@ -769,12 +798,21 @@ export function classifyAuditAction(query = '', previousResult = null) { ...@@ -769,12 +798,21 @@ export function classifyAuditAction(query = '', previousResult = null) {
} }
export function isPersistentAuditRuleRequest(query = '') { export function isPersistentAuditRuleRequest(query = '') {
return /(以后|今后|长期|记住|保存到.?Skill|写入.?Skill|都不要|后续都)/i.test( return (
String(query), !/(?:不|别|不要|无需)(?:再)?(?:保存|写入|记住)|仅本次|只.*本次/i.test(
String(query),
) &&
/(以后|今后|长期|记住|保存到.?Skill|写入.?Skill|都不要|后续都)/i.test(
String(query),
)
); );
} }
export function buildAuditStylePrompt(changes = [], persisted = false) { export function buildAuditStylePrompt(
changes = [],
persisted = false,
pending = false,
) {
return [ return [
'这是一次审计报告 Word 排版调整请求。', '这是一次审计报告 Word 排版调整请求。',
changes.length changes.length
...@@ -783,6 +821,9 @@ export function buildAuditStylePrompt(changes = [], persisted = false) { ...@@ -783,6 +821,9 @@ export function buildAuditStylePrompt(changes = [], persisted = false) {
persisted persisted
? '该样式已保存为公共 Skill 的默认样式。' ? '该样式已保存为公共 Skill 的默认样式。'
: '该调整仅作用于当前报告。', : '该调整仅作用于当前报告。',
pending
? '请询问:是否将本次 Word 样式保存到公共 Skill,作为以后报告的默认样式?用户可回复“保存到 Skill”或“仅本次使用”,也可以点击对应按钮。未经确认不得声称已保存。'
: '',
'风险点、法规依据、来源、风险等级和审计分析结果保持不变。', '风险点、法规依据、来源、风险等级和审计分析结果保持不变。',
'请简短确认,不要重新生成审计风险点清单。', '请简短确认,不要重新生成审计风险点清单。',
].join('\n'); ].join('\n');
...@@ -816,12 +857,19 @@ export function parseRefineInstruction(query = '', previousThreshold) { ...@@ -816,12 +857,19 @@ export function parseRefineInstruction(query = '', previousThreshold) {
); );
} }
[ [
{ pattern: /(?:去掉|删除|不要|排除)([^,,。;;]+)/g, key: 'remove' }, {
pattern:
/(?:过滤掉|过滤|移除|剔除|不纳入|去掉|删除|不要|排除)([^,,。;;]+)/g,
key: 'remove',
},
{ pattern: /(?:只保留|仅保留|保留)([^,,。;;]+)/g, key: 'keep' }, { pattern: /(?:只保留|仅保留|保留)([^,,。;;]+)/g, key: 'keep' },
{ pattern: /(?:增加|加入|补充)([^,,。;;]+)/g, key: 'add' }, { pattern: /(?:增加|加入|补充)([^,,。;;]+)/g, key: 'add' },
].forEach(({ pattern, key }) => { ].forEach(({ pattern, key }) => {
for (const match of query.matchAll(pattern)) { for (const match of query.matchAll(pattern)) {
cleanInstructionValue(match[1]) if (/^(?:保存|写入|记住)/.test(match[1].trim())) continue;
cleanInstructionValue(
match[1].replace(/(?:并|然后)?(?:保存|写入)到.*$/i, ''),
)
.split(/和|与|以及|、|\s+/) .split(/和|与|以及|、|\s+/)
.filter((value) => value.length >= 2) .filter((value) => value.length >= 2)
.forEach((value) => instruction[key].push(value)); .forEach((value) => instruction[key].push(value));
...@@ -859,6 +907,26 @@ function scoreAllRecords(records, query, extraKeywords = []) { ...@@ -859,6 +907,26 @@ function scoreAllRecords(records, query, extraKeywords = []) {
.sort((a, b) => b.matchScore - a.matchScore); .sort((a, b) => b.matchScore - a.matchScore);
} }
// 大规模知识库先做本地相关度预筛,再交给 AI 分批语义分析;
// 候选池上限应明显大于最终复核上限,给候选阶段“宁可保留”留出召回空间
export function selectAnalysisCandidates(
records = [],
query = '',
options = {},
) {
const limit = Math.max(50, Math.min(2000, Number(options.limit) || 500));
if (records.length <= limit) return records;
const keywordInfo = extractAuditKeywords(query, options.extraKeywords || []);
const relevantCategories = getRelevantCategories(records, keywordInfo);
return records
.map((record) =>
scoreAuditRecord(record, query, keywordInfo, relevantCategories),
)
.filter(Boolean)
.sort((left, right) => right.matchScore - left.matchScore)
.slice(0, limit);
}
export function isAuditRefineRequest(query, previousResult) { export function isAuditRefineRequest(query, previousResult) {
return Boolean( return Boolean(
previousResult && previousResult &&
...@@ -866,6 +934,54 @@ export function isAuditRefineRequest(query, previousResult) { ...@@ -866,6 +934,54 @@ export function isAuditRefineRequest(query, previousResult) {
); );
} }
// 缩小范围直接使用当前结果,避免重新召回此前已经排除的风险点。
export function refineCurrentAuditResult(query, previousResult) {
if (
!isAuditRefineRequest(query, previousResult) ||
isAuditStyleRequest(query)
)
return null;
const instruction = parseRefineInstruction(query, previousResult.threshold);
if (instruction.add.length || /阈值|最低分|匹配度/.test(query)) return null;
const records = (previousResult.records || []).filter(
(record) =>
(!instruction.remove.length ||
!recordMatchesTerms(record, instruction.remove)) &&
(!instruction.keep.length ||
recordMatchesTerms(record, instruction.keep)),
);
const selectedIds = new Set(records.map(getAuditRecordKey));
const removed = (previousResult.records || []).filter(
(record) => !selectedIds.has(getAuditRecordKey(record)),
);
return {
...previousResult,
request: query,
refined: true,
instruction,
records,
matchedCount: records.length,
selectedRiskPointIds: [...selectedIds],
generatedAt: new Date().toISOString(),
pendingSkillTarget: 'rule',
adjustmentNote: removed.length
? `已在当前清单中移除 ${removed.length} 条,保留 ${records.length} 条,可下载更新后的 Word。`
: '未匹配到需要调整的条目,请提供具体项目名称或风险描述。',
pendingSkillRule:
instruction.remove.length && removed.length
? {
domain:
previousResult.analysis?.auditDomain || previousResult.project,
removeTerms: instruction.remove,
riskPointIds: removed.map(getAuditRecordKey),
label: `过滤 ${instruction.remove.join('、')}`,
description: '业务人员在当前审计清单中排除的风险点。',
sourceQuery: query,
}
: null,
};
}
export function buildAuditResult( export function buildAuditResult(
query, query,
records, records,
......
...@@ -459,7 +459,6 @@ export function serializeAuditSkillObject(skillObject, fileDescriptor = null) { ...@@ -459,7 +459,6 @@ export function serializeAuditSkillObject(skillObject, fileDescriptor = null) {
skill_file: fileDescriptor || object.skillFile, skill_file: fileDescriptor || object.skillFile,
enabled: object.enabled !== false, enabled: object.enabled !== false,
description: object.description || '', description: object.description || '',
audit_domain: object.auditDomain || '',
}; };
} }
......
...@@ -217,12 +217,41 @@ export function isAuditStyleRequest(query = '') { ...@@ -217,12 +217,41 @@ export function isAuditStyleRequest(query = '') {
); );
} }
export function isPersistentStyleRequest(query = '') { export function isTemporaryStyleRequest(query = '') {
return /(以后|今后|默认|保存为|记住|都这样|统一样式|长期)/.test( return /(?:不|别|不要|无需)(?:再)?(?:保存|写入|记住)|(?:仅|只)(?:用于|用在|作用于)?本次|仅此次|不用保存/.test(
String(query), String(query),
); );
} }
export function isPersistentStyleRequest(query = '') {
return (
!isTemporaryStyleRequest(query) &&
/(以后|今后|默认|保存为|记住|都这样|统一样式|长期|(?:保存|写入)到?\s*(?:公共\s*)?skill)/i.test(
String(query),
)
);
}
// 仅接受明确的确认短句,避免把下一轮业务请求误当成保存授权。
export function parseAuditSkillSaveDecision(query = '') {
const text = String(query)
.trim()
.replace(/[。!!\s]+$/g, '');
if (
/^(?:仅本次(?:使用|生效)?|只用于本次|不用(?:保存)?|不保存|不要保存|无需保存)(?:到?\s*(?:公共\s*)?skill)?$/i.test(
text,
)
)
return 'dismiss';
if (
/^(?:是|是的|好的?|可以|确认|保存|记住|(?:请)?(?:保存|写入)(?:当前|本次)?(?:的)?(?:样式|配置)?到\s*(?:公共\s*)?skill|(?:请)?保存为默认(?:样式|配置)?)$/i.test(
text,
)
)
return 'save';
return null;
}
export function parseAuditStyleRequest( export function parseAuditStyleRequest(
query = '', query = '',
currentStyle = DEFAULT_WORD_STYLE, currentStyle = DEFAULT_WORD_STYLE,
......
This diff is collapsed.
...@@ -547,7 +547,7 @@ export async function queryConversations(query) { ...@@ -547,7 +547,7 @@ export async function queryConversations(query) {
page_size: 20, page_size: 20,
mode: 'basic', mode: 'basic',
select: select:
'metadata.label metadata.pinned metadata.agent metadata.agentConfig metadata.avatar metadata.topic metadata.topicConfig metadata.knowledgeBases', 'metadata.label metadata.pinned metadata.agentConfig metadata.topic metadata.topicConfig metadata.knowledgeBases',
sort: '-metadata.pinned -created_at', sort: '-metadata.pinned -created_at',
filter: { filter: {
'metadata.user': userInfo._id, 'metadata.user': userInfo._id,
......
const assert = require('node:assert/strict');
require('./register-audit.cjs');
const {
classifyAuditAction,
refineCurrentAuditResult,
isPersistentAuditRuleRequest,
toPersistedAuditResult,
} = require('../src/features/audit/engine.js');
const initial = {
kind: 'checklist',
project: '采购管理专项审计',
threshold: 60,
records: [
{ id: '1', riskDescription: '采购合同审批', matchScore: 95 },
{ id: '2', riskDescription: '付款审批', matchScore: 90 },
{ id: '3', riskDescription: '供应商准入', matchScore: 85 },
],
analysis: { selectedRiskPointIds: ['1', '2', '3'], auditDomain: '采购' },
reportStyle: { bodyFont: '宋体' },
summaryContent: '原始清单摘要',
};
for (const verb of [
'过滤掉',
'过滤',
'移除',
'剔除',
'不纳入',
'去掉',
'删除',
'排除',
]) {
assert.equal(classifyAuditAction(`${verb}采购合同`, initial), 'refine_scope');
const result = refineCurrentAuditResult(`${verb}采购合同`, initial);
assert.deepEqual(
result.records.map((r) => r.id),
['2', '3'],
);
assert.deepEqual(result.pendingSkillRule.riskPointIds, ['1']);
assert.equal(result.pendingSkillRule.domain, '采购');
assert.deepEqual(result.reportStyle, initial.reportStyle);
assert.equal(result.summaryContent, initial.summaryContent);
}
const first = toPersistedAuditResult(
refineCurrentAuditResult('过滤掉采购合同', initial),
);
const second = refineCurrentAuditResult('再过滤掉付款审批', first);
assert.deepEqual(
second.records.map((r) => r.id),
['3'],
);
assert.deepEqual(
initial.records.map((r) => r.id),
['1', '2', '3'],
);
assert.equal(
refineCurrentAuditResult('过滤掉不存在的项', initial).pendingSkillRule,
null,
);
assert.equal(
refineCurrentAuditResult('过滤掉供应商准入', second).records.length,
0,
);
assert.equal(refineCurrentAuditResult('生成反洗钱清单', initial), null);
assert.equal(refineCurrentAuditResult('过滤掉采购合同', null), null);
assert.equal(refineCurrentAuditResult('增加发票核验', initial), null);
assert.equal(refineCurrentAuditResult('阈值改为50', initial), null);
assert.equal(isPersistentAuditRuleRequest('过滤掉采购合同'), false);
assert.equal(isPersistentAuditRuleRequest('过滤掉采购合同,保存到Skill'), true);
assert.equal(
isPersistentAuditRuleRequest('过滤掉采购合同,不要保存到Skill'),
false,
);
assert.equal(isPersistentAuditRuleRequest('以后再说,仅本次使用'), false);
console.log('audit refine regression checks passed');
const assert = require('node:assert/strict');
require('./register-audit.cjs');
const {
DEFAULT_WORD_STYLE,
isPersistentStyleRequest,
isTemporaryStyleRequest,
parseAuditSkillSaveDecision,
parseAuditStyleRequest,
} = require('../src/features/audit/style.js');
const {
appendAuditWordStyle,
DEFAULT_AUDIT_SKILL_OBJECT,
parseSkillContent,
} = require('../src/features/audit/skills.js');
const {
buildAuditStylePrompt,
toPersistedAuditResult,
} = require('../src/features/audit/engine.js');
for (const query of [
'保存到 Skill',
'保存到公共 Skill',
'写入到skill',
'保存为默认样式',
'以后都这样',
]) {
assert.equal(isPersistentStyleRequest(query), true, query);
}
for (const query of [
'不要保存到 Skill',
'不保存到公共 Skill',
'不用保存到 Skill',
'字体设置为仿宋,仅本次使用',
]) {
assert.equal(isPersistentStyleRequest(query), false, query);
assert.equal(isTemporaryStyleRequest(query), true, query);
}
for (const query of [
'保存到 Skill',
'保存到公共 Skill。',
'是的',
'好的',
'保存',
'请保存本次样式到 Skill',
]) {
assert.equal(parseAuditSkillSaveDecision(query), 'save', query);
}
for (const query of [
'仅本次使用',
'不用',
'不要保存到 Skill',
'不保存',
'只用于本次',
]) {
assert.equal(parseAuditSkillSaveDecision(query), 'dismiss', query);
}
for (const query of [
'是否保存到 Skill?',
'保存到 Skill 会影响什么?',
'生成采购清单',
'正文大小设置为小四,保存到 Skill',
]) {
assert.equal(parseAuditSkillSaveDecision(query), null, query);
}
const request = parseAuditStyleRequest(
'正文字体设置为仿宋,正文字号设置为小四,行间距设置为1.5倍',
);
assert.equal(request.persist, false);
assert.equal(request.changes.length, 3);
assert.equal(request.style.fontFamily, '仿宋');
assert.equal(request.style.bodyFontSizePt, 12);
assert.equal(request.style.lineSpacing, 1.5);
assert.equal(DEFAULT_WORD_STYLE.fontFamily, '宋体');
assert.equal(
parseAuditStyleRequest('行间距设置为1.5倍,保存到 Skill').persist,
true,
);
assert.equal(
parseAuditStyleRequest('行间距设置为1.5倍,不要保存到 Skill').persist,
false,
);
assert.match(
buildAuditStylePrompt(request.changes, false, true),
/是否将本次 Word 样式保存/,
);
assert.doesNotMatch(
buildAuditStylePrompt(request.changes, true, false),
/是否将本次 Word 样式保存/,
);
const result = {
records: [{ id: 'risk-1', riskDescription: '原文' }],
summaryContent: '原清单摘要',
reportStyle: request.style,
pendingSkillTarget: 'style',
pendingSkillStyle: { style: request.style, changes: request.changes },
};
const restored = JSON.parse(JSON.stringify(toPersistedAuditResult(result)));
assert.deepEqual(restored.pendingSkillStyle, result.pendingSkillStyle);
assert.equal(restored.summaryContent, '原清单摘要');
assert.deepEqual(restored.records, result.records);
const skill = appendAuditWordStyle(
DEFAULT_AUDIT_SKILL_OBJECT,
restored.pendingSkillStyle.style,
);
const parsed = parseSkillContent(skill.content);
assert.equal(parsed.styles.fontFamily, '仿宋');
assert.equal(parsed.styles.bodyFontSizePt, 12);
assert.equal(parsed.styles.lineSpacing, 1.5);
console.log('audit style save regression checks passed');
const fs = require('node:fs');
const path = require('node:path');
const ts = require('typescript');
const originalLoader = require.extensions['.js'];
require.extensions['.js'] = (module, filename) => {
if (
!filename.startsWith(
path.resolve(__dirname, '../src/features/audit') + path.sep,
)
) {
return originalLoader(module, filename);
}
module._compile(
ts.transpileModule(fs.readFileSync(filename, 'utf8'), {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
},
}).outputText,
filename,
);
};
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