feat(web): 更新审批中心、审计、政策制度页面及对应的业务脚本,增强前端交互逻辑

This commit is contained in:
caoxiaozhu
2026-05-15 06:57:07 +00:00
parent 344ac126b3
commit 244b3a58f7
7 changed files with 1142 additions and 325 deletions

View File

@@ -10,7 +10,8 @@ import {
createAgentAssetVersion,
fetchAgentAssetDetail,
fetchAgentAssets,
fetchAgentRuns
fetchAgentRuns,
updateAgentAsset
} from '../../services/agentAssets.js'
import { isManagerUser } from '../../utils/accessControl.js'
@@ -120,6 +121,8 @@ const SCENARIO_LABELS = {
duplicate_expense: '重复报销',
explain: '规则解释',
invoice_anomaly: '票据异常',
travel_policy: '差旅制度',
travel_standard: '差旅标准',
accounts_payable: '应付',
accounts_receivable: '应收',
approval_required: '需审批',
@@ -216,10 +219,126 @@ const STATUS_OPTIONS = [
{ value: 'disabled', label: '已停用' }
]
const EXPENSE_RULE_BLOCK_PATTERN = /```expense-rule\s*([\s\S]*?)\s*```/i
const RULE_TEMPLATE_LABELS = {
travel_standard_v1: '差旅标准模板',
expense_amount_limit_v1: '金额上限模板',
attachment_requirement_v1: '附件要求模板',
general_policy_v1: '通用制度模板'
}
function normalizeText(value) {
return String(value || '').trim()
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
function readConfigJson(value) {
if (isPlainObject(value?.configJson)) {
return value.configJson
}
if (isPlainObject(value?.config_json)) {
return value.config_json
}
return {}
}
function cloneJsonObject(value) {
if (!isPlainObject(value)) {
return null
}
try {
return JSON.parse(JSON.stringify(value))
} catch {
return { ...value }
}
}
function resolveRuleTemplateLabel(value) {
const templateKey = normalizeText(value)
return RULE_TEMPLATE_LABELS[templateKey] || templateKey || '未指定模板'
}
function extractRuntimeRuleFromMarkdown(markdown) {
const match = String(markdown || '').match(EXPENSE_RULE_BLOCK_PATTERN)
if (!match) {
return null
}
try {
const payload = JSON.parse(match[1])
return isPlainObject(payload) ? payload : null
} catch {
return null
}
}
function stripRuntimeRuleBlock(markdown) {
const text = String(markdown || '')
const stripped = text.replace(EXPENSE_RULE_BLOCK_PATTERN, '').replace(/\n{3,}/g, '\n\n').trim()
return stripped
}
function stringifyRuntimeRule(runtimeRule) {
return JSON.stringify(isPlainObject(runtimeRule) ? runtimeRule : {}, null, 2)
}
function parseRuntimeRuleText(runtimeRuleText) {
const text = normalizeText(runtimeRuleText)
if (!text) {
return null
}
try {
const payload = JSON.parse(text)
return isPlainObject(payload) ? payload : null
} catch {
return null
}
}
function buildDefaultRuntimeRule(source) {
const configJson = readConfigJson(source)
const scenarioItems = Array.isArray(source?.scenario_json)
? source.scenario_json
: Array.isArray(source?.scenarioList)
? source.scenarioList
: []
const configRuntimeRule = cloneJsonObject(configJson.runtime_rule)
return {
kind: normalizeText(configRuntimeRule?.kind || configJson.runtime_kind) || 'policy_rule_draft',
version:
typeof configRuntimeRule?.version === 'number' && Number.isFinite(configRuntimeRule.version)
? configRuntimeRule.version
: 1,
template_key:
normalizeText(configRuntimeRule?.template_key || configJson.rule_template_key) || 'general_policy_v1',
rule_name: normalizeText(configRuntimeRule?.rule_name || source?.name) || '未命名规则',
scenario:
normalizeText(configRuntimeRule?.scenario || scenarioItems[0]) || 'expense',
review_required:
typeof configRuntimeRule?.review_required === 'boolean' ? configRuntimeRule.review_required : true
}
}
function resolveRuntimeRuleForVersion(source, rawMarkdown, runtimeRuleFallback = null) {
return (
cloneJsonObject(extractRuntimeRuleFromMarkdown(rawMarkdown)) ||
cloneJsonObject(runtimeRuleFallback) ||
buildDefaultRuntimeRule(source)
)
}
function buildMarkdownVersionContent(markdownContent, runtimeRule) {
const body = stripRuntimeRuleBlock(markdownContent)
const runtimeBlock = ['```expense-rule', stringifyRuntimeRule(runtimeRule), '```'].join('\n')
return body ? `${body}\n\n${runtimeBlock}` : runtimeBlock
}
function makeShort(value) {
const text = normalizeText(value).replace(/\s+/g, '')
if (!text) {
@@ -273,16 +392,27 @@ function formatScenarioList(items) {
.join(' / ')
}
function buildHistory(recentVersions = []) {
return recentVersions.map((item) => ({
version: item.version,
note: item.change_note || '无版本说明',
time: formatDateTime(item.created_at),
content: item.content,
contentType: item.content_type,
createdBy: item.created_by,
isCurrent: Boolean(item.is_current)
}))
function buildHistory(recentVersions = [], source) {
const currentRuntimeRule = cloneJsonObject(readConfigJson(source).runtime_rule)
return recentVersions.map((item) => {
const rawContent = typeof item.content === 'string' ? item.content : ''
return {
version: item.version,
note: item.change_note || '无版本说明',
time: formatDateTime(item.created_at),
content: rawContent,
markdownContent: stripRuntimeRuleBlock(rawContent),
runtimeRule: resolveRuntimeRuleForVersion(
source,
rawContent,
item.is_current ? currentRuntimeRule : null
),
contentType: item.content_type,
createdBy: item.created_by,
isCurrent: Boolean(item.is_current)
}
})
}
function resolveTypeKey(assetType) {
@@ -423,7 +553,15 @@ function buildListItem(asset) {
function buildRuleFields(detail) {
return [
{ label: '规则编码', value: detail.code },
{
label: '模板键',
value: normalizeText(detail.config_json?.rule_template_key) || '未指定'
},
{ label: '业务域', value: resolveDomainLabel(detail.domain) },
{
label: '运行时类型',
value: normalizeText(detail.config_json?.runtime_kind) || 'policy_rule_draft'
},
{ label: '适用场景', value: formatScenarioList(detail.scenario_json) },
{ label: '当前版本', value: detail.current_version || '-' }
]
@@ -555,7 +693,8 @@ function buildOutputRules(detail, typeKey, latestRun, latestCall) {
if (typeKey === 'rules') {
return [
'规则 Markdown 保存后会生成新版本。',
'规则使用固定模板落 Markdown,并配套维护 runtime_rule JSON。',
'保存 Markdown 或 JSON 都会生成新版本快照。',
'未审核通过的规则版本不能正式上线。',
'版本切换当前只影响前端展示内容,不会直接回滚后端版本。'
]
@@ -730,15 +869,26 @@ function buildDetailViewModel(detail, runs) {
const typeKey = resolveTypeKey(detail.asset_type)
const latestRun = typeKey === 'tasks' ? findLatestTaskRun(runs, detail.id) : null
const latestCall = typeKey === 'mcp' ? findLatestMcpCall(runs, detail.code) : null
const configJson = readConfigJson(detail)
const statusMeta = resolveStatusMeta(detail.status)
const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status)
const history = buildHistory(detail.recent_versions || [])
const history = buildHistory(detail.recent_versions || [], detail)
const previewVersion = history.find((item) => item.isCurrent) || history[0] || null
const previewMarkdown =
const previewRawMarkdown =
detail.current_version_content_type === 'markdown'
? String(previewVersion?.content ?? detail.current_version_content ?? '')
: ''
const previewRuntimeRule = resolveRuntimeRuleForVersion(
detail,
previewRawMarkdown,
previewVersion?.runtimeRule || configJson.runtime_rule
)
const previewMarkdown = stripRuntimeRuleBlock(previewRawMarkdown)
const titles = DETAIL_TITLES[typeKey]
const previewChangeNote = previewVersion?.note || detail.current_version_change_note || '无版本说明'
const ruleTemplateKey = normalizeText(configJson.rule_template_key || previewRuntimeRule.template_key)
const ruleTemplateLabel = normalizeText(configJson.rule_template_label) || resolveRuleTemplateLabel(ruleTemplateKey)
const runtimeKind = normalizeText(configJson.runtime_kind || previewRuntimeRule.kind) || 'policy_rule_draft'
return {
id: detail.id,
@@ -761,9 +911,16 @@ function buildDetailViewModel(detail, runs) {
hitRate: buildRowMetric(detail, typeKey),
updatedAt: formatDateTime(detail.updated_at),
badgeTone: BADGE_TONES[typeKey],
configJson,
scenarioList: Array.isArray(detail.scenario_json) ? [...detail.scenario_json] : [],
markdownContent: previewMarkdown,
runtimeRuleText: stringifyRuntimeRule(previewRuntimeRule),
ruleTemplateKey,
ruleTemplateLabel,
runtimeKind,
currentVersionContentType: detail.current_version_content_type,
currentVersionChangeNote: detail.current_version_change_note || '无版本说明',
displayVersionChangeNote: previewChangeNote,
reviewStatusLabel: reviewMeta.label,
reviewStatusTone: reviewMeta.tone,
reviewStatusValue: detail.latest_review?.review_status || '',
@@ -1090,6 +1247,30 @@ export default {
return currentUser.value?.name || currentUser.value?.username || 'system'
}
function buildRuleConfigPayload(asset, runtimeRule) {
const configJson = {
...readConfigJson(asset),
runtime_kind: normalizeText(runtimeRule?.kind) || asset.runtimeKind || 'policy_rule_draft',
runtime_rule: runtimeRule
}
const templateKey = normalizeText(runtimeRule?.template_key) || asset.ruleTemplateKey
if (templateKey) {
configJson.rule_template_key = templateKey
configJson.rule_template_label = resolveRuleTemplateLabel(templateKey)
}
return configJson
}
async function persistRuleRuntimeConfig(asset, runtimeRule) {
await updateAgentAsset(
asset.id,
{
config_json: buildRuleConfigPayload(asset, runtimeRule)
},
{ actor: resolveActor() }
)
}
async function loadRuns(options = {}) {
if (runLoading.value && !options.force) {
return
@@ -1153,6 +1334,8 @@ export default {
function openAssetDetail(asset) {
selectedSkill.value = {
...asset,
configJson: {},
scenarioList: [],
fields: [],
promptSections: [],
outputRules: [],
@@ -1161,7 +1344,12 @@ export default {
tools: [],
history: [],
markdownContent: '',
runtimeRuleText: '',
ruleTemplateKey: '',
ruleTemplateLabel: '',
runtimeKind: 'policy_rule_draft',
displayVersion: asset.version,
displayVersionChangeNote: '无版本说明',
loading: true,
reviewStatusLabel: '加载中',
reviewStatusTone: 'draft'
@@ -1194,9 +1382,17 @@ export default {
}
selectedSkill.value.displayVersion = versionSwitchTarget.value.version
if (typeof versionSwitchTarget.value.content === 'string') {
selectedSkill.value.markdownContent = versionSwitchTarget.value.content
selectedSkill.value.displayVersionChangeNote = versionSwitchTarget.value.note || '无版本说明'
if (typeof versionSwitchTarget.value.markdownContent === 'string') {
selectedSkill.value.markdownContent = versionSwitchTarget.value.markdownContent
}
const runtimeRule = versionSwitchTarget.value.runtimeRule || buildDefaultRuntimeRule(selectedSkill.value)
selectedSkill.value.runtimeRuleText = stringifyRuntimeRule(runtimeRule)
selectedSkill.value.runtimeKind =
normalizeText(runtimeRule.kind) || selectedSkill.value.runtimeKind || 'policy_rule_draft'
selectedSkill.value.ruleTemplateKey =
normalizeText(runtimeRule.template_key) || selectedSkill.value.ruleTemplateKey
selectedSkill.value.ruleTemplateLabel = resolveRuleTemplateLabel(selectedSkill.value.ruleTemplateKey)
versionSwitchTarget.value = null
}
@@ -1210,6 +1406,12 @@ export default {
return
}
const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
if (!runtimeRule) {
toast('运行时 JSON 必须是合法的对象。')
return
}
const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
actionState.value = 'save-markdown'
@@ -1218,13 +1420,14 @@ export default {
selectedSkill.value.id,
{
version: nextVersion,
content: selectedSkill.value.markdownContent,
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
content_type: 'markdown',
change_note: '通过任务规则中心保存 Markdown 规则内容。',
change_note: '通过任务规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
created_by: resolveActor()
},
{ actor: resolveActor() }
)
await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
await refreshCurrentAssets()
await loadSelectedAssetDetail(selectedSkill.value.id)
toast(`规则 Markdown 已保存为 ${nextVersion}`)
@@ -1235,6 +1438,48 @@ export default {
}
}
async function saveRuleRuntimeJson() {
if (!selectedSkill.value || !selectedSkillIsRule.value || !canEditMarkdown.value || detailBusy.value) {
return
}
if (!normalizeText(selectedSkill.value.markdownContent)) {
toast('规则 Markdown 模板不能为空。')
return
}
const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
if (!runtimeRule) {
toast('运行时 JSON 必须是合法的对象。')
return
}
const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
actionState.value = 'save-runtime-json'
try {
await createAgentAssetVersion(
selectedSkill.value.id,
{
version: nextVersion,
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
content_type: 'markdown',
change_note: '通过任务规则中心保存运行时 JSON 配置。',
created_by: resolveActor()
},
{ actor: resolveActor() }
)
await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
await refreshCurrentAssets()
await loadSelectedAssetDetail(selectedSkill.value.id)
toast(`规则 JSON 已保存为 ${nextVersion}`)
} catch (error) {
toast(error?.message || '规则 JSON 保存失败,请稍后重试。')
} finally {
actionState.value = ''
}
}
async function reviewSelectedRule(reviewStatus) {
if (!selectedSkill.value || !selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
return
@@ -1340,6 +1585,7 @@ export default {
cancelVersionSwitch,
confirmVersionSwitch,
saveRuleMarkdown,
saveRuleRuntimeJson,
reviewSelectedRule,
activateSelectedRule,
loadAssets