feat(web): 更新审批中心、审计、政策制度页面及对应的业务脚本,增强前端交互逻辑
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user