Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
ai-chat-xy
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
ai-chat-xy
Commits
5919a2cf
Commit
5919a2cf
authored
Aug 05, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 添加审计功能
parent
671bc70b
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
2326 additions
and
26 deletions
+2326
-26
.umirc.ts
.umirc.ts
+1
-0
manifest.json
public/manifest.json
+1
-1
components.jsx
src/features/audit/components.jsx
+605
-0
constants.js
src/features/audit/constants.js
+158
-0
engine.js
src/features/audit/engine.js
+665
-0
excel.js
src/features/audit/excel.js
+259
-0
export.js
src/features/audit/export.js
+440
-0
index.jsx
src/pages/Chat/index.jsx
+197
-25
No files found.
.umirc.ts
View file @
5919a2cf
...
...
@@ -8,6 +8,7 @@ export default defineConfig({
model
:
{},
initialState
:
{},
request
:
{},
esbuildMinifyIIFE
:
true
,
reactQuery
:
{
devtool
:
false
,
},
...
...
public/manifest.json
View file @
5919a2cf
{
"name"
:
"ai_chat"
,
"version"
:
"1.0.
1
"
,
"version"
:
"1.0.
3
"
,
"build"
:
"abc"
,
"description"
:
"merge master"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/features/audit/components.jsx
0 → 100644
View file @
5919a2cf
import
{
DatabaseOutlined
,
DownloadOutlined
,
FileExcelOutlined
,
FileMarkdownOutlined
,
FileWordOutlined
,
InboxOutlined
,
ReloadOutlined
,
UploadOutlined
,
}
from
'@ant-design/icons'
;
import
{
Alert
,
Button
,
Collapse
,
Descriptions
,
Divider
,
Empty
,
Flex
,
InputNumber
,
message
,
Modal
,
Progress
,
Space
,
Table
,
Tag
,
Tooltip
,
Typography
,
Upload
,
}
from
'antd'
;
import
{
createStyles
}
from
'antd-style'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
AUDIT_MATCH_THRESHOLD
}
from
'./constants'
;
import
{
groupAuditRecords
}
from
'./engine'
;
import
{
importAuditRecords
,
parseAuditExcel
}
from
'./excel'
;
import
{
exportAuditExcel
,
exportAuditMarkdown
,
exportAuditWord
,
filterAuditRecords
,
}
from
'./export'
;
const
useAuditStyles
=
createStyles
(({
token
,
css
})
=>
({
knowledgeBar
:
css
`
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 48px;
padding: 8px 12px;
border: 1px solid
${
token
.
colorBorderSecondary
}
;
border-radius: 8px;
background:
${
token
.
colorBgContainer
}
;
@media (max-width: 575px) {
align-items: flex-start;
flex-direction: column;
}
`
,
knowledgeMeta
:
css
`
min-width: 0;
color:
${
token
.
colorTextSecondary
}
;
strong {
color:
${
token
.
colorText
}
;
margin-right: 8px;
}
`
,
result
:
css
`
width: 100%;
margin-top: 8px;
padding-top: 16px;
border-top: 1px solid
${
token
.
colorBorderSecondary
}
;
`
,
resultHeader
:
css
`
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
@media (max-width: 575px) {
flex-direction: column;
}
`
,
stats
:
css
`
color:
${
token
.
colorTextSecondary
}
;
font-size: 13px;
line-height: 1.6;
font-variant-numeric: tabular-nums;
`
,
businessSection
:
css
`
padding: 14px 0 4px;
border-top: 1px solid
${
token
.
colorBorderSecondary
}
;
&:first-of-type {
border-top: 0;
padding-top: 2px;
}
`
,
businessTitle
:
css
`
margin: 0 0 12px !important;
font-size: 16px !important;
line-height: 1.5 !important;
`
,
levelOne
:
css
`
margin: 0 0 16px 12px;
@media (max-width: 575px) {
margin-left: 0;
}
`
,
levelOneTitle
:
css
`
display: block;
margin-bottom: 8px;
color:
${
token
.
colorText
}
;
font-size: 14px;
`
,
detailCollapse
:
css
`
border-radius: 8px;
overflow: hidden;
background:
${
token
.
colorFillAlter
}
;
.ant-collapse-header {
min-height: 44px;
align-items: center !important;
padding: 10px 12px !important;
}
.ant-collapse-content-box {
padding: 0 12px 12px !important;
}
`
,
risk
:
css
`
padding: 12px 0 12px 12px;
border-left: 3px solid
${
token
.
colorPrimaryBorder
}
;
border-bottom: 1px solid
${
token
.
colorBorderSecondary
}
;
&:last-child {
border-bottom: 0;
}
`
,
riskTitle
:
css
`
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
`
,
riskDescription
:
css
`
margin: 0 0 10px !important;
color:
${
token
.
colorText
}
;
line-height: 1.7;
overflow-wrap: anywhere;
`
,
exportBar
:
css
`
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding-top: 14px;
border-top: 1px solid
${
token
.
colorBorderSecondary
}
;
`
,
importPreview
:
css
`
margin-top: 16px;
`
,
importErrors
:
css
`
max-height: 120px;
overflow: auto;
margin-top: 12px;
`
,
}));
function
scoreColor
(
score
)
{
if
(
score
>=
85
)
return
'success'
;
if
(
score
>=
70
)
return
'processing'
;
if
(
score
>=
55
)
return
'warning'
;
return
'default'
;
}
function
downloadText
(
content
,
fileName
)
{
const
url
=
URL
.
createObjectURL
(
new
Blob
([
content
],
{
type
:
'text/markdown;charset=utf-8'
}),
);
const
anchor
=
document
.
createElement
(
'a'
);
anchor
.
href
=
url
;
anchor
.
download
=
fileName
;
document
.
body
.
appendChild
(
anchor
);
anchor
.
click
();
anchor
.
remove
();
window
.
setTimeout
(()
=>
URL
.
revokeObjectURL
(
url
),
1000
);
}
export
function
AuditKnowledgeBar
({
count
,
loading
,
onOpen
,
onRefresh
})
{
const
{
styles
}
=
useAuditStyles
();
return
(
<
div
className=
{
styles
.
knowledgeBar
}
>
<
div
className=
{
styles
.
knowledgeMeta
}
>
<
DatabaseOutlined
style=
{
{
marginRight
:
8
}
}
/>
<
strong
>
审计风险点知识库
</
strong
>
{
loading
?
'正在读取'
:
`共 ${count} 条有效风险点`
}
</
div
>
<
Space
wrap
>
<
Tooltip
title=
"刷新知识库"
>
<
Button
aria
-
label=
"刷新知识库"
icon=
{
<
ReloadOutlined
/>
}
loading=
{
loading
}
onClick=
{
onRefresh
}
/>
</
Tooltip
>
<
Button
icon=
{
<
UploadOutlined
/>
}
type=
"primary"
onClick=
{
onOpen
}
>
导入 Excel
</
Button
>
</
Space
>
</
div
>
);
}
function
RiskDetail
({
risk
,
index
,
styles
})
{
return
(
<
div
className=
{
styles
.
risk
}
>
<
div
className=
{
styles
.
riskTitle
}
>
<
Typography
.
Text
strong
>
风险点
{
index
+
1
}
· 来源行号
{
risk
.
sourceRow
||
'未知'
}
</
Typography
.
Text
>
<
Space
size=
{
6
}
wrap
>
<
Tag
color=
{
scoreColor
(
risk
.
matchScore
)
}
>
{
risk
.
matchScore
}
分
</
Tag
>
{
risk
.
riskLevel
&&
<
Tag
>
{
risk
.
riskLevel
}
风险
</
Tag
>
}
</
Space
>
</
div
>
<
Typography
.
Paragraph
className=
{
styles
.
riskDescription
}
>
{
risk
.
riskDescription
}
</
Typography
.
Paragraph
>
<
Descriptions
size=
"small"
column=
{
{
xs
:
1
,
sm
:
1
,
md
:
2
}
}
items=
{
[
{
key
:
'basis'
,
label
:
'法规依据'
,
children
:
risk
.
regulationBasis
||
'未提供'
,
},
{
key
:
'source'
,
label
:
'风险点来源'
,
children
:
risk
.
riskSource
||
'未提供'
,
},
{
key
:
'reason'
,
label
:
'命中依据'
,
children
:
risk
.
matchReasons
?.
join
(
'、'
)
||
'字段词项覆盖'
,
},
{
key
:
'level'
,
label
:
'匹配等级'
,
children
:
risk
.
matchLevel
},
]
}
/>
</
div
>
);
}
export
function
AuditChecklistResult
({
result
,
aiContent
=
''
})
{
const
{
styles
}
=
useAuditStyles
();
const
[
threshold
,
setThreshold
]
=
useState
(
Number
(
result
.
threshold
||
AUDIT_MATCH_THRESHOLD
),
);
const
[
wordLoading
,
setWordLoading
]
=
useState
(
false
);
useEffect
(()
=>
{
setThreshold
(
Number
(
result
.
threshold
||
AUDIT_MATCH_THRESHOLD
));
},
[
result
.
generatedAt
,
result
.
threshold
]);
const
visibleRecords
=
useMemo
(
()
=>
filterAuditRecords
(
result
,
threshold
),
[
result
,
threshold
],
);
const
groups
=
useMemo
(
()
=>
groupAuditRecords
(
visibleRecords
),
[
visibleRecords
],
);
const
isQa
=
result
.
kind
===
'qa'
;
const
handleWordExport
=
async
()
=>
{
setWordLoading
(
true
);
try
{
await
exportAuditWord
(
result
,
aiContent
,
threshold
);
}
catch
(
error
)
{
message
.
error
(
`Word 导出失败:
${
error
.
message
||
'未知错误'
}
`
);
}
finally
{
setWordLoading
(
false
);
}
};
return
(
<
section
className=
{
styles
.
result
}
aria
-
label=
"审计风险点结果"
>
<
div
className=
{
styles
.
resultHeader
}
>
<
div
>
<
Typography
.
Title
level=
{
4
}
style=
{
{
margin
:
0
}
}
>
{
isQa
?
'引用风险点'
:
'审计内容'
}
</
Typography
.
Title
>
<
div
className=
{
styles
.
stats
}
>
知识库
{
result
.
totalCount
}
条 · 全量评分
{
result
.
scoredCount
}
条 ·
当前显示
{
visibleRecords
.
length
}
条
{
result
.
refined
?
` · 已应用调整“${result.request}”`
:
''
}
</
div
>
</
div
>
<
Tag
color=
{
visibleRecords
.
length
?
'processing'
:
'warning'
}
>
匹配度 ≥
{
threshold
}
</
Tag
>
</
div
>
{
!
visibleRecords
.
length
?
(
<
Alert
type=
"warning"
showIcon
message=
"当前阈值下未命中风险点"
description=
"请补充更明确的业务板块、项目类别或风险点来源,或在消息中指定更低的匹配度阈值。"
/>
)
:
(
groups
.
map
((
business
,
businessIndex
)
=>
(
<
section
className=
{
styles
.
businessSection
}
key=
{
business
.
businessSegment
}
>
<
Typography
.
Title
level=
{
5
}
className=
{
styles
.
businessTitle
}
>
{
businessIndex
+
1
}
.
{
business
.
businessSegment
}
</
Typography
.
Title
>
{
business
.
level1Projects
.
map
((
level1
,
level1Index
)
=>
(
<
div
className=
{
styles
.
levelOne
}
key=
{
`${business.businessSegment}-${level1.level1Project}`
}
>
<
Typography
.
Text
strong
className=
{
styles
.
levelOneTitle
}
>
{
businessIndex
+
1
}
.
{
level1Index
+
1
}
{
level1
.
level1Project
}
</
Typography
.
Text
>
<
Collapse
className=
{
styles
.
detailCollapse
}
bordered=
{
false
}
items=
{
level1
.
level2Projects
.
map
((
level2
,
level2Index
)
=>
({
key
:
`${level1.level1Project}-${level2.level2Project}`
,
label
:
`${businessIndex + 1}.${level1Index + 1}.${
level2Index + 1
} ${level2.level2Project}`
,
extra
:
`${level2.risks.length} 个风险点`
,
children
:
level2
.
risks
.
map
((
risk
,
riskIndex
)
=>
(
<
RiskDetail
key=
{
`${risk.id || risk.sourceRow}-${riskIndex}`
}
risk=
{
risk
}
index=
{
riskIndex
}
styles=
{
styles
}
/>
)),
}))
}
/>
</
div
>
))
}
</
section
>
))
)
}
{
!
isQa
&&
visibleRecords
.
length
>
0
&&
(
<
div
className=
{
styles
.
exportBar
}
>
<
Space
wrap
>
<
Typography
.
Text
>
导出最低匹配度
</
Typography
.
Text
>
<
InputNumber
aria
-
label=
"导出最低匹配度"
min=
{
Number
(
result
.
threshold
||
0
)
}
max=
{
100
}
step=
{
5
}
value=
{
threshold
}
onChange=
{
(
value
)
=>
setThreshold
(
Number
(
value
||
result
.
threshold
))
}
/>
</
Space
>
<
Space
wrap
>
<
Button
icon=
{
<
FileMarkdownOutlined
/>
}
onClick=
{
()
=>
exportAuditMarkdown
(
result
,
aiContent
,
threshold
)
}
>
Markdown
</
Button
>
<
Button
icon=
{
<
FileExcelOutlined
/>
}
onClick=
{
()
=>
exportAuditExcel
(
result
,
aiContent
,
threshold
)
}
>
Excel
</
Button
>
<
Button
type=
"primary"
icon=
{
<
FileWordOutlined
/>
}
loading=
{
wordLoading
}
onClick=
{
handleWordExport
}
>
Word
</
Button
>
</
Space
>
</
div
>
)
}
</
section
>
);
}
export
function
AuditKnowledgeImportDialog
({
open
,
existingRecords
,
createRecordFn
,
updateRecordFn
,
onCancel
,
onImported
,
})
{
const
{
styles
}
=
useAuditStyles
();
const
[
parsing
,
setParsing
]
=
useState
(
false
);
const
[
parsed
,
setParsed
]
=
useState
(
null
);
const
[
importing
,
setImporting
]
=
useState
(
false
);
const
[
progress
,
setProgress
]
=
useState
(
null
);
useEffect
(()
=>
{
if
(
!
open
)
{
setParsed
(
null
);
setProgress
(
null
);
}
},
[
open
]);
const
handleFile
=
async
(
file
)
=>
{
setParsing
(
true
);
setProgress
(
null
);
try
{
const
result
=
await
parseAuditExcel
(
file
);
setParsed
(
result
);
if
(
!
result
.
records
.
length
)
message
.
warning
(
'Excel 中未识别到有效风险点'
);
}
catch
(
error
)
{
setParsed
(
null
);
message
.
error
(
`Excel 解析失败:
${
error
.
message
||
'文件格式异常'
}
`
);
}
finally
{
setParsing
(
false
);
}
return
false
;
};
const
handleImport
=
async
()
=>
{
if
(
!
parsed
?.
records
.
length
)
return
;
setImporting
(
true
);
try
{
const
result
=
await
importAuditRecords
({
records
:
parsed
.
records
,
existingRecords
:
existingRecords
||
[],
createRecord
:
createRecordFn
,
updateRecord
:
updateRecordFn
,
onProgress
:
setProgress
,
});
if
(
result
.
failed
)
{
message
.
warning
(
`导入完成:新增
${
result
.
created
}
,更新
${
result
.
updated
}
,跳过
${
result
.
skipped
}
,失败
${
result
.
failed
}
`
,
);
}
else
{
message
.
success
(
`导入完成:新增
${
result
.
created
}
,更新
${
result
.
updated
}
,跳过
${
result
.
skipped
}
`
,
);
}
await
onImported
?.(
result
);
if
(
!
result
.
failed
)
onCancel
?.();
}
catch
(
error
)
{
message
.
error
(
`知识库写入失败:
${
error
.
message
||
'未知错误'
}
`
);
}
finally
{
setImporting
(
false
);
}
};
const
columns
=
[
{
title
:
'业务板块'
,
dataIndex
:
'businessSegment'
,
width
:
130
},
{
title
:
'一级项目'
,
dataIndex
:
'level1Project'
,
width
:
150
},
{
title
:
'二级项目'
,
dataIndex
:
'level2Project'
,
width
:
160
},
{
title
:
'具体风险点描述'
,
dataIndex
:
'riskDescription'
,
width
:
320
,
ellipsis
:
true
,
},
{
title
:
'风险等级'
,
dataIndex
:
'riskLevel'
,
width
:
100
},
{
title
:
'来源行号'
,
dataIndex
:
'sourceRow'
,
width
:
100
},
];
return
(
<
Modal
title=
"导入审计风险点 Excel"
open=
{
open
}
width=
{
920
}
onCancel=
{
onCancel
}
maskClosable=
{
!
importing
}
closable=
{
!
importing
}
footer=
{
[
<
Button
key=
"cancel"
onClick=
{
onCancel
}
disabled=
{
importing
}
>
取消
</
Button
>,
<
Button
key=
"markdown"
icon=
{
<
DownloadOutlined
/>
}
disabled=
{
!
parsed
?.
records
.
length
||
importing
}
onClick=
{
()
=>
downloadText
(
parsed
.
markdown
,
`${parsed.fileName.replace(/\.[^.]+$/, '')}.md`
,
)
}
>
下载 Markdown
</
Button
>,
<
Button
key=
"import"
type=
"primary"
icon=
{
<
DatabaseOutlined
/>
}
loading=
{
importing
}
disabled=
{
!
parsed
?.
records
.
length
}
onClick=
{
handleImport
}
>
写入知识库
</
Button
>,
]
}
>
<
Upload
.
Dragger
accept=
".xlsx,.xls"
maxCount=
{
1
}
showUploadList=
{
false
}
beforeUpload=
{
handleFile
}
disabled=
{
parsing
||
importing
}
>
<
p
className=
"ant-upload-drag-icon"
>
{
parsing
?
<
ReloadOutlined
spin
/>
:
<
InboxOutlined
/>
}
</
p
>
<
p
className=
"ant-upload-text"
>
{
parsing
?
'正在解析 Excel'
:
'选择或拖入 Excel 文件'
}
</
p
>
</
Upload
.
Dragger
>
{
progress
&&
(
<
div
className=
{
styles
.
importPreview
}
>
<
Progress
percent=
{
Math
.
round
((
progress
.
completed
/
progress
.
total
)
*
100
)
}
status=
{
progress
.
failed
?
'exception'
:
'active'
}
format=
{
()
=>
`${progress.completed}/${progress.total}`
}
/>
{
progress
.
errors
?.
length
>
0
&&
(
<
Alert
type=
"error"
showIcon
message=
{
`${progress.errors.length} 条记录写入失败`
}
description=
{
progress
.
errors
.
slice
(
0
,
6
)
.
map
(
(
error
)
=>
`来源行号 ${error.sourceRow || '未知'}:${error.message}`
,
)
.
join
(
';'
)
}
/>
)
}
</
div
>
)
}
{
parsed
&&
(
<
div
className=
{
styles
.
importPreview
}
>
<
Flex
justify=
"space-between"
align=
"center"
wrap=
"wrap"
gap=
{
8
}
>
<
Typography
.
Text
strong
>
{
parsed
.
fileName
}
</
Typography
.
Text
>
<
Space
wrap
>
<
Tag
color=
"success"
>
有效
{
parsed
.
records
.
length
}
</
Tag
>
<
Tag
>
重复行
{
parsed
.
duplicateRows
}
</
Tag
>
<
Tag
color=
{
parsed
.
errors
.
length
?
'warning'
:
'default'
}
>
异常
{
parsed
.
errors
.
length
}
</
Tag
>
</
Space
>
</
Flex
>
<
Divider
style=
{
{
margin
:
'12px 0'
}
}
/>
{
parsed
.
records
.
length
?
(
<
Table
size=
"small"
rowKey=
{
(
record
)
=>
`${record.sheetName}-${record.sourceRow}`
}
columns=
{
columns
}
dataSource=
{
parsed
.
records
.
slice
(
0
,
20
)
}
pagination=
{
false
}
scroll=
{
{
x
:
960
,
y
:
280
}
}
/>
)
:
(
<
Empty
image=
{
Empty
.
PRESENTED_IMAGE_SIMPLE
}
/>
)
}
{
parsed
.
errors
.
length
>
0
&&
(
<
div
className=
{
styles
.
importErrors
}
>
<
Alert
type=
"warning"
showIcon
message=
{
`${parsed.errors.length} 行未导入`
}
description=
{
parsed
.
errors
.
slice
(
0
,
8
)
.
map
(
(
error
)
=>
`${error.sheetName} 第 ${error.sourceRow} 行:${error.message}`
,
)
.
join
(
';'
)
}
/>
</
div
>
)
}
</
div
>
)
}
</
Modal
>
);
}
src/features/audit/constants.js
0 → 100644
View file @
5919a2cf
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
=
`
风险点、法规依据、风险点来源、风险等级必须来自审计风险点知识库,不得编造或改写原文。
审计内容必须按“业务板块 -> 一级项目 -> 二级项目 -> 风险点”组织,相同层级必须合并。
每条风险点必须保留匹配度、具体风险点描述、法规依据、风险点来源、风险等级。
如果知识库没有足够相关内容,必须明确说明知识库覆盖不足。
`
;
src/features/audit/engine.js
0 → 100644
View file @
5919a2cf
import
{
AUDIT_CHECKLIST_RULES
,
AUDIT_DOMAIN_KEYWORDS
,
AUDIT_FIELD_ALIASES
,
AUDIT_MATCH_THRESHOLD
,
AUDIT_PAGE_SIZE
,
AUDIT_PROMPT_RECORD_LIMIT
,
AUDIT_RISK_POINT_SUBJECT
,
AUDIT_STOP_WORDS
,
}
from
'./constants'
;
const
GENERATE_PATTERNS
=
[
'生成'
,
'审计清单'
,
'检查清单'
,
'检查表'
,
'审计方案'
,
'我要开展'
,
'开展'
,
'做一个'
,
'帮我做'
,
'专项审计'
,
'出一份'
,
];
const
QA_PATTERNS
=
[
'什么'
,
'哪些'
,
'如何'
,
'是否'
,
'为什么'
,
'依据'
,
'法规'
,
'风险点有哪些'
,
'帮我查'
,
'查询'
,
'检索'
,
];
const
REFINE_PATTERNS
=
[
'去掉'
,
'删除'
,
'不要'
,
'排除'
,
'只保留'
,
'仅保留'
,
'增加'
,
'加入'
,
'补充'
,
'阈值'
,
'最低分'
,
];
function
toText
(
value
)
{
if
(
Array
.
isArray
(
value
))
{
return
value
.
join
(
' '
);
}
if
(
value
===
undefined
||
value
===
null
)
{
return
''
;
}
return
String
(
value
).
trim
();
}
export
function
normalizeRecordList
(
res
)
{
if
(
Array
.
isArray
(
res
))
return
res
;
if
(
Array
.
isArray
(
res
?.
records
))
return
res
.
records
;
if
(
Array
.
isArray
(
res
?.
items
))
return
res
.
items
;
if
(
Array
.
isArray
(
res
?.
data
))
return
res
.
data
;
if
(
Array
.
isArray
(
res
?.
data
?.
records
))
return
res
.
data
.
records
;
if
(
Array
.
isArray
(
res
?.
data
?.
items
))
return
res
.
data
.
items
;
if
(
Array
.
isArray
(
res
?.
hits
?.
hits
))
{
return
res
.
hits
.
hits
.
map
((
item
)
=>
item
.
_source
||
item
);
}
return
[];
}
function
getResponseTotal
(
res
)
{
const
values
=
[
res
?.
total
,
res
?.
count
,
res
?.
data
?.
total
,
res
?.
data
?.
count
,
res
?.
hits
?.
total
?.
value
,
res
?.
hits
?.
total
,
];
return
values
.
find
((
value
)
=>
Number
.
isFinite
(
Number
(
value
)));
}
function
getAuditField
(
record
,
aliases
=
[])
{
const
sources
=
[
record
?.
metadata
,
record
?.
fields
,
record
?.
data
,
record
,
].
filter
(
Boolean
);
for
(
const
source
of
sources
)
{
for
(
const
alias
of
aliases
)
{
const
value
=
source
?.[
alias
];
if
(
value
!==
undefined
&&
value
!==
null
&&
value
!==
''
)
return
value
;
}
}
return
''
;
}
export
function
normalizeAuditRecord
(
record
)
{
const
normalized
=
{
id
:
record
?.
name
||
record
?.
_id
||
record
?.
id
||
''
,
title
:
record
?.
title
||
''
,
businessSegment
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
businessSegment
),
),
level1Project
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
level1Project
),
),
level2Project
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
level2Project
),
),
riskDescription
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
riskDescription
),
),
regulationBasis
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
regulationBasis
),
),
riskSource
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
riskSource
)),
riskLevel
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
riskLevel
)),
keywords
:
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
keywords
),
mdContent
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
mdContent
)),
enabled
:
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
enabled
),
sourceRow
:
toText
(
getAuditField
(
record
,
AUDIT_FIELD_ALIASES
.
sourceRow
)),
};
normalized
.
searchText
=
[
normalized
.
businessSegment
,
normalized
.
level1Project
,
normalized
.
level2Project
,
normalized
.
riskDescription
,
normalized
.
regulationBasis
,
normalized
.
riskSource
,
normalized
.
riskLevel
,
toText
(
normalized
.
keywords
),
]
.
filter
(
Boolean
)
.
join
(
' '
);
return
normalized
;
}
export
function
isAuditRecordEnabled
(
record
)
{
if
(
record
.
enabled
===
''
||
record
.
enabled
===
undefined
||
record
.
enabled
===
null
)
{
return
true
;
}
return
!
[
'false'
,
'0'
,
'否'
,
'停用'
,
'禁用'
].
includes
(
String
(
record
.
enabled
).
toLowerCase
(),
);
}
export
async
function
fetchAllAuditRiskPoints
(
queryRecords
)
{
const
records
=
[];
const
seenIds
=
new
Set
();
let
page
=
1
;
let
expectedTotal
=
null
;
while
(
page
<=
20
)
{
const
res
=
await
queryRecords
({
subject
:
AUDIT_RISK_POINT_SUBJECT
,
page
,
page_size
:
AUDIT_PAGE_SIZE
,
mode
:
'basic'
,
});
const
pageRecords
=
normalizeRecordList
(
res
);
expectedTotal
=
Number
(
getResponseTotal
(
res
))
||
expectedTotal
;
let
added
=
0
;
pageRecords
.
forEach
((
record
,
index
)
=>
{
const
normalized
=
normalizeAuditRecord
(
record
);
const
identity
=
normalized
.
id
||
`
${
page
}
:
${
index
}
:
${
auditRecordIdentity
(
normalized
,
true
)}
`
;
if
(
!
seenIds
.
has
(
identity
))
{
seenIds
.
add
(
identity
);
records
.
push
(
normalized
);
added
+=
1
;
}
});
if
(
pageRecords
.
length
===
0
||
pageRecords
.
length
<
AUDIT_PAGE_SIZE
||
added
===
0
||
(
expectedTotal
&&
records
.
length
>=
expectedTotal
)
)
{
break
;
}
page
+=
1
;
}
return
records
.
filter
(
(
record
)
=>
isAuditRecordEnabled
(
record
)
&&
(
record
.
riskDescription
||
record
.
mdContent
)
&&
(
record
.
businessSegment
||
record
.
level1Project
||
record
.
level2Project
),
);
}
export
function
auditRecordIdentity
(
record
,
includeContent
=
false
)
{
const
values
=
[
record
.
businessSegment
,
record
.
level1Project
,
record
.
level2Project
,
record
.
riskDescription
,
record
.
riskSource
,
];
if
(
includeContent
)
{
values
.
push
(
record
.
regulationBasis
,
record
.
riskLevel
,
record
.
mdContent
);
}
return
values
.
map
((
value
)
=>
toText
(
value
)).
join
(
'|'
);
}
export
function
buildRiskPointMarkdown
(
record
,
index
=
null
)
{
if
(
record
.
mdContent
)
{
return
index
?
`### 风险点
${
index
}
\n
${
record
.
mdContent
}
`
:
record
.
mdContent
;
}
const
heading
=
index
?
`### 风险点
${
index
}
`
:
'### 风险点'
;
return
[
`
${
heading
}
:
${[
record
.
businessSegment
||
'未分类业务板块'
,
record
.
level1Project
||
'未分类一级项目'
,
record
.
level2Project
||
'未分类二级项目'
,
].
join
(
' / '
)}
`
,
`- 来源行号:
${
record
.
sourceRow
||
'未知'
}
`
,
`- 业务板块:
${
record
.
businessSegment
||
''
}
`
,
`- 一级项目:
${
record
.
level1Project
||
''
}
`
,
`- 二级项目:
${
record
.
level2Project
||
''
}
`
,
`- 具体风险点描述:
${
record
.
riskDescription
||
''
}
`
,
`- 法规依据:
${
record
.
regulationBasis
||
''
}
`
,
`- 风险点来源:
${
record
.
riskSource
||
''
}
`
,
`- 风险等级:
${
record
.
riskLevel
||
''
}
`
,
].
join
(
'
\
n'
);
}
export
function
extractSourceConstraints
(
query
=
''
)
{
const
constraints
=
[];
const
patterns
=
[
/
(?:
来自|来源
(?:
为|是|于
)?
|源自|出自
)([\u
4e00-
\u
9fffA-Za-z0-9()()·
\-\s]{2,40})
/g
,
/
(?:
与|和|跟
)([\u
4e00-
\u
9fffA-Za-z0-9()()·
\-\s]{2,30})(?:
有关|相关
)
/g
,
];
patterns
.
forEach
((
pattern
)
=>
{
for
(
const
match
of
String
(
query
).
matchAll
(
pattern
))
{
const
value
=
match
[
1
]
.
split
(
/的|中|里|内|相关|有关|关于|检查点|检查|审计|风险点|内容|资料|记录|清单|,|,|。|;|;/
,
)[
0
]
.
trim
();
if
(
value
.
length
>=
2
&&
!
constraints
.
includes
(
value
))
{
constraints
.
push
(
value
);
}
}
});
[
'安永'
,
'普华永道'
,
'德勤'
,
'毕马威'
,
'证监局'
,
'中基协'
].
forEach
(
(
alias
)
=>
{
if
(
String
(
query
).
includes
(
alias
)
&&
!
constraints
.
includes
(
alias
))
{
constraints
.
push
(
alias
);
}
},
);
return
constraints
;
}
function
extractPrimaryKeywords
(
query
=
''
)
{
const
results
=
new
Set
();
String
(
query
)
.
match
(
/
[\u
4e00-
\u
9fffA-Za-z0-9
]{2,}
/g
)
?.
forEach
((
token
)
=>
{
token
.
split
(
/专项|审计|业务|风险|检查|项目|清单|生成|查询|检索|相关/
)
.
map
((
item
)
=>
item
.
trim
())
.
filter
((
item
)
=>
item
.
length
>=
2
&&
!
AUDIT_STOP_WORDS
.
has
(
item
))
.
forEach
((
item
)
=>
results
.
add
(
item
));
});
return
Array
.
from
(
results
);
}
export
function
extractAuditKeywords
(
query
=
''
,
extraKeywords
=
[])
{
const
keywords
=
new
Set
(
extractPrimaryKeywords
(
query
));
const
domains
=
[];
const
cleaned
=
String
(
query
).
replace
(
/
[\s
,。;、,.;::()()
]
+/g
,
''
);
Object
.
entries
(
AUDIT_DOMAIN_KEYWORDS
).
forEach
(([
domain
,
values
])
=>
{
if
(
cleaned
.
includes
(
domain
)
||
values
.
some
((
value
)
=>
cleaned
.
includes
(
value
))
)
{
domains
.
push
(
domain
);
keywords
.
add
(
domain
);
values
.
forEach
((
value
)
=>
keywords
.
add
(
value
));
}
});
extraKeywords
.
map
((
keyword
)
=>
toText
(
keyword
))
.
filter
((
keyword
)
=>
keyword
.
length
>=
2
&&
keyword
.
length
<=
12
)
.
forEach
((
keyword
)
=>
keywords
.
add
(
keyword
));
return
{
domains
,
primaryKeywords
:
extractPrimaryKeywords
(
query
),
keywords
:
Array
.
from
(
keywords
)
.
filter
((
keyword
)
=>
!
AUDIT_STOP_WORDS
.
has
(
keyword
))
.
sort
((
a
,
b
)
=>
b
.
length
-
a
.
length
),
sourceConstraints
:
extractSourceConstraints
(
query
),
};
}
function
categoryMatches
(
value
,
categories
)
{
return
categories
.
some
(
(
category
)
=>
category
&&
value
&&
(
value
.
includes
(
category
)
||
category
.
includes
(
value
)),
);
}
function
getRelevantCategories
(
records
,
keywordInfo
)
{
const
candidates
=
new
Set
();
records
.
forEach
((
record
)
=>
{
[
record
.
businessSegment
,
record
.
level1Project
,
record
.
level2Project
,
].
forEach
((
category
)
=>
{
if
(
category
&&
keywordInfo
.
keywords
.
some
((
keyword
)
=>
category
.
includes
(
keyword
))
)
{
candidates
.
add
(
category
);
}
});
});
return
Array
.
from
(
candidates
);
}
function
ngramCoverage
(
query
,
text
)
{
const
cleanedQuery
=
extractPrimaryKeywords
(
query
).
join
(
''
);
if
(
cleanedQuery
.
length
<
2
||
!
text
)
return
0
;
const
grams
=
new
Set
();
for
(
let
index
=
0
;
index
<
cleanedQuery
.
length
-
1
;
index
+=
1
)
{
grams
.
add
(
cleanedQuery
.
slice
(
index
,
index
+
2
));
}
if
(
!
grams
.
size
)
return
0
;
const
matched
=
Array
.
from
(
grams
).
filter
((
gram
)
=>
text
.
includes
(
gram
),
).
length
;
return
matched
/
grams
.
size
;
}
function
matchScoreLevel
(
score
)
{
if
(
score
>=
85
)
return
'高'
;
if
(
score
>=
70
)
return
'较高'
;
if
(
score
>=
55
)
return
'中'
;
return
'低'
;
}
export
function
scoreAuditRecord
(
record
,
query
,
keywordInfo
,
relevantCategories
,
)
{
if
(
keywordInfo
.
sourceConstraints
.
length
&&
!
keywordInfo
.
sourceConstraints
.
some
((
source
)
=>
record
.
riskSource
.
includes
(
source
),
)
)
{
return
null
;
}
let
score
=
0
;
const
reasons
=
[];
const
sourceMatched
=
keywordInfo
.
sourceConstraints
.
some
((
source
)
=>
record
.
riskSource
.
includes
(
source
),
);
if
(
sourceMatched
)
{
score
+=
0.36
;
reasons
.
push
(
'来源约束'
);
}
if
(
categoryMatches
(
record
.
businessSegment
,
relevantCategories
))
{
score
+=
0.18
;
reasons
.
push
(
'业务板块'
);
}
if
(
categoryMatches
(
record
.
level1Project
,
relevantCategories
))
{
score
+=
0.12
;
reasons
.
push
(
'一级项目'
);
}
if
(
categoryMatches
(
record
.
level2Project
,
relevantCategories
))
{
score
+=
0.12
;
reasons
.
push
(
'二级项目'
);
}
const
matchedKeywords
=
[];
keywordInfo
.
keywords
.
forEach
((
keyword
)
=>
{
if
(
!
keyword
)
return
;
if
(
record
.
level2Project
.
includes
(
keyword
))
score
+=
0.12
;
else
if
(
record
.
level1Project
.
includes
(
keyword
)
||
record
.
businessSegment
.
includes
(
keyword
)
)
score
+=
0.09
;
else
if
(
record
.
riskSource
.
includes
(
keyword
))
score
+=
0.16
;
else
if
(
record
.
searchText
.
includes
(
keyword
))
score
+=
0.06
;
else
return
;
matchedKeywords
.
push
(
keyword
);
});
const
primaryMatched
=
keywordInfo
.
primaryKeywords
.
filter
((
keyword
)
=>
record
.
searchText
.
includes
(
keyword
),
);
if
(
primaryMatched
.
length
)
{
score
+=
Math
.
min
(
0.62
,
0.5
+
(
primaryMatched
.
length
-
1
)
*
0.06
);
reasons
.
push
(
`主题词:
${
primaryMatched
.
slice
(
0
,
3
).
join
(
'、'
)}
`
);
}
else
if
(
matchedKeywords
.
length
)
{
score
+=
Math
.
min
(
0.5
,
0.3
+
(
matchedKeywords
.
length
-
1
)
*
0.05
);
reasons
.
push
(
`扩展词:
${
matchedKeywords
.
slice
(
0
,
3
).
join
(
'、'
)}
`
);
}
const
phraseCoverage
=
ngramCoverage
(
query
,
record
.
searchText
);
if
(
phraseCoverage
>
0
)
{
score
+=
Math
.
min
(
0.22
,
phraseCoverage
*
0.22
);
}
if
(
sourceMatched
&&
matchedKeywords
.
length
)
score
=
Math
.
max
(
score
,
1.08
);
const
matchScore
=
Math
.
max
(
0
,
Math
.
min
(
100
,
Math
.
round
((
Number
(
score
)
/
1.3
)
*
100
)),
);
return
{
...
record
,
matchScore
,
matchLevel
:
matchScoreLevel
(
matchScore
),
matchReasons
:
reasons
,
};
}
export
function
groupAuditRecords
(
records
=
[])
{
const
businessMap
=
new
Map
();
const
seen
=
new
Set
();
records
.
forEach
((
record
)
=>
{
const
identity
=
auditRecordIdentity
(
record
,
true
);
if
(
seen
.
has
(
identity
))
return
;
seen
.
add
(
identity
);
const
business
=
record
.
businessSegment
||
'未分类'
;
const
level1
=
record
.
level1Project
||
'未分类'
;
const
level2
=
record
.
level2Project
||
'未分类'
;
if
(
!
businessMap
.
has
(
business
))
businessMap
.
set
(
business
,
new
Map
());
const
level1Map
=
businessMap
.
get
(
business
);
if
(
!
level1Map
.
has
(
level1
))
level1Map
.
set
(
level1
,
new
Map
());
const
level2Map
=
level1Map
.
get
(
level1
);
if
(
!
level2Map
.
has
(
level2
))
level2Map
.
set
(
level2
,
[]);
level2Map
.
get
(
level2
).
push
(
record
);
});
return
Array
.
from
(
businessMap
,
([
businessSegment
,
level1Map
])
=>
({
businessSegment
,
level1Projects
:
Array
.
from
(
level1Map
,
([
level1Project
,
level2Map
])
=>
({
level1Project
,
level2Projects
:
Array
.
from
(
level2Map
,
([
level2Project
,
risks
])
=>
({
level2Project
,
risks
:
risks
.
sort
((
a
,
b
)
=>
b
.
matchScore
-
a
.
matchScore
),
})),
})),
}));
}
export
function
classifyAuditIntent
(
query
=
''
)
{
if
(
GENERATE_PATTERNS
.
some
((
pattern
)
=>
query
.
includes
(
pattern
)))
return
'checklist'
;
if
(
QA_PATTERNS
.
some
((
pattern
)
=>
query
.
includes
(
pattern
)))
return
'qa'
;
return
'checklist'
;
}
function
cleanInstructionValue
(
value
=
''
)
{
return
value
.
replace
(
/相关的|有关的|相关|有关|风险点|检查点|内容|类别|业务|板块|项目/g
,
''
,
)
.
replace
(
/
[
,,。;;!!??
]
/g
,
' '
)
.
replace
(
/的$/g
,
''
)
.
trim
();
}
export
function
parseRefineInstruction
(
query
=
''
,
previousThreshold
)
{
const
instruction
=
{
keep
:
[],
remove
:
[],
add
:
[],
threshold
:
previousThreshold
||
AUDIT_MATCH_THRESHOLD
,
};
const
thresholdMatch
=
query
.
match
(
/
(?:
阈值|最低分
(?:
值
)?
|匹配度
)[^
0-9
]{0,8}(\d{1,3})
/
,
);
if
(
thresholdMatch
)
{
instruction
.
threshold
=
Math
.
max
(
0
,
Math
.
min
(
100
,
Number
(
thresholdMatch
[
1
])),
);
}
[
{
pattern
:
/
(?:
去掉|删除|不要|排除
)([^
,,。;;
]
+
)
/g
,
key
:
'remove'
},
{
pattern
:
/
(?:
只保留|仅保留|保留
)([^
,,。;;
]
+
)
/g
,
key
:
'keep'
},
{
pattern
:
/
(?:
增加|加入|补充
)([^
,,。;;
]
+
)
/g
,
key
:
'add'
},
].
forEach
(({
pattern
,
key
})
=>
{
for
(
const
match
of
query
.
matchAll
(
pattern
))
{
cleanInstructionValue
(
match
[
1
])
.
split
(
/和|与|以及|、|
\s
+/
)
.
filter
((
value
)
=>
value
.
length
>=
2
)
.
forEach
((
value
)
=>
instruction
[
key
].
push
(
value
));
}
});
return
instruction
;
}
function
recordMatchesTerms
(
record
,
terms
)
{
return
terms
.
some
((
term
)
=>
term
&&
record
.
searchText
.
includes
(
term
));
}
function
scoreAllRecords
(
records
,
query
,
extraKeywords
=
[])
{
const
keywordInfo
=
extractAuditKeywords
(
query
,
extraKeywords
);
const
relevantCategories
=
getRelevantCategories
(
records
,
keywordInfo
);
return
records
.
map
((
record
)
=>
scoreAuditRecord
(
record
,
query
,
keywordInfo
,
relevantCategories
),
)
.
filter
(
Boolean
)
.
sort
((
a
,
b
)
=>
b
.
matchScore
-
a
.
matchScore
);
}
export
function
isAuditRefineRequest
(
query
,
previousResult
)
{
return
Boolean
(
previousResult
&&
REFINE_PATTERNS
.
some
((
pattern
)
=>
query
.
includes
(
pattern
)),
);
}
export
function
buildAuditResult
(
query
,
records
,
previousResult
=
null
,
options
=
{},
)
{
const
isRefine
=
isAuditRefineRequest
(
query
,
previousResult
);
const
baseQuery
=
isRefine
?
previousResult
.
project
:
query
;
const
instruction
=
isRefine
?
parseRefineInstruction
(
query
,
previousResult
.
threshold
)
:
{
keep
:
[],
remove
:
[],
add
:
[],
threshold
:
AUDIT_MATCH_THRESHOLD
};
const
scoringQuery
=
instruction
.
add
.
length
?
`
${
baseQuery
}
${
instruction
.
add
.
join
(
' '
)}
`
:
baseQuery
;
let
allScored
=
scoreAllRecords
(
records
,
scoringQuery
,
options
.
extraKeywords
||
[],
);
if
(
instruction
.
remove
.
length
)
{
allScored
=
allScored
.
filter
(
(
record
)
=>
!
recordMatchesTerms
(
record
,
instruction
.
remove
),
);
}
if
(
instruction
.
keep
.
length
)
{
allScored
=
allScored
.
filter
((
record
)
=>
recordMatchesTerms
(
record
,
instruction
.
keep
),
);
}
const
selected
=
allScored
.
filter
(
(
record
)
=>
record
.
matchScore
>=
instruction
.
threshold
,
);
return
{
version
:
1
,
kind
:
classifyAuditIntent
(
isRefine
?
baseQuery
:
query
),
project
:
baseQuery
,
request
:
query
,
refined
:
Boolean
(
isRefine
),
instruction
,
expandedKeywords
:
options
.
extraKeywords
||
[],
threshold
:
instruction
.
threshold
,
totalCount
:
records
.
length
,
scoredCount
:
allScored
.
length
,
matchedCount
:
selected
.
length
,
records
:
selected
,
groups
:
groupAuditRecords
(
selected
),
generatedAt
:
new
Date
().
toISOString
(),
};
}
export
function
findPreviousAuditResult
(
messages
=
[])
{
for
(
let
index
=
messages
.
length
-
1
;
index
>=
0
;
index
-=
1
)
{
if
(
messages
[
index
]?.
auditResult
)
return
messages
[
index
].
auditResult
;
if
(
messages
[
index
]?.
message
?.
auditResult
)
return
messages
[
index
].
message
.
auditResult
;
}
return
null
;
}
export
function
buildAuditKnowledgePrompt
(
result
)
{
const
records
=
result
.
records
.
slice
(
0
,
AUDIT_PROMPT_RECORD_LIMIT
);
const
context
=
records
.
map
(
(
record
,
index
)
=>
`
${
buildRiskPointMarkdown
(
record
,
index
+
1
)}
\n- 规则匹配度:
${
record
.
matchScore
}
分(
${
record
.
matchLevel
}
)\n- 命中依据:
${
record
.
matchReasons
.
join
(
'、'
)
||
'字段词项覆盖'
}
`
,
)
.
join
(
'
\
n
\
n'
);
const
stats
=
`知识库共
${
result
.
totalCount
}
条,全量评分
${
result
.
scoredCount
}
条,匹配度 >=
${
result
.
threshold
}
分的有
${
result
.
matchedCount
}
条。`
;
if
(
!
records
.
length
)
{
return
[
'审计风险点知识库检索结果:'
,
stats
,
'没有达到当前阈值的风险点。请明确说明知识库覆盖不足或建议用户降低匹配度阈值,不得编造风险点。'
,
`用户请求:
${
result
.
request
}
`
,
''
,
].
join
(
'
\
n'
);
}
if
(
result
.
kind
===
'qa'
)
{
return
[
'审计风险点知识库问答上下文:'
,
stats
,
AUDIT_CHECKLIST_RULES
,
context
,
''
,
'请简洁回答用户问题,并引用来源行号和匹配度。不要生成完整审计清单。'
,
''
,
].
join
(
'
\
n'
);
}
return
[
'审计检查清单知识库上下文:'
,
stats
,
AUDIT_CHECKLIST_RULES
,
context
,
''
,
'风险点层级和完整详情由系统结构化展示,你只生成以下六个清单要素,不要重复展开风险点:'
,
'## 审计项目'
,
'使用业务人员输入的审计项目名称。'
,
'## 审计对象'
,
'结合审计项目与命中内容,参考公募基金公司组织架构列示可能涉及的部门。'
,
'## 审计方法'
,
'列示可能使用的访谈、穿行测试、抽样、数据分析、凭证核验等审计技术。'
,
'## 审计人员'
,
'给出角色配置建议,不编造真实姓名。'
,
'## 审计时间'
,
'给出阶段安排,不编造已经发生的日期。'
,
'## 注意事项'
,
'提示保密、取证、样本代表性及专业判断等事项。'
,
result
.
refined
?
`本次调整指令:
${
result
.
request
}
`
:
''
,
''
,
]
.
filter
(
Boolean
)
.
join
(
'
\
n'
);
}
src/features/audit/excel.js
0 → 100644
View file @
5919a2cf
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
;
}
src/features/audit/export.js
0 → 100644
View file @
5919a2cf
import
JSZip
from
'jszip'
;
import
*
as
XLSX
from
'xlsx'
;
import
{
groupAuditRecords
}
from
'./engine'
;
const
SUMMARY_FIELDS
=
[
'审计项目'
,
'审计对象'
,
'审计方法'
,
'审计人员'
,
'审计时间'
,
'注意事项'
,
];
const
WORD_SECTION_ORDER
=
[
'审计项目'
,
'审计对象'
,
'审计内容'
,
'审计方法'
,
'审计人员'
,
'审计时间'
,
'注意事项'
,
];
const
WORD_FONT
=
'宋体'
;
function
escapeXml
(
value
=
''
)
{
return
String
(
value
)
.
replace
(
/&/g
,
'&'
)
.
replace
(
/</g
,
'<'
)
.
replace
(
/>/g
,
'>'
)
.
replace
(
/"/g
,
'"'
)
.
replace
(
/'/g
,
'''
);
}
function
safeFileName
(
value
=
'审计检查清单'
)
{
return
String
(
value
)
.
replace
(
/
[\\/
:*?"<>|
]
/g
,
'-'
)
.
replace
(
/
\s
+/g
,
'_'
)
.
slice
(
0
,
80
);
}
function
stripMarkdown
(
value
=
''
)
{
return
String
(
value
)
.
replace
(
/```
[\s\S]
*
?
```/g
,
''
)
.
replace
(
/<
[^
>
]
+>/g
,
''
)
.
replace
(
/^#
{1,6}\s
+/gm
,
''
)
.
replace
(
/
\*\*
|__/g
,
''
)
.
replace
(
/^
[
-*+
]\s
+/gm
,
''
)
.
replace
(
/
\[(
.*
?)\]\([^
)
]
*
\)
/g
,
'$1'
)
.
trim
();
}
export
function
parseAuditSummary
(
content
=
''
,
project
=
''
)
{
const
summary
=
{
审计项目
:
project
};
SUMMARY_FIELDS
.
forEach
((
field
,
index
)
=>
{
const
nextFields
=
SUMMARY_FIELDS
.
slice
(
index
+
1
)
.
map
((
item
)
=>
`#{1,6}\\s*
${
item
}
`
)
.
join
(
'|'
);
const
pattern
=
new
RegExp
(
`#{1,6}\\s*
${
field
}
\\s*\\n([\\s\\S]*?)(?=
${
nextFields
?
`(?:
${
nextFields
}
)|$`
:
'$'
}
)`
,
'i'
,
);
const
match
=
String
(
content
).
match
(
pattern
);
if
(
match
?.[
1
]?.
trim
())
summary
[
field
]
=
stripMarkdown
(
match
[
1
]);
});
summary
.
审计项目
=
summary
.
审计项目
||
project
;
summary
.
审计对象
=
summary
.
审计对象
||
'根据审计范围确定相关业务及管理部门'
;
summary
.
审计方法
=
summary
.
审计方法
||
'访谈、穿行测试、抽样检查、数据分析及凭证核验'
;
summary
.
审计人员
=
summary
.
审计人员
||
'由审计负责人按专业分工配置'
;
summary
.
审计时间
=
summary
.
审计时间
||
'按审计准备、现场实施和报告阶段安排'
;
summary
.
注意事项
=
summary
.
注意事项
||
'注意资料保密、证据留痕、样本代表性和专业判断'
;
return
summary
;
}
export
function
filterAuditRecords
(
result
,
threshold
=
result
.
threshold
)
{
return
(
result
.
records
||
[]).
filter
(
(
record
)
=>
Number
(
record
.
matchScore
)
>=
Number
(
threshold
),
);
}
export
function
buildChecklistMarkdown
(
result
,
aiContent
=
''
,
threshold
)
{
const
summary
=
parseAuditSummary
(
aiContent
,
result
.
project
);
const
records
=
filterAuditRecords
(
result
,
threshold
);
const
groups
=
groupAuditRecords
(
records
);
const
lines
=
[
`# 审计检查清单:
${
summary
.
审计项目
}
`
,
''
];
SUMMARY_FIELDS
.
forEach
((
field
)
=>
{
lines
.
push
(
`##
${
field
}
`
,
summary
[
field
]
||
''
,
''
);
});
lines
.
push
(
'## 审计内容'
,
''
);
if
(
!
groups
.
length
)
{
lines
.
push
(
'当前匹配度阈值下无风险点。'
);
}
groups
.
forEach
((
business
,
businessIndex
)
=>
{
lines
.
push
(
`###
${
businessIndex
+
1
}
.
${
business
.
businessSegment
}
`
,
''
);
business
.
level1Projects
.
forEach
((
level1
,
level1Index
)
=>
{
lines
.
push
(
`####
${
businessIndex
+
1
}
.
${
level1Index
+
1
}
${
level1
.
level1Project
}
`
,
''
,
);
level1
.
level2Projects
.
forEach
((
level2
,
level2Index
)
=>
{
lines
.
push
(
`#####
${
businessIndex
+
1
}
.
${
level1Index
+
1
}
.
${
level2Index
+
1
}
${
level2
.
level2Project
}
`
,
''
,
);
level2
.
risks
.
forEach
((
risk
,
riskIndex
)
=>
{
lines
.
push
(
`**风险点
${
riskIndex
+
1
}
(来源行号:
${
risk
.
sourceRow
||
'未知'
}
)**`
,
''
,
`- 匹配度:
${
risk
.
matchScore
}
分(
${
risk
.
matchLevel
}
)`
,
`- 具体风险点描述:
${
risk
.
riskDescription
||
''
}
`
,
`- 法规依据:
${
risk
.
regulationBasis
||
''
}
`
,
`- 风险点来源:
${
risk
.
riskSource
||
''
}
`
,
`- 风险等级:
${
risk
.
riskLevel
||
''
}
`
,
''
,
);
});
});
});
});
return
lines
.
join
(
'
\
n'
).
trim
();
}
function
downloadBlob
(
blob
,
fileName
)
{
const
url
=
URL
.
createObjectURL
(
blob
);
const
anchor
=
document
.
createElement
(
'a'
);
anchor
.
href
=
url
;
anchor
.
download
=
fileName
;
document
.
body
.
appendChild
(
anchor
);
anchor
.
click
();
anchor
.
remove
();
window
.
setTimeout
(()
=>
URL
.
revokeObjectURL
(
url
),
1000
);
}
export
function
exportAuditMarkdown
(
result
,
aiContent
,
threshold
)
{
const
markdown
=
buildChecklistMarkdown
(
result
,
aiContent
,
threshold
);
downloadBlob
(
new
Blob
([
markdown
],
{
type
:
'text/markdown;charset=utf-8'
}),
`
${
safeFileName
(
result
.
project
)}
.md`
,
);
}
export
function
exportAuditExcel
(
result
,
aiContent
,
threshold
)
{
const
summary
=
parseAuditSummary
(
aiContent
,
result
.
project
);
const
records
=
filterAuditRecords
(
result
,
threshold
);
const
workbook
=
XLSX
.
utils
.
book_new
();
const
summarySheet
=
XLSX
.
utils
.
json_to_sheet
(
SUMMARY_FIELDS
.
map
((
field
)
=>
({
清单要素
:
field
,
内容
:
summary
[
field
]
||
''
,
})),
);
summarySheet
[
'!cols'
]
=
[{
wch
:
14
},
{
wch
:
90
}];
XLSX
.
utils
.
book_append_sheet
(
workbook
,
summarySheet
,
'审计方案'
);
const
riskSheet
=
XLSX
.
utils
.
json_to_sheet
(
records
.
map
((
record
,
index
)
=>
({
序号
:
index
+
1
,
业务板块
:
record
.
businessSegment
,
一级项目
:
record
.
level1Project
,
二级项目
:
record
.
level2Project
,
匹配度
:
record
.
matchScore
,
匹配等级
:
record
.
matchLevel
,
具体风险点描述
:
record
.
riskDescription
,
法规依据
:
record
.
regulationBasis
,
风险点来源
:
record
.
riskSource
,
风险等级
:
record
.
riskLevel
,
来源行号
:
record
.
sourceRow
,
})),
);
riskSheet
[
'!cols'
]
=
[
{
wch
:
8
},
{
wch
:
18
},
{
wch
:
22
},
{
wch
:
24
},
{
wch
:
10
},
{
wch
:
10
},
{
wch
:
60
},
{
wch
:
50
},
{
wch
:
22
},
{
wch
:
12
},
{
wch
:
10
},
];
XLSX
.
utils
.
book_append_sheet
(
workbook
,
riskSheet
,
'风险点明细'
);
XLSX
.
writeFile
(
workbook
,
`
${
safeFileName
(
result
.
project
)}
.xlsx`
);
}
function
wordText
(
value
=
''
)
{
return
String
(
value
)
.
split
(
'
\
n'
)
.
map
(
(
line
,
index
)
=>
`
${
index
?
'<w:br/>'
:
''
}
<w:t xml:space="preserve">
${
escapeXml
(
line
,
)}
</w:t>`
,
)
.
join
(
''
);
}
function
wordRun
(
text
,
{
bold
=
false
,
color
=
''
,
size
=
''
,
font
=
WORD_FONT
}
=
{},
)
{
const
properties
=
[
font
?
`<w:rFonts w:ascii="
${
font
}
" w:eastAsia="
${
font
}
" w:hAnsi="
${
font
}
"/>`
:
''
,
bold
?
'<w:b/><w:bCs/>'
:
''
,
color
?
`<w:color w:val="
${
color
}
"/>`
:
''
,
size
?
`<w:sz w:val="
${
size
}
"/><w:szCs w:val="
${
size
}
"/>`
:
''
,
].
join
(
''
);
return
`<w:r>
${
properties
?
`<w:rPr>
${
properties
}
</w:rPr>`
:
''
}${
wordText
(
text
,
)}
</w:r>`
;
}
function
wordParagraph
(
runs
=
[],
{
style
=
''
,
alignment
=
''
,
firstLine
=
''
,
leftIndent
=
''
,
spacingAfter
=
''
,
}
=
{},
)
{
const
properties
=
[
style
?
`<w:pStyle w:val="
${
style
}
"/>`
:
''
,
alignment
?
`<w:jc w:val="
${
alignment
}
"/>`
:
''
,
firstLine
?
`<w:ind w:firstLine="
${
firstLine
}
"/>`
:
''
,
leftIndent
?
`<w:ind w:left="
${
leftIndent
}
"/>`
:
''
,
spacingAfter
!==
''
?
`<w:spacing w:after="
${
spacingAfter
}
"/>`
:
''
,
].
join
(
''
);
return
`<w:p>
${
properties
?
`<w:pPr>
${
properties
}
</w:pPr>`
:
''
}${
Array
.
isArray
(
runs
)
?
runs
.
join
(
''
)
:
runs
}
</w:p>`
;
}
function
formatWordDate
(
value
=
new
Date
())
{
const
pad
=
(
number
)
=>
String
(
number
).
padStart
(
2
,
'0'
);
return
`
${
value
.
getFullYear
()}
-
${
pad
(
value
.
getMonth
()
+
1
)}
-
${
pad
(
value
.
getDate
(),
)}
${
pad
(
value
.
getHours
())}
:
${
pad
(
value
.
getMinutes
())}
:
${
pad
(
value
.
getSeconds
(),
)}
`
;
}
function
createWordStylesXml
()
{
const
fonts
=
`<w:rFonts w:ascii="
${
WORD_FONT
}
" w:eastAsia="
${
WORD_FONT
}
" w:hAnsi="
${
WORD_FONT
}
"/>`
;
return
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:docDefaults>
<w:rPrDefault><w:rPr>
${
fonts
}
<w:sz w:val="22"/><w:szCs w:val="22"/><w:lang w:val="zh-CN" w:eastAsia="zh-CN"/></w:rPr></w:rPrDefault>
<w:pPrDefault><w:pPr><w:spacing w:after="200" w:line="276" w:lineRule="auto"/></w:pPr></w:pPrDefault>
</w:docDefaults>
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style>
<w:style w:type="paragraph" w:styleId="Title">
<w:name w:val="Title"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/>
<w:pPr><w:pBdr><w:bottom w:val="single" w:sz="8" w:space="4" w:color="4F81BD"/></w:pBdr><w:spacing w:after="300" w:line="240" w:lineRule="auto"/><w:contextualSpacing/></w:pPr>
<w:rPr>
${
fonts
}
<w:color w:val="17365D"/><w:spacing w:val="5"/><w:kern w:val="28"/><w:sz w:val="52"/><w:szCs w:val="52"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/>
<w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="480" w:after="0"/><w:outlineLvl w:val="0"/></w:pPr>
<w:rPr>
${
fonts
}
<w:b/><w:bCs/><w:color w:val="365F91"/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading2">
<w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/>
<w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="200" w:after="0"/><w:outlineLvl w:val="1"/></w:pPr>
<w:rPr>
${
fonts
}
<w:b/><w:bCs/><w:color w:val="4F81BD"/><w:sz w:val="24"/><w:szCs w:val="24"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading3">
<w:name w:val="heading 3"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/>
<w:pPr><w:keepNext/><w:keepLines/><w:spacing w:before="200" w:after="0"/><w:outlineLvl w:val="2"/></w:pPr>
<w:rPr>
${
fonts
}
<w:b/><w:bCs/><w:color w:val="4F81BD"/><w:sz w:val="22"/><w:szCs w:val="22"/></w:rPr>
</w:style>
</w:styles>`
;
}
function
createWordDocumentXml
(
result
,
aiContent
,
threshold
)
{
const
summary
=
parseAuditSummary
(
aiContent
,
result
.
project
);
const
groups
=
groupAuditRecords
(
filterAuditRecords
(
result
,
threshold
));
const
body
=
[
wordParagraph
(
wordRun
(
'智能审计清单'
),
{
style
:
'Title'
,
alignment
:
'center'
,
}),
wordParagraph
(
wordRun
(
`生成时间:
${
formatWordDate
()}
`
,
{
size
:
'18'
,
color
:
'808080'
,
}),
{
alignment
:
'right'
},
),
wordParagraph
([
wordRun
(
'审计项目描述:'
,
{
bold
:
true
}),
wordRun
(
result
.
project
||
summary
.
审计项目
||
''
),
]),
wordParagraph
(
''
,
{
spacingAfter
:
'0'
}),
];
WORD_SECTION_ORDER
.
forEach
((
field
)
=>
{
body
.
push
(
wordParagraph
(
wordRun
(
field
),
{
style
:
'Heading1'
}));
if
(
field
!==
'审计内容'
)
{
body
.
push
(
wordParagraph
(
wordRun
(
stripMarkdown
(
summary
[
field
]
||
''
)),
{
firstLine
:
'420'
,
}),
);
return
;
}
if
(
!
groups
.
length
)
{
body
.
push
(
wordParagraph
(
wordRun
(
'(当前匹配度阈值下无风险点)'
),
{
firstLine
:
'420'
,
}),
);
return
;
}
groups
.
forEach
((
business
,
businessIndex
)
=>
{
body
.
push
(
wordParagraph
(
wordRun
(
`业务板块
${
businessIndex
+
1
}
:
${
business
.
businessSegment
}
`
,
{
size
:
'24'
},
),
{
style
:
'Heading2'
},
),
);
business
.
level1Projects
.
forEach
((
level1
,
level1Index
)
=>
{
body
.
push
(
wordParagraph
(
wordRun
(
`一级项目
${
level1Index
+
1
}
:
${
level1
.
level1Project
}
`
,
{
size
:
'22'
,
}),
{
style
:
'Heading3'
},
),
);
level1
.
level2Projects
.
forEach
((
level2
,
level2Index
)
=>
{
body
.
push
(
wordParagraph
(
wordRun
(
`二级项目
${
level2Index
+
1
}
:
${
level2
.
level2Project
}
`
,
{
bold
:
true
,
size
:
'24'
,
}),
{
leftIndent
:
'227'
},
),
);
level2
.
risks
.
forEach
((
risk
,
riskIndex
)
=>
{
const
riskLevel
=
risk
.
riskLevel
||
''
;
const
riskLevelColor
=
riskLevel
.
includes
(
'高'
)
?
'C00000'
:
riskLevel
.
includes
(
'中'
)
?
'C08000'
:
''
;
const
detailParagraph
=
(
label
,
value
,
color
=
''
)
=>
wordParagraph
(
[
wordRun
(
`
${
label
}
:`
,
{
bold
:
true
,
size
:
'21'
}),
wordRun
(
value
||
'(无)'
,
{
color
,
size
:
'21'
}),
],
{
leftIndent
:
'454'
},
);
body
.
push
(
wordParagraph
(
wordRun
(
`风险点
${
riskIndex
+
1
}
(源文件第
${
risk
.
sourceRow
||
'未知'
}
行)`
,
{
bold
:
true
,
size
:
'21'
},
),
{
leftIndent
:
'283'
},
),
detailParagraph
(
'匹配度'
,
`
${
risk
.
matchScore
}
分(
${
risk
.
matchLevel
||
''
}
)`
,
),
detailParagraph
(
'具体风险点描述'
,
risk
.
riskDescription
),
detailParagraph
(
'法规依据'
,
risk
.
regulationBasis
),
detailParagraph
(
'风险点来源'
,
risk
.
riskSource
),
detailParagraph
(
'风险等级'
,
riskLevel
,
riskLevelColor
),
);
});
});
});
body
.
push
(
wordParagraph
(
''
));
});
});
return
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>
${
body
.
join
(
''
,
)}
<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="720" w:footer="720" w:gutter="0"/><w:cols w:space="720"/><w:docGrid w:linePitch="360"/></w:sectPr></w:body></w:document>`
;
}
export
async
function
createAuditWordBlob
(
result
,
aiContent
,
threshold
)
{
const
zip
=
new
JSZip
();
zip
.
file
(
'[Content_Types].xml'
,
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/></Types>`
,
);
zip
.
folder
(
'_rels'
)
.
file
(
'.rels'
,
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`
,
);
const
word
=
zip
.
folder
(
'word'
);
word
.
file
(
'document.xml'
,
createWordDocumentXml
(
result
,
aiContent
,
threshold
),
);
word
.
file
(
'styles.xml'
,
createWordStylesXml
());
word
.
folder
(
'_rels'
)
.
file
(
'document.xml.rels'
,
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`
,
);
return
zip
.
generateAsync
({
type
:
'blob'
,
mimeType
:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
,
});
}
export
async
function
exportAuditWord
(
result
,
aiContent
,
threshold
)
{
downloadBlob
(
await
createAuditWordBlob
(
result
,
aiContent
,
threshold
),
`
${
safeFileName
(
result
.
project
)}
.docx`
,
);
}
src/pages/Chat/index.jsx
View file @
5919a2cf
...
...
@@ -39,15 +39,29 @@ import { marked } from 'marked';
import
marketKatex
from
'marked-katex-extension'
;
import
{
forwardRef
,
useCallback
,
useEffect
,
useImperativeHandle
,
useMemo
,
useRef
,
useState
,
}
from
'react'
;
import
{
AuditChecklistResult
,
AuditKnowledgeBar
,
AuditKnowledgeImportDialog
,
}
from
'../../features/audit/components'
;
import
{
buildAuditKnowledgePrompt
,
buildAuditResult
,
fetchAllAuditRiskPoints
,
findPreviousAuditResult
,
isAuditRefineRequest
,
}
from
'../../features/audit/engine'
;
import
{
callMcpTool
,
createConversation
,
createRecord
,
documentAnalyze
,
documentRerank
,
extractLlmChunkContent
,
...
...
@@ -55,10 +69,12 @@ import {
imageAnalyze
,
llmChat
,
queryMcps
,
queryRecords
,
queryTopics
,
resolveLlmModel
,
semanticSearch
,
updateConversation
,
updateRecord
,
}
from
'../../services/DataService'
;
// 导入发票助手组件
...
...
@@ -94,11 +110,10 @@ const AGENT_DEFINITIONS = [
key
:
'audit-checklist'
,
icon
:
<
FileDoneOutlined
/>,
label
:
'审计检查清单生成'
,
description
:
'根据当前审计项目、整改问题和责任单位上下文,生成可执行的审计检查清单。'
,
description
:
'基于审计风险点知识库,生成可执行的审计检查清单。'
,
prompt
:
'你是审计检查清单生成智能体。你需要把审计
项目、整改问题、责任单位、知识库
依据拆解为现场可执行检查点,输出包含检查对象、检查资料、核验方法、风险提示和责任跟进建议。'
,
topicName
:
'
mock_audit_rectification_query
'
,
'你是审计检查清单生成智能体。你需要把审计
主题、风险点知识库依据、法规
依据拆解为现场可执行检查点,输出包含检查对象、检查资料、核验方法、风险提示和责任跟进建议。'
,
topicName
:
'
audit_checklist_generator
'
,
},
{
key
:
'invoice-assistant'
,
...
...
@@ -112,19 +127,79 @@ const AGENT_DEFINITIONS = [
const
DEFAULT_AGENT
=
AGENT_DEFINITIONS
[
0
];
async
function
expandAuditKeywordsWithLlm
(
query
,
records
)
{
if
(
!
query
||
!
records
.
length
)
return
[];
const
categories
=
Array
.
from
(
new
Set
(
records
.
flatMap
((
record
)
=>
[
record
.
businessSegment
,
record
.
level1Project
,
record
.
level2Project
,
]),
),
)
.
filter
(
Boolean
)
.
slice
(
0
,
80
)
.
join
(
'、'
);
try
{
const
response
=
await
llmChat
({
model
:
'qwen-plus'
,
stream
:
false
,
enable_thinking
:
false
,
messages
:
[
{
role
:
'system'
,
content
:
'你是基金行业审计专家,只负责生成知识库检索关键词,不输出事实、结论或法规解释。'
,
},
{
role
:
'user'
,
content
:
`请针对以下审计主题生成 10-20 个高度相关的专业关键词、同义词、监管常用表述或风险点常见表述。每个关键词 2-8 个字,用逗号分隔,不要编号和解释。\n\n审计主题:
${
query
}
\n\n知识库分类参考:
${
categories
}
`
,
},
],
});
if
(
!
response
.
ok
)
return
[];
const
responseText
=
await
response
.
text
();
let
content
=
responseText
;
try
{
const
data
=
JSON
.
parse
(
responseText
);
content
=
data
?.
choices
?.[
0
]?.
message
?.
content
||
data
?.
choices
?.[
0
]?.
delta
?.
content
||
data
?.
output
?.
text
||
''
;
}
catch
(
error
)
{
content
=
responseText
;
}
return
Array
.
from
(
new
Set
(
String
(
content
)
.
replace
(
/^关键词
\s
*
[
::
]
/
,
''
)
.
split
(
/
[
,,、;;
\n\s]
+/
)
.
map
((
keyword
)
=>
keyword
.
trim
())
.
filter
((
keyword
)
=>
keyword
.
length
>=
2
&&
keyword
.
length
<=
12
),
),
).
slice
(
0
,
20
);
}
catch
(
error
)
{
console
.
warn
(
'审计关键词扩展失败,使用本地规则继续检索'
,
error
);
return
[];
}
}
const
AUDIT_TOPIC
=
{
name
:
'
mock_audit_rectification_query
'
,
title
:
'审计
整改情况查询
'
,
name
:
'
audit_checklist_generator
'
,
title
:
'审计
检查清单生成
'
,
metadata
:
{
prompt
:
'你是内网审计业务工作台助手,专注
审计整改情况查询、问题跟踪、责任单位协同和检查清单生成
。'
,
'你是内网审计业务工作台助手,专注
根据审计风险点知识库生成审计检查清单
。'
,
context
:
'回答时
优先使用已选择的审计对象上下文,说明数据来源于当前内网业务对象;如果上下文不足,指出需要补充的对象或字段
。'
,
'回答时
必须优先使用系统提供的审计风险点知识库 Markdown 上下文。风险点描述、法规依据、风险点来源、风险等级不得编造或改写;如果上下文不足,明确说明知识库覆盖不足
。'
,
enable_search
:
false
,
questions
:
[
'
汇总当前项目的整改进度和高风险事项
'
,
'
分析该整改问题逾期的主要原因
'
,
'
生成责任单位现场复核需要关注的检查点
'
,
'
生成采购管理专项审计检查清单
'
,
'
生成反洗钱专项审计检查清单
'
,
'
查询员工行为管理相关风险点和法规依据
'
,
],
},
};
...
...
@@ -155,7 +230,8 @@ function buildAgentPromptGroup(topics = []) {
return
{
...
agent
,
initialContent
:
agent
.
description
,
initialContent
:
agent
.
topicName
===
AUDIT_TOPIC
.
name
?
undefined
:
agent
.
description
,
topic
,
};
}),
...
...
@@ -835,8 +911,13 @@ export default forwardRef((props, ref) => {
const
[
attachmentsOpen
,
setAttachmentsOpen
]
=
useState
(
false
);
const
[
attachedFiles
,
setAttachedFiles
]
=
useState
([]);
const
[
inputValue
,
setInputValue
]
=
useState
(
''
);
const
[
auditKnowledgeOpen
,
setAuditKnowledgeOpen
]
=
useState
(
false
);
const
[
auditKnowledgeLoading
,
setAuditKnowledgeLoading
]
=
useState
(
false
);
const
[
auditRecords
,
setAuditRecords
]
=
useState
([]);
const
senderRef
=
useRef
(
null
);
const
abortController
=
useRef
(
null
);
const
auditRecordsRef
=
useRef
([]);
const
pendingAuditResultRef
=
useRef
(
null
);
const
auditContext
=
useMemo
(
()
=>
buildAuditContext
(
DEFAULT_AUDIT_SELECTION
),
[],
...
...
@@ -855,6 +936,24 @@ export default forwardRef((props, ref) => {
];
},
[
configuredTopics
]);
const
loadAuditRecords
=
useCallback
(
async
(
force
=
false
)
=>
{
if
(
!
force
&&
auditRecordsRef
.
current
.
length
)
{
return
auditRecordsRef
.
current
;
}
setAuditKnowledgeLoading
(
true
);
try
{
const
records
=
await
fetchAllAuditRiskPoints
(
queryRecords
);
auditRecordsRef
.
current
=
records
;
setAuditRecords
(
records
);
return
records
;
}
catch
(
error
)
{
message
.
error
(
`审计风险点知识库读取失败:
${
error
.
message
||
'未知错误'
}
`
);
throw
error
;
}
finally
{
setAuditKnowledgeLoading
(
false
);
}
},
[]);
useImperativeHandle
(
ref
,
()
=>
{
return
{
submit
:
async
(
message
)
=>
{
...
...
@@ -885,6 +984,10 @@ export default forwardRef((props, ref) => {
if
(
chunk
.
data
.
includes
(
'DONE'
))
{
return
{
...
originMessage
,
auditResult
:
originMessage
?.
auditResult
||
pendingAuditResultRef
.
current
||
undefined
,
tool_calls
:
undefined
,
};
}
...
...
@@ -934,6 +1037,10 @@ export default forwardRef((props, ref) => {
think
:
`
${
originThink
}${
currentThink
}
`
,
content
:
`
${
originContent
}${
currentContent
}
`
,
role
:
'assistant'
,
auditResult
:
originMessage
?.
auditResult
||
pendingAuditResultRef
.
current
||
undefined
,
tool_calls
:
toolCalls
,
tools
:
toolCalls
,
};
...
...
@@ -980,7 +1087,7 @@ export default forwardRef((props, ref) => {
const
topic
=
conversationTopic
;
const
agent
=
conversationAgent
;
const
auditContextText
=
formatAuditContext
(
conversation
.
metadata
.
auditContext
||
[],
conversation
?.
metadata
?
.
auditContext
||
[],
);
const
auditPrompt
=
auditContextText
?
`\n\n当前审计对象上下文:\n
${
auditContextText
}
`
...
...
@@ -997,7 +1104,7 @@ export default forwardRef((props, ref) => {
}${
agentPrompt
}${
auditPrompt
}
`
;
};
const
getMessagePrompt
=
async
(
message
)
=>
{
const
getMessagePrompt
=
async
(
message
,
historyMessages
=
[]
)
=>
{
const
topic
=
conversationTopic
;
const
files
=
message
.
files
||
[];
...
...
@@ -1009,9 +1116,12 @@ export default forwardRef((props, ref) => {
let
searchPrompt
=
''
;
let
filePrompt
=
''
;
let
kbPrompt
=
''
;
const
auditContextText
=
message
.
settings
?.
auditContextText
||
formatAuditContext
(
conversation
.
metadata
.
auditContext
||
[]);
let
auditKnowledgePrompt
=
''
;
const
auditContextText
=
formatAuditContext
(
message
.
settings
?.
auditContext
||
conversation
?.
metadata
?.
auditContext
||
[],
);
const
auditPrompt
=
auditContextText
?
`当前审计对象上下文:\n
${
auditContextText
}
\n\n`
:
''
;
...
...
@@ -1159,7 +1269,32 @@ export default forwardRef((props, ref) => {
.
join
(
'、'
)}
)和用户上传文件回答;如果依据不足,请说明缺口。\n\n`
;
}
return
`
${
auditPrompt
}${
searchPrompt
}${
filePrompt
}${
kbPrompt
}
用户提问:
${
message
.
content
}
`
;
if
(
topic
?.
name
===
AUDIT_TOPIC
.
name
)
{
const
records
=
await
loadAuditRecords
();
const
previousResult
=
findPreviousAuditResult
(
historyMessages
);
const
expansionQuery
=
isAuditRefineRequest
(
message
.
content
,
previousResult
,
)
?
`
${
previousResult
.
project
}
${
message
.
content
}
`
:
message
.
content
;
const
expandedKeywords
=
await
expandAuditKeywordsWithLlm
(
expansionQuery
,
records
,
);
const
result
=
buildAuditResult
(
message
.
content
,
records
,
previousResult
,
{
extraKeywords
:
expandedKeywords
,
},
);
pendingAuditResultRef
.
current
=
result
;
auditKnowledgePrompt
=
buildAuditKnowledgePrompt
(
result
);
}
return
`
${
auditPrompt
}${
auditKnowledgePrompt
}${
searchPrompt
}${
filePrompt
}${
kbPrompt
}
用户提问:
${
message
.
content
}
`
;
};
const
[
agent
]
=
useXAgent
({
...
...
@@ -1171,23 +1306,31 @@ export default forwardRef((props, ref) => {
try
{
const
controller
=
new
AbortController
();
onStream
(
controller
);
pendingAuditResultRef
.
current
=
null
;
onUpdate
({
data
:
'LOADING'
,
});
const
prompt
=
await
getMessagePrompt
(
message
);
const
tools
=
await
getTools
();
const
prompt
=
await
getMessagePrompt
(
message
,
messages
);
const
tools
=
conversationTopic
?
[]
:
await
getTools
();
const
sanitizeLlmMessage
=
({
auditResult
,
files
,
loading
,
settings
,
...
llmMessage
})
=>
llmMessage
;
const
requestMessages
=
[
{
role
:
'system'
,
content
:
await
getSystemPrompt
(),
},
...
messages
.
slice
(
0
,
-
1
),
{
...
messages
.
slice
(
0
,
-
1
)
.
map
(
sanitizeLlmMessage
)
,
sanitizeLlmMessage
(
{
...
message
,
content
:
prompt
,
},
}
)
,
];
const
content
=
JSON
.
stringify
(
requestMessages
);
...
...
@@ -1564,6 +1707,12 @@ export default forwardRef((props, ref) => {
__html
:
marked
.
parse
(
preprocessLaTeX
(
item
.
message
.
content
)),
}
}
></
div
>
{
item
.
message
.
auditResult
&&
(
<
AuditChecklistResult
result=
{
item
.
message
.
auditResult
}
aiContent=
{
item
.
message
.
content
}
/>
)
}
</
Flex
>
);
};
...
...
@@ -1618,6 +1767,12 @@ export default forwardRef((props, ref) => {
senderRef
.
current
?.
focus
();
},
[
conversation
]);
useEffect
(()
=>
{
if
(
!
conversation
&&
userInfo
)
{
loadAuditRecords
().
catch
(()
=>
{});
}
},
[
conversation
,
loadAuditRecords
,
userInfo
]);
useEffect
(()
=>
{
if
(
conversation
)
{
const
oldMessage
=
_
.
last
(
conversation
.
metadata
.
messages
);
...
...
@@ -1649,6 +1804,7 @@ export default forwardRef((props, ref) => {
files
=
null
,
settings
=
null
,
agent
=
null
,
agentLabel
=
null
,
knowledgeBases
=
null
,
})
=>
{
const
selectedAgent
=
serializeAgent
(
agent
||
DEFAULT_AGENT
);
...
...
@@ -1675,12 +1831,14 @@ export default forwardRef((props, ref) => {
if
(
!
conversation
)
{
const
conversationData
=
{
label
:
agent
label
:
agentLabel
||
(
agent
?
selectedAgent
?.
label
:
content
||
selectedTopic
?.
title
||
selectedAgent
?.
label
||
AUDIT_TOPIC
.
title
,
AUDIT_TOPIC
.
title
)
,
agentConfig
:
selectedAgent
||
serializeAgent
(
DEFAULT_AGENT
),
knowledgeBases
:
serializedKnowledgeBases
||
[],
auditContext
:
nextSettings
.
auditContext
,
...
...
@@ -1813,6 +1971,12 @@ export default forwardRef((props, ref) => {
</
div
>
)
:
(
<
div
className=
{
`${styles.chatListInner} ${styles.placeholder}`
}
>
<
AuditKnowledgeBar
count=
{
auditRecords
.
length
}
loading=
{
auditKnowledgeLoading
}
onOpen=
{
()
=>
setAuditKnowledgeOpen
(
true
)
}
onRefresh=
{
()
=>
loadAuditRecords
(
true
)
}
/>
<
div
className=
{
styles
.
promptRow
}
>
{
promptGroups
.
map
((
group
)
=>
(
<
Prompts
...
...
@@ -1941,6 +2105,14 @@ export default forwardRef((props, ref) => {
<
div
className=
{
styles
.
chat
}
>
{
chatList
}
{
chatSender
}
<
AuditKnowledgeImportDialog
open=
{
auditKnowledgeOpen
}
existingRecords=
{
auditRecords
}
createRecordFn=
{
createRecord
}
updateRecordFn=
{
updateRecord
}
onCancel=
{
()
=>
setAuditKnowledgeOpen
(
false
)
}
onImported=
{
()
=>
loadAuditRecords
(
true
)
}
/>
</
div
>
);
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment