2026-05-18 02:51:25 +00:00
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
2026-05-11 06:32:38 +00:00
|
|
|
|
|
2026-05-13 03:35:44 +00:00
|
|
|
|
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
2026-05-18 09:42:23 +00:00
|
|
|
|
import { fetchEmployees } from '../../services/employees.js'
|
2026-05-13 06:52:30 +00:00
|
|
|
|
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
|
2026-05-11 06:32:38 +00:00
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
|
|
|
|
|
import { useToast } from '../../composables/useToast.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
activateAgentAsset,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
compareAgentAssetSpreadsheetVersions,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
createAgentAssetReview,
|
|
|
|
|
|
createAgentAssetVersion,
|
|
|
|
|
|
fetchAgentAssetDetail,
|
|
|
|
|
|
fetchAgentAssets,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
fetchAgentAssetSpreadsheetBlob,
|
2026-05-18 09:42:23 +00:00
|
|
|
|
fetchAgentAssetSpreadsheetChangeRecords,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
fetchAgentAssetSpreadsheetOnlyOfficeConfig,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
fetchAgentAssetRuleJson,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
fetchAgentAssetVersionTimeline,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
fetchAgentRuns,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
saveAgentAssetRuleJson,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
importAgentAssetSpreadsheetContent,
|
|
|
|
|
|
restoreAgentAssetVersion,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
updateAgentAsset
|
2026-05-11 06:32:38 +00:00
|
|
|
|
} from '../../services/agentAssets.js'
|
2026-05-18 02:51:25 +00:00
|
|
|
|
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
|
|
|
|
|
|
import { isFinanceUser, isManagerUser } from '../../utils/accessControl.js'
|
|
|
|
|
|
import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js'
|
|
|
|
|
|
|
|
|
|
|
|
const RULE_TABLE_COLUMNS = {
|
|
|
|
|
|
name: '规则名称',
|
|
|
|
|
|
category: '业务域',
|
|
|
|
|
|
owner: '负责人',
|
|
|
|
|
|
scope: '适用场景',
|
2026-05-19 20:23:58 +08:00
|
|
|
|
version: '修改次数',
|
|
|
|
|
|
metric: '修改人'
|
2026-05-18 02:51:25 +00:00
|
|
|
|
}
|
2026-05-09 15:46:16 +00:00
|
|
|
|
|
|
|
|
|
|
const TYPE_META = {
|
2026-05-11 01:53:30 +00:00
|
|
|
|
rules: {
|
2026-05-11 06:32:38 +00:00
|
|
|
|
assetType: 'rule',
|
|
|
|
|
|
label: '规则',
|
|
|
|
|
|
typeLabel: '规则',
|
2026-05-18 02:51:25 +00:00
|
|
|
|
tableColumns: RULE_TABLE_COLUMNS
|
2026-05-09 15:46:16 +00:00
|
|
|
|
},
|
2026-05-11 01:53:30 +00:00
|
|
|
|
skills: {
|
2026-05-11 06:32:38 +00:00
|
|
|
|
assetType: 'skill',
|
|
|
|
|
|
label: '技能',
|
|
|
|
|
|
typeLabel: '技能',
|
|
|
|
|
|
createButtonLabel: '技能已接入',
|
|
|
|
|
|
hintText: '技能页签已接到真实资产 API,可查看输入、输出、依赖和场景信息。',
|
|
|
|
|
|
searchPlaceholder: '搜索技能名称、编码或负责人',
|
2026-05-11 06:33:46 +00:00
|
|
|
|
showMetricColumn: false,
|
2026-05-11 01:53:30 +00:00
|
|
|
|
tableColumns: {
|
|
|
|
|
|
name: '技能名称',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
category: '业务域',
|
2026-05-11 01:53:30 +00:00
|
|
|
|
owner: '负责人',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
scope: '适用场景',
|
|
|
|
|
|
runtime: '输入摘要',
|
|
|
|
|
|
version: '当前版本',
|
2026-05-11 06:33:46 +00:00
|
|
|
|
metric: ''
|
2026-05-11 01:53:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-05-09 15:46:16 +00:00
|
|
|
|
mcp: {
|
2026-05-11 06:32:38 +00:00
|
|
|
|
assetType: 'mcp',
|
|
|
|
|
|
label: 'MCP',
|
|
|
|
|
|
typeLabel: 'MCP',
|
|
|
|
|
|
createButtonLabel: 'MCP 已接入',
|
|
|
|
|
|
hintText: 'MCP 页签已接到真实资产 API,可查看服务地址、鉴权方式、超时和降级策略。',
|
|
|
|
|
|
searchPlaceholder: '搜索 MCP 名称、编码或负责人',
|
2026-05-09 15:46:16 +00:00
|
|
|
|
tableColumns: {
|
|
|
|
|
|
name: 'MCP 服务',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
category: '业务域',
|
2026-05-09 15:46:16 +00:00
|
|
|
|
owner: '维护人',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
scope: '适用场景',
|
|
|
|
|
|
runtime: '调用地址',
|
|
|
|
|
|
version: '当前版本',
|
|
|
|
|
|
metric: '超时配置'
|
2026-05-09 15:46:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-05-11 06:32:38 +00:00
|
|
|
|
tasks: {
|
|
|
|
|
|
assetType: 'task',
|
|
|
|
|
|
label: '任务',
|
|
|
|
|
|
typeLabel: '任务',
|
|
|
|
|
|
createButtonLabel: '任务已接入',
|
|
|
|
|
|
hintText: '任务页签已接到真实资产 API,可查看调度周期、执行 Agent 和最近执行结果。',
|
|
|
|
|
|
searchPlaceholder: '搜索任务名称、编码或负责人',
|
2026-05-09 15:46:16 +00:00
|
|
|
|
tableColumns: {
|
|
|
|
|
|
name: '任务名称',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
category: '业务域',
|
2026-05-09 15:46:16 +00:00
|
|
|
|
owner: '负责人',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
scope: '适用场景',
|
|
|
|
|
|
runtime: '调度周期',
|
|
|
|
|
|
version: '当前版本',
|
|
|
|
|
|
metric: '执行 Agent'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const TAB_META = {
|
|
|
|
|
|
financialRules: {
|
|
|
|
|
|
assetType: 'rule',
|
|
|
|
|
|
typeKey: 'rules',
|
|
|
|
|
|
label: '财务规则',
|
|
|
|
|
|
typeLabel: '财务规则',
|
|
|
|
|
|
createButtonLabel: '财务规则已接入',
|
|
|
|
|
|
hintText: '仅展示 tag 为“财务规则”的规则资产;未打新 tag 的旧规则已从规则中心隐藏。',
|
|
|
|
|
|
searchPlaceholder: '搜索财务规则名称、编码或负责人',
|
|
|
|
|
|
tableColumns: RULE_TABLE_COLUMNS,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
showRuntimeColumn: false,
|
|
|
|
|
|
showStatusColumn: false,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
badgeTone: 'emerald'
|
|
|
|
|
|
},
|
|
|
|
|
|
riskRules: {
|
|
|
|
|
|
assetType: 'rule',
|
|
|
|
|
|
typeKey: 'rules',
|
|
|
|
|
|
label: '风险规则',
|
|
|
|
|
|
typeLabel: '风险规则',
|
|
|
|
|
|
createButtonLabel: '风险规则已接入',
|
2026-05-19 20:23:58 +08:00
|
|
|
|
hintText: '仅展示平台风险规则;适用场景按差旅、发票、餐饮招待等分类,可用「使用场景」筛选。',
|
2026-05-18 02:51:25 +00:00
|
|
|
|
searchPlaceholder: '搜索风险规则名称、编码或负责人',
|
|
|
|
|
|
tableColumns: RULE_TABLE_COLUMNS,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
showRuntimeColumn: false,
|
|
|
|
|
|
showVersionColumn: false,
|
|
|
|
|
|
showStatusColumn: false,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
badgeTone: 'rose'
|
|
|
|
|
|
},
|
|
|
|
|
|
skills: {
|
|
|
|
|
|
...TYPE_META.skills,
|
|
|
|
|
|
typeKey: 'skills',
|
|
|
|
|
|
badgeTone: 'blue'
|
|
|
|
|
|
},
|
|
|
|
|
|
mcp: {
|
|
|
|
|
|
...TYPE_META.mcp,
|
|
|
|
|
|
typeKey: 'mcp',
|
|
|
|
|
|
badgeTone: 'amber'
|
|
|
|
|
|
},
|
|
|
|
|
|
tasks: {
|
|
|
|
|
|
...TYPE_META.tasks,
|
|
|
|
|
|
typeKey: 'tasks',
|
|
|
|
|
|
badgeTone: 'violet'
|
|
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const STATUS_META = {
|
|
|
|
|
|
draft: { label: '草稿中', tone: 'draft' },
|
|
|
|
|
|
review: { label: '待审核', tone: 'warning' },
|
|
|
|
|
|
active: { label: '已上线', tone: 'success' },
|
|
|
|
|
|
disabled: { label: '已停用', tone: 'disabled' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const REVIEW_META = {
|
|
|
|
|
|
approved: { label: '已通过', tone: 'success' },
|
|
|
|
|
|
pending: { label: '待审核', tone: 'warning' },
|
|
|
|
|
|
rejected: { label: '已驳回', tone: 'danger' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const VERSION_STATE_META = {
|
|
|
|
|
|
published: { label: '已上线', tone: 'success' },
|
|
|
|
|
|
draft: { label: '草稿', tone: 'draft' },
|
|
|
|
|
|
pending_review: { label: '待审核', tone: 'warning' },
|
|
|
|
|
|
approved: { label: '已通过待上线', tone: 'success' },
|
|
|
|
|
|
rejected: { label: '已驳回', tone: 'danger' },
|
|
|
|
|
|
history: { label: '历史版本', tone: 'disabled' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const DOMAIN_LABELS = {
|
|
|
|
|
|
expense: '报销',
|
|
|
|
|
|
ar: '应收',
|
|
|
|
|
|
ap: '应付',
|
|
|
|
|
|
knowledge: '知识',
|
|
|
|
|
|
system: '系统'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const SCENARIO_LABELS = {
|
|
|
|
|
|
expense: '报销',
|
|
|
|
|
|
risk_check: '风险检查',
|
|
|
|
|
|
duplicate_expense: '重复报销',
|
|
|
|
|
|
explain: '规则解释',
|
|
|
|
|
|
invoice_anomaly: '票据异常',
|
2026-05-15 06:57:07 +00:00
|
|
|
|
travel_policy: '差旅制度',
|
|
|
|
|
|
travel_standard: '差旅标准',
|
2026-05-18 10:02:04 +00:00
|
|
|
|
communication_expense: '通信费报销',
|
|
|
|
|
|
expense_standard: '费用标准',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
accounts_payable: '应付',
|
|
|
|
|
|
accounts_receivable: '应收',
|
|
|
|
|
|
approval_required: '需审批',
|
|
|
|
|
|
query: '查询',
|
|
|
|
|
|
summary: '汇总',
|
|
|
|
|
|
system: '系统',
|
|
|
|
|
|
schedule: '调度',
|
|
|
|
|
|
rule_center: '规则中心',
|
|
|
|
|
|
review_digest: '待审摘要',
|
|
|
|
|
|
aging_summary: '账龄汇总',
|
|
|
|
|
|
invoice_validation: '发票验真'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const DETAIL_TITLES = {
|
|
|
|
|
|
rules: {
|
|
|
|
|
|
configTitle: '规则元信息',
|
|
|
|
|
|
configDesc: '展示规则编码、版本、业务域和当前审核 / 上线状态。',
|
|
|
|
|
|
detailTitle: '规则版本说明',
|
|
|
|
|
|
detailDesc: '规则正文由 Markdown 驱动,保存后会生成新的版本快照。',
|
|
|
|
|
|
outputTitle: '审核与上线',
|
|
|
|
|
|
outputDesc: '规则上线受审核状态控制,未审核通过的版本会被后端拦截。',
|
|
|
|
|
|
ruleListTitle: '上线要求',
|
|
|
|
|
|
checkListTitle: '当前状态',
|
|
|
|
|
|
triggerTitle: '适用场景',
|
|
|
|
|
|
triggerDesc: '当前规则注册到的业务场景',
|
|
|
|
|
|
toolTitle: '关联信息',
|
|
|
|
|
|
toolDesc: '规则当前审核、保存和版本快照信息',
|
|
|
|
|
|
historyTitle: '版本历史',
|
|
|
|
|
|
historyDesc: '最近 5 个规则版本',
|
|
|
|
|
|
publishTitle: '上线控制',
|
|
|
|
|
|
publishDesc: '正式上线会调用后端激活接口,审核未通过时会被拦截。'
|
|
|
|
|
|
},
|
|
|
|
|
|
skills: {
|
|
|
|
|
|
configTitle: '技能配置',
|
|
|
|
|
|
configDesc: '展示技能编码、输入摘要、版本和业务域。',
|
|
|
|
|
|
detailTitle: '技能结构',
|
|
|
|
|
|
detailDesc: '按输入、输出和依赖组织技能定义。',
|
|
|
|
|
|
outputTitle: '输出契约',
|
|
|
|
|
|
outputDesc: '技能详情重点展示输入参数、输出参数和依赖能力。',
|
|
|
|
|
|
ruleListTitle: '输出要求',
|
|
|
|
|
|
checkListTitle: '当前快照',
|
|
|
|
|
|
triggerTitle: '适用场景',
|
|
|
|
|
|
triggerDesc: '当前技能注册到的场景标签',
|
|
|
|
|
|
toolTitle: '依赖能力',
|
|
|
|
|
|
toolDesc: '技能当前依赖的数据库或其他能力',
|
|
|
|
|
|
historyTitle: '版本历史',
|
|
|
|
|
|
historyDesc: '最近版本记录',
|
|
|
|
|
|
publishTitle: '发布状态',
|
|
|
|
|
|
publishDesc: '技能当前状态由资产中心统一管理。'
|
|
|
|
|
|
},
|
|
|
|
|
|
mcp: {
|
|
|
|
|
|
configTitle: 'MCP 连接配置',
|
|
|
|
|
|
configDesc: '展示服务地址、超时和调用方式。',
|
|
|
|
|
|
detailTitle: '服务协议',
|
|
|
|
|
|
detailDesc: '按服务类型、鉴权方式和降级策略组织外部服务信息。',
|
|
|
|
|
|
outputTitle: '调用约束',
|
|
|
|
|
|
outputDesc: 'MCP 详情重点展示鉴权方式、返回策略和最近调用状态。',
|
|
|
|
|
|
ruleListTitle: '调用约束',
|
|
|
|
|
|
checkListTitle: '最近状态',
|
|
|
|
|
|
triggerTitle: '适用场景',
|
|
|
|
|
|
triggerDesc: '当前 MCP 覆盖的业务场景',
|
|
|
|
|
|
toolTitle: '运行信息',
|
|
|
|
|
|
toolDesc: '结合 AgentRun 中的 ToolCall 还原最近一次调用状态',
|
|
|
|
|
|
historyTitle: '版本历史',
|
|
|
|
|
|
historyDesc: '最近版本记录',
|
|
|
|
|
|
publishTitle: '服务状态',
|
|
|
|
|
|
publishDesc: 'MCP 资产已接入规则中心,但真实外部调用仍以后续链路集成为准。'
|
|
|
|
|
|
},
|
|
|
|
|
|
tasks: {
|
|
|
|
|
|
configTitle: '任务配置',
|
|
|
|
|
|
configDesc: '展示调度周期、执行 Agent 和任务编码。',
|
|
|
|
|
|
detailTitle: '任务结构',
|
|
|
|
|
|
detailDesc: '按调度计划、目标场景和运行结果组织任务信息。',
|
|
|
|
|
|
outputTitle: '运行要求',
|
|
|
|
|
|
outputDesc: '任务详情重点展示调度 Agent、最近运行结果和运行日志入口。',
|
|
|
|
|
|
ruleListTitle: '运行要求',
|
|
|
|
|
|
checkListTitle: '最近执行',
|
|
|
|
|
|
triggerTitle: '适用场景',
|
|
|
|
|
|
triggerDesc: '当前任务覆盖的业务场景',
|
|
|
|
|
|
toolTitle: '最近调用',
|
|
|
|
|
|
toolDesc: '根据 AgentRun 中的最近执行记录回显任务运行情况',
|
|
|
|
|
|
historyTitle: '版本历史',
|
|
|
|
|
|
historyDesc: '最近版本记录',
|
|
|
|
|
|
publishTitle: '调度状态',
|
|
|
|
|
|
publishDesc: '任务资产已接入规则中心,后续 Day 4 运行时会继续消费这些配置。'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const STATUS_OPTIONS = [
|
|
|
|
|
|
{ value: '', label: '全部状态' },
|
|
|
|
|
|
{ value: 'draft', label: '草稿中' },
|
|
|
|
|
|
{ value: 'review', label: '待审核' },
|
|
|
|
|
|
{ value: 'active', label: '已上线' },
|
|
|
|
|
|
{ value: 'disabled', label: '已停用' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const EXPENSE_RULE_BLOCK_PATTERN = /```expense-rule\s*([\s\S]*?)\s*```/i
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const RULE_SPREADSHEET_BLOCK_PATTERN = /```rule-spreadsheet\s*([\s\S]*?)\s*```/i
|
2026-05-15 06:57:07 +00:00
|
|
|
|
|
|
|
|
|
|
const RULE_TEMPLATE_LABELS = {
|
|
|
|
|
|
travel_standard_v1: '差旅标准模板',
|
|
|
|
|
|
expense_amount_limit_v1: '金额上限模板',
|
|
|
|
|
|
attachment_requirement_v1: '附件要求模板',
|
|
|
|
|
|
general_policy_v1: '通用制度模板'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const RULE_TAB_TAG_ALIASES = {
|
|
|
|
|
|
financialRules: new Set(['财务规则', '财务', 'financialrule', 'financialrules', 'financerule', 'financerules', 'financial', 'finance']),
|
|
|
|
|
|
riskRules: new Set(['风险规则', '风险', '风控', 'riskrule', 'riskrules', 'risk'])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const RISK_SCENARIO_OPTIONS = [
|
|
|
|
|
|
{ value: '', label: '全部场景' },
|
|
|
|
|
|
{ value: '差旅', label: '差旅' },
|
|
|
|
|
|
{ value: '发票', label: '发票' },
|
|
|
|
|
|
{ value: '餐饮招待', label: '餐饮招待' },
|
|
|
|
|
|
{ value: '交通出行', label: '交通出行' },
|
|
|
|
|
|
{ value: '办公物料', label: '办公物料' },
|
|
|
|
|
|
{ value: '费用科目', label: '费用科目' },
|
|
|
|
|
|
{ value: '通用', label: '通用' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const LEGACY_RISK_SCENARIO_KEYS = new Set([
|
|
|
|
|
|
'expense',
|
|
|
|
|
|
'risk_check',
|
|
|
|
|
|
'travel',
|
|
|
|
|
|
'meal',
|
|
|
|
|
|
'invoice',
|
|
|
|
|
|
'travel_policy',
|
|
|
|
|
|
'travel_standard',
|
|
|
|
|
|
'attachment_policy',
|
|
|
|
|
|
'scene_policy',
|
|
|
|
|
|
'invoice_anomaly'
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const SPREADSHEET_DETAIL_MODE = 'spreadsheet'
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const JSON_RISK_DETAIL_MODE = 'json_risk'
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const PREVIEW_RULE_ID = 'preview-rule-expense-company-travel-expense'
|
|
|
|
|
|
const PREVIEW_RULE_CODE = 'rule.expense.company_travel_expense_reimbursement'
|
|
|
|
|
|
const PREVIEW_RULE_VERSION_SPECS = [
|
|
|
|
|
|
{
|
|
|
|
|
|
version: 'v1.2.0',
|
|
|
|
|
|
fileName: '公司差旅费报销规则.xlsx',
|
|
|
|
|
|
updatedAt: '2026-05-17T09:30:00Z',
|
|
|
|
|
|
updatedBy: '王楠',
|
|
|
|
|
|
note: '补充城市分级与住宿限额示例。',
|
|
|
|
|
|
source: 'preview',
|
|
|
|
|
|
isCurrent: true
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
version: 'v1.1.0',
|
|
|
|
|
|
fileName: '公司差旅费报销规则-v1.1.0.xlsx',
|
|
|
|
|
|
updatedAt: '2026-05-14T15:20:00Z',
|
|
|
|
|
|
updatedBy: '顾承宇',
|
|
|
|
|
|
note: '新增票据要求与超标审批列。',
|
|
|
|
|
|
source: 'preview',
|
|
|
|
|
|
isCurrent: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
version: 'v1.0.0',
|
|
|
|
|
|
fileName: '公司差旅费报销规则-v1.0.0.xlsx',
|
|
|
|
|
|
updatedAt: '2026-05-10T11:10:00Z',
|
|
|
|
|
|
updatedBy: '系统初始化',
|
|
|
|
|
|
note: '首版差旅费报销规则表预览。',
|
|
|
|
|
|
source: 'preview',
|
|
|
|
|
|
isCurrent: false
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
function buildPreviewSpreadsheetMeta(spec) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
file_name: spec.fileName,
|
|
|
|
|
|
storage_key: `preview/agent-assets/${PREVIEW_RULE_ID}/${spec.version}/${encodeURIComponent(spec.fileName)}`,
|
|
|
|
|
|
mime_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
|
|
|
|
size_bytes: 82416,
|
|
|
|
|
|
checksum: `preview-${spec.version.replace(/\./g, '-')}`,
|
|
|
|
|
|
updated_at: spec.updatedAt,
|
|
|
|
|
|
updated_by: spec.updatedBy,
|
|
|
|
|
|
source: spec.source
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildPreviewSpreadsheetVersionMarkdown(spec) {
|
|
|
|
|
|
const metadata = buildPreviewSpreadsheetMeta(spec)
|
|
|
|
|
|
return [
|
|
|
|
|
|
'# 公司差旅费报销规则',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 规则载体',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'- 页面状态:前端预览',
|
|
|
|
|
|
`- 当前规则版本:\`${spec.version}\``,
|
|
|
|
|
|
`- 表格文件:\`${spec.fileName}\``,
|
|
|
|
|
|
`- 最近更新人:${spec.updatedBy}`,
|
|
|
|
|
|
`- 最近更新时间:${spec.updatedAt}`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 说明',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'- 当前环境暂无真实规则文件,先用于展示 Excel 规则详情页布局。',
|
|
|
|
|
|
'- 真实上传、编辑与版本回写逻辑会在接好正式数据后启用。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'```rule-spreadsheet',
|
|
|
|
|
|
JSON.stringify(metadata, null, 2),
|
|
|
|
|
|
'```'
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createPreviewRuleDetailPayload() {
|
|
|
|
|
|
const recentVersions = PREVIEW_RULE_VERSION_SPECS.map((spec, index) => ({
|
|
|
|
|
|
id: `${PREVIEW_RULE_ID}-version-${index + 1}`,
|
|
|
|
|
|
asset_id: PREVIEW_RULE_ID,
|
|
|
|
|
|
version: spec.version,
|
|
|
|
|
|
content: buildPreviewSpreadsheetVersionMarkdown(spec),
|
|
|
|
|
|
content_type: 'markdown',
|
|
|
|
|
|
change_note: spec.note,
|
|
|
|
|
|
created_by: spec.updatedBy,
|
|
|
|
|
|
created_at: spec.updatedAt,
|
|
|
|
|
|
is_current: spec.isCurrent
|
|
|
|
|
|
}))
|
|
|
|
|
|
const currentSpec = PREVIEW_RULE_VERSION_SPECS[0]
|
|
|
|
|
|
const currentMeta = buildPreviewSpreadsheetMeta(currentSpec)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: PREVIEW_RULE_ID,
|
|
|
|
|
|
asset_type: 'rule',
|
|
|
|
|
|
code: PREVIEW_RULE_CODE,
|
|
|
|
|
|
name: '公司差旅费报销规则',
|
|
|
|
|
|
description: '前端预览态:先展示 Excel 规则详情页布局、版本卡片和编辑入口位置。',
|
|
|
|
|
|
domain: 'expense',
|
|
|
|
|
|
scenario_json: ['expense', 'travel_policy', 'travel_standard'],
|
|
|
|
|
|
owner: '财务制度管理组',
|
|
|
|
|
|
reviewer: '顾承宇',
|
|
|
|
|
|
status: 'active',
|
|
|
|
|
|
current_version: currentSpec.version,
|
|
|
|
|
|
published_version: currentSpec.version,
|
|
|
|
|
|
working_version: currentSpec.version,
|
|
|
|
|
|
config_json: {
|
|
|
|
|
|
severity: 'medium',
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
tag: '财务规则',
|
|
|
|
|
|
detail_mode: 'spreadsheet',
|
|
|
|
|
|
runtime_kind: 'travel_policy',
|
|
|
|
|
|
rule_template_label: '差旅报销 Excel 模板',
|
|
|
|
|
|
rule_document: {
|
|
|
|
|
|
...currentMeta,
|
|
|
|
|
|
asset_version: currentSpec.version
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created_at: '2026-05-10T11:10:00Z',
|
|
|
|
|
|
updated_at: currentSpec.updatedAt,
|
|
|
|
|
|
current_version_content: recentVersions[0].content,
|
|
|
|
|
|
current_version_content_type: 'markdown',
|
|
|
|
|
|
current_version_change_note: currentSpec.note,
|
|
|
|
|
|
recent_versions: recentVersions,
|
|
|
|
|
|
latest_review: {
|
|
|
|
|
|
id: `${PREVIEW_RULE_ID}-review-1`,
|
|
|
|
|
|
asset_id: PREVIEW_RULE_ID,
|
|
|
|
|
|
version: currentSpec.version,
|
|
|
|
|
|
reviewer: '顾承宇',
|
|
|
|
|
|
review_status: 'approved',
|
|
|
|
|
|
review_note: '当前为页面预览态,先确认布局与交互位置。',
|
|
|
|
|
|
reviewed_at: '2026-05-17T10:00:00Z',
|
|
|
|
|
|
created_at: '2026-05-17T10:00:00Z'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildPreviewRuleListItem() {
|
|
|
|
|
|
const payload = createPreviewRuleDetailPayload()
|
|
|
|
|
|
return {
|
|
|
|
|
|
...buildListItem(payload),
|
|
|
|
|
|
isPreviewMock: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildPreviewRuleDetail() {
|
|
|
|
|
|
const detail = buildDetailViewModel(createPreviewRuleDetailPayload(), [])
|
|
|
|
|
|
return {
|
|
|
|
|
|
...detail,
|
|
|
|
|
|
isPreviewMock: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
function normalizeText(value) {
|
|
|
|
|
|
return String(value || '').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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 {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
function readRuleDocumentMeta(value) {
|
|
|
|
|
|
const configJson = readConfigJson(value)
|
|
|
|
|
|
return isPlainObject(configJson.rule_document) ? configJson.rule_document : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isSpreadsheetRuleSource(value) {
|
|
|
|
|
|
const configJson = readConfigJson(value)
|
|
|
|
|
|
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === SPREADSHEET_DETAIL_MODE
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
function isJsonRiskRuleSource(value) {
|
|
|
|
|
|
const configJson = readConfigJson(value)
|
|
|
|
|
|
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === JSON_RISK_DETAIL_MODE
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
function normalizeRuleTagValue(value) {
|
|
|
|
|
|
return normalizeText(value).toLowerCase().replace(/[\s_-]+/g, '')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collectRuleTagValues(source) {
|
|
|
|
|
|
const configJson = readConfigJson(source)
|
|
|
|
|
|
const rawValues = [
|
|
|
|
|
|
configJson.tag,
|
|
|
|
|
|
configJson.rule_tag,
|
|
|
|
|
|
...(Array.isArray(configJson.tags) ? configJson.tags : []),
|
|
|
|
|
|
...(Array.isArray(configJson.rule_tags) ? configJson.rule_tags : [])
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
return rawValues.map((item) => normalizeText(item)).filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveRuleTabId(source) {
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const code = normalizeText(source?.code || '').toLowerCase()
|
|
|
|
|
|
if (code.startsWith('risk.')) {
|
|
|
|
|
|
return 'riskRules'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isJsonRiskRuleSource(source)) {
|
|
|
|
|
|
return 'riskRules'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const normalizedTags = collectRuleTagValues(source).map((item) => normalizeRuleTagValue(item))
|
|
|
|
|
|
|
|
|
|
|
|
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.riskRules.has(item))) {
|
|
|
|
|
|
return 'riskRules'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.financialRules.has(item))) {
|
|
|
|
|
|
return 'financialRules'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveTabId(source, typeKey) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
return resolveRuleTabId(source)
|
|
|
|
|
|
}
|
|
|
|
|
|
return typeKey
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveTabMeta(tabId, typeKey) {
|
|
|
|
|
|
if (TAB_META[tabId]) {
|
|
|
|
|
|
return TAB_META[tabId]
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...TYPE_META.rules,
|
|
|
|
|
|
typeKey: 'rules',
|
|
|
|
|
|
badgeTone: 'emerald'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return TAB_META[typeKey]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
function resolveRiskRuleDescription(payload) {
|
|
|
|
|
|
if (!isPlainObject(payload)) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalizeText(payload.description)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveRiskRuleSourceRef(payload) {
|
|
|
|
|
|
if (!isPlainObject(payload)) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const metadata = isPlainObject(payload.metadata) ? payload.metadata : {}
|
|
|
|
|
|
return normalizeText(metadata.source_ref)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inferRiskCategoryFromCode(code) {
|
|
|
|
|
|
const normalized = normalizeText(code).toLowerCase()
|
|
|
|
|
|
if (normalized.startsWith('risk.travel.')) {
|
|
|
|
|
|
return '差旅'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.startsWith('risk.invoice.')) {
|
|
|
|
|
|
return '发票'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.includes('entertainment') || normalized.includes('meal_localized')) {
|
|
|
|
|
|
return '餐饮招待'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.includes('consecutive_transport')) {
|
|
|
|
|
|
return '交通出行'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.startsWith('risk.expense.')) {
|
|
|
|
|
|
return '费用科目'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '通用'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveRiskRuleCategory(source) {
|
|
|
|
|
|
const configJson = readConfigJson(source)
|
|
|
|
|
|
const explicit = normalizeText(configJson.risk_category)
|
|
|
|
|
|
if (explicit) {
|
|
|
|
|
|
return explicit
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const payloadCategory = normalizeText(source?.risk_category)
|
|
|
|
|
|
if (payloadCategory) {
|
|
|
|
|
|
return payloadCategory
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scenarioItems = Array.isArray(source?.scenario_json)
|
|
|
|
|
|
? source.scenario_json
|
|
|
|
|
|
: Array.isArray(source?.scenarioList)
|
|
|
|
|
|
? source.scenarioList
|
|
|
|
|
|
: []
|
|
|
|
|
|
const businessScenario = scenarioItems
|
|
|
|
|
|
.map((item) => normalizeText(item))
|
|
|
|
|
|
.find((item) => item && !LEGACY_RISK_SCENARIO_KEYS.has(item))
|
|
|
|
|
|
if (businessScenario) {
|
|
|
|
|
|
return businessScenario
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return inferRiskCategoryFromCode(source?.code)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildRiskListSubtitle(text, maxLength = 42) {
|
|
|
|
|
|
const normalized = normalizeText(text)
|
|
|
|
|
|
if (!normalized) {
|
|
|
|
|
|
return '平台内置风险规则'
|
|
|
|
|
|
}
|
|
|
|
|
|
const firstSentence = normalized.split(/[。;;!?\n]/)[0] || normalized
|
|
|
|
|
|
if (firstSentence.length <= maxLength) {
|
|
|
|
|
|
return firstSentence
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${firstSentence.slice(0, maxLength)}…`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyRiskRuleJsonState(target, payload, apiPayload) {
|
|
|
|
|
|
const rulePayload = isPlainObject(payload) ? payload : {}
|
|
|
|
|
|
const fullDescription =
|
|
|
|
|
|
resolveRiskRuleDescription(rulePayload) ||
|
|
|
|
|
|
normalizeText(apiPayload?.description) ||
|
|
|
|
|
|
normalizeText(target.riskRuleDescription)
|
|
|
|
|
|
const riskCategory =
|
|
|
|
|
|
normalizeText(rulePayload.risk_category) ||
|
|
|
|
|
|
resolveRiskRuleCategory({ ...target, risk_category: rulePayload.risk_category, config_json: rulePayload })
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...target,
|
|
|
|
|
|
riskRuleDescription: fullDescription,
|
|
|
|
|
|
riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48),
|
|
|
|
|
|
riskCategory,
|
|
|
|
|
|
scope: riskCategory,
|
|
|
|
|
|
riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload),
|
|
|
|
|
|
riskRuleSummary: {
|
|
|
|
|
|
name: apiPayload?.name || target.name,
|
|
|
|
|
|
evaluator: apiPayload?.evaluator || rulePayload.evaluator || '',
|
|
|
|
|
|
ontologySignal: apiPayload?.ontology_signal || rulePayload.ontology_signal || '',
|
|
|
|
|
|
inputs: apiPayload?.inputs || rulePayload.inputs || {},
|
|
|
|
|
|
outcomes: apiPayload?.outcomes || rulePayload.outcomes || {}
|
|
|
|
|
|
},
|
|
|
|
|
|
riskRuleJsonText: JSON.stringify(rulePayload, null, 2)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
function extractSpreadsheetMetaFromMarkdown(markdown) {
|
|
|
|
|
|
const match = String(markdown || '').match(RULE_SPREADSHEET_BLOCK_PATTERN)
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = JSON.parse(match[1])
|
|
|
|
|
|
return isPlainObject(payload) ? payload : null
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
function makeShort(value) {
|
|
|
|
|
|
const text = normalizeText(value).replace(/\s+/g, '')
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
|
return 'AG'
|
|
|
|
|
|
}
|
|
|
|
|
|
return text.slice(0, 2).toUpperCase()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDateTime(value) {
|
|
|
|
|
|
if (!value) {
|
|
|
|
|
|
return '未记录'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const date = new Date(value)
|
|
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
|
|
|
|
return String(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new Intl.DateTimeFormat('zh-CN', {
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
hour12: false
|
|
|
|
|
|
})
|
|
|
|
|
|
.format(date)
|
|
|
|
|
|
.replace(/\//g, '-')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveDomainLabel(value) {
|
|
|
|
|
|
return DOMAIN_LABELS[value] || normalizeText(value) || '未分类'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveStatusMeta(value) {
|
|
|
|
|
|
return STATUS_META[value] || { label: normalizeText(value) || '未知状态', tone: 'draft' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveReviewMeta(value) {
|
|
|
|
|
|
return REVIEW_META[value] || { label: '暂无审核', tone: 'draft' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
function resolveTimelineEventMeta(value) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
created: { label: '创建工作稿', icon: 'mdi mdi-file-document-edit-outline', tone: 'draft' },
|
|
|
|
|
|
submitted: { label: '提交审核', icon: 'mdi mdi-send-outline', tone: 'warning' },
|
|
|
|
|
|
approved: { label: '审核通过', icon: 'mdi mdi-check-decagram-outline', tone: 'success' },
|
|
|
|
|
|
rejected: { label: '审核驳回', icon: 'mdi mdi-close-octagon-outline', tone: 'danger' },
|
|
|
|
|
|
published: { label: '正式上线', icon: 'mdi mdi-rocket-launch-outline', tone: 'success' },
|
|
|
|
|
|
restored: { label: '恢复生成工作稿', icon: 'mdi mdi-history', tone: 'info' }
|
|
|
|
|
|
}[normalizeText(value)] || { label: normalizeText(value) || '版本事件', icon: 'mdi mdi-circle-medium', tone: 'draft' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveDiffChangeMeta(value) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
added: { label: '新增', tone: 'success' },
|
|
|
|
|
|
removed: { label: '删除', tone: 'danger' },
|
|
|
|
|
|
modified: { label: '修改', tone: 'warning' }
|
|
|
|
|
|
}[normalizeText(value)] || { label: normalizeText(value) || '变化', tone: 'draft' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
function formatScenarioList(items) {
|
|
|
|
|
|
if (!Array.isArray(items) || !items.length) {
|
|
|
|
|
|
return '未配置场景'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return items
|
|
|
|
|
|
.map((item) => SCENARIO_LABELS[item] || item)
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(' / ')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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
|
|
|
|
|
|
),
|
2026-05-18 02:51:25 +00:00
|
|
|
|
spreadsheetMeta: extractSpreadsheetMetaFromMarkdown(rawContent),
|
2026-05-15 06:57:07 +00:00
|
|
|
|
contentType: item.content_type,
|
|
|
|
|
|
createdBy: item.created_by,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
isCurrent: Boolean(item.is_current),
|
|
|
|
|
|
isPublished: Boolean(item.is_published),
|
|
|
|
|
|
isWorking: Boolean(item.is_working),
|
|
|
|
|
|
lifecycleState: item.lifecycle_state || 'history',
|
|
|
|
|
|
lifecycleMeta: VERSION_STATE_META[item.lifecycle_state] || VERSION_STATE_META.history
|
2026-05-15 06:57:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveTypeKey(assetType) {
|
|
|
|
|
|
if (assetType === 'rule') {
|
|
|
|
|
|
return 'rules'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (assetType === 'skill') {
|
|
|
|
|
|
return 'skills'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (assetType === 'mcp') {
|
|
|
|
|
|
return 'mcp'
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'tasks'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatSeverity(value) {
|
|
|
|
|
|
const severity = normalizeText(value).toLowerCase()
|
|
|
|
|
|
if (severity === 'high') {
|
|
|
|
|
|
return '高风险'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (severity === 'medium') {
|
|
|
|
|
|
return '中风险'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (severity === 'low') {
|
|
|
|
|
|
return '低风险'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '未配置'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatInputSummary(items) {
|
|
|
|
|
|
if (!Array.isArray(items) || !items.length) {
|
|
|
|
|
|
return '无输入'
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${items.length} 项输入`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatOutputSummary(items) {
|
|
|
|
|
|
if (!Array.isArray(items) || !items.length) {
|
|
|
|
|
|
return '无输出'
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${items.length} 项输出`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatTaskRisk(scenarios) {
|
|
|
|
|
|
if (Array.isArray(scenarios) && scenarios.includes('risk_check')) {
|
|
|
|
|
|
return '高风险'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
Array.isArray(scenarios) &&
|
|
|
|
|
|
(scenarios.includes('accounts_receivable') || scenarios.includes('accounts_payable'))
|
|
|
|
|
|
) {
|
|
|
|
|
|
return '中风险'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '常规'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findLatestTaskRun(runs, assetId) {
|
|
|
|
|
|
return runs.find((item) => item.task_id === assetId) || null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findLatestMcpCall(runs, assetCode) {
|
|
|
|
|
|
const expectedToolName = normalizeText(assetCode).replace(/^mcp\./, '')
|
|
|
|
|
|
|
|
|
|
|
|
for (const run of runs) {
|
|
|
|
|
|
for (const toolCall of run.tool_calls || []) {
|
|
|
|
|
|
const toolName = normalizeText(toolCall.tool_name)
|
|
|
|
|
|
if (
|
|
|
|
|
|
toolName === expectedToolName ||
|
|
|
|
|
|
toolName.endsWith(expectedToolName) ||
|
|
|
|
|
|
expectedToolName.endsWith(toolName)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
run,
|
|
|
|
|
|
toolCall
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-09 15:46:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildRowRuntime(asset, typeKey) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
return formatSeverity(asset.config_json?.severity)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return formatInputSummary(asset.config_json?.input_schema)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return normalizeText(asset.config_json?.endpoint) || '未配置地址'
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalizeText(asset.config_json?.cron) || '未配置调度'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildRowMetric(asset, typeKey) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
2026-05-19 20:23:58 +08:00
|
|
|
|
return normalizeText(asset.modified_by) || '未记录'
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return '进入详情查看输出'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalizeText(asset.config_json?.agent) || '未配置 Agent'
|
2026-05-09 15:46:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
function buildListItem(asset) {
|
|
|
|
|
|
const typeKey = resolveTypeKey(asset.asset_type)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const tabId = resolveTabId(asset, typeKey)
|
|
|
|
|
|
if (!tabId) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tabMeta = resolveTabMeta(tabId, typeKey)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const statusMeta = resolveStatusMeta(asset.status)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const workingVersion = asset.working_version || asset.current_version || '-'
|
|
|
|
|
|
const changeCount =
|
|
|
|
|
|
typeof asset.change_count === 'number'
|
|
|
|
|
|
? asset.change_count
|
|
|
|
|
|
: Array.isArray(asset.recent_versions)
|
|
|
|
|
|
? Math.max(asset.recent_versions.length - 1, 0)
|
|
|
|
|
|
: 0
|
|
|
|
|
|
const modifiedBy =
|
|
|
|
|
|
normalizeText(asset.modified_by) ||
|
|
|
|
|
|
normalizeText(
|
|
|
|
|
|
Array.isArray(asset.recent_versions)
|
|
|
|
|
|
? asset.recent_versions.find((item) => item.version === workingVersion)?.created_by
|
|
|
|
|
|
: ''
|
|
|
|
|
|
)
|
|
|
|
|
|
const isRiskRule = tabId === 'riskRules'
|
|
|
|
|
|
const riskCategory = isRiskRule ? resolveRiskRuleCategory(asset) : ''
|
|
|
|
|
|
const listSubtitle = isRiskRule
|
|
|
|
|
|
? buildRiskListSubtitle(asset.description)
|
|
|
|
|
|
: normalizeText(asset.description)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
|
2026-05-09 15:46:16 +00:00
|
|
|
|
return {
|
2026-05-11 06:32:38 +00:00
|
|
|
|
id: asset.id,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
tabId,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
type: typeKey,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
isPreviewMock: Boolean(asset.isPreviewMock),
|
|
|
|
|
|
typeLabel: tabMeta.typeLabel,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
short: makeShort(asset.name),
|
|
|
|
|
|
name: asset.name,
|
|
|
|
|
|
code: asset.code,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
summary: listSubtitle,
|
|
|
|
|
|
listSubtitle,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
category: resolveDomainLabel(asset.domain),
|
|
|
|
|
|
owner: asset.owner,
|
|
|
|
|
|
reviewer: asset.reviewer || '待分配',
|
2026-05-19 20:23:58 +08:00
|
|
|
|
scope: isRiskRule ? riskCategory || '通用' : formatScenarioList(asset.scenario_json),
|
|
|
|
|
|
riskCategory,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
model: buildRowRuntime(asset, typeKey),
|
2026-05-19 20:23:58 +08:00
|
|
|
|
version: workingVersion,
|
|
|
|
|
|
versionDisplay: typeKey === 'rules' ? `${changeCount} 次` : workingVersion,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
publishedVersion: asset.published_version || '-',
|
2026-05-19 20:23:58 +08:00
|
|
|
|
workingVersion,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
status: statusMeta.label,
|
|
|
|
|
|
statusValue: asset.status,
|
|
|
|
|
|
statusTone: statusMeta.tone,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
hitRate: buildRowMetric({ ...asset, modified_by: modifiedBy }, typeKey),
|
|
|
|
|
|
modifiedBy,
|
|
|
|
|
|
changeCount,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
updatedAt: formatDateTime(asset.updated_at),
|
2026-05-18 02:51:25 +00:00
|
|
|
|
badgeTone: tabMeta.badgeTone,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
spotlight: asset.status === 'active',
|
|
|
|
|
|
domainValue: asset.domain
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildRuleFields(detail) {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const ruleDocument = readRuleDocumentMeta(detail)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
return [
|
|
|
|
|
|
{ label: '规则编码', value: detail.code },
|
2026-05-18 02:51:25 +00:00
|
|
|
|
{
|
|
|
|
|
|
label: '明细载体',
|
|
|
|
|
|
value: isSpreadsheetRuleSource(detail) ? 'Excel 表格' : 'Markdown / JSON'
|
|
|
|
|
|
},
|
|
|
|
|
|
...(ruleDocument
|
|
|
|
|
|
? [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '关联文件',
|
|
|
|
|
|
value: normalizeText(ruleDocument.file_name) || '未上传'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
: []),
|
2026-05-15 06:57:07 +00:00
|
|
|
|
{
|
|
|
|
|
|
label: '模板键',
|
|
|
|
|
|
value: normalizeText(detail.config_json?.rule_template_key) || '未指定'
|
|
|
|
|
|
},
|
2026-05-11 06:32:38 +00:00
|
|
|
|
{ label: '业务域', value: resolveDomainLabel(detail.domain) },
|
2026-05-15 06:57:07 +00:00
|
|
|
|
{
|
|
|
|
|
|
label: '运行时类型',
|
|
|
|
|
|
value: normalizeText(detail.config_json?.runtime_kind) || 'policy_rule_draft'
|
|
|
|
|
|
},
|
2026-05-11 06:32:38 +00:00
|
|
|
|
{ label: '适用场景', value: formatScenarioList(detail.scenario_json) },
|
2026-05-18 02:51:25 +00:00
|
|
|
|
{ label: '线上版本', value: detail.published_version || '-' },
|
|
|
|
|
|
{ label: '工作版本', value: detail.working_version || detail.current_version || '-' }
|
2026-05-11 06:32:38 +00:00
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildSkillFields(detail) {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: '技能编码', value: detail.code },
|
|
|
|
|
|
{ label: '业务域', value: resolveDomainLabel(detail.domain) },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '输入参数',
|
|
|
|
|
|
value: Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('、') : '未配置'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '输出参数',
|
|
|
|
|
|
value: Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('、') : '未配置'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMcpFields(detail, latestCall) {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: '服务编码', value: detail.code },
|
|
|
|
|
|
{ label: '调用地址', value: normalizeText(detail.config_json?.endpoint) || '未配置' },
|
|
|
|
|
|
{ label: '鉴权方式', value: normalizeText(content.auth_mode) || '未配置' },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近调用',
|
|
|
|
|
|
value: latestCall ? `${latestCall.toolCall.status} / ${formatDateTime(latestCall.run.started_at)}` : '暂无调用记录'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildTaskFields(detail, latestRun) {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: '任务编码', value: detail.code },
|
|
|
|
|
|
{ label: 'Cron', value: normalizeText(detail.config_json?.cron) || normalizeText(content.schedule) || '未配置' },
|
|
|
|
|
|
{ label: '执行 Agent', value: normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置' },
|
|
|
|
|
|
{ label: '风险等级', value: formatTaskRisk(detail.scenario_json) },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近执行',
|
|
|
|
|
|
value: latestRun ? formatDateTime(latestRun.started_at) : '暂无执行记录'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildFields(detail, typeKey, latestRun, latestCall) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
return buildRuleFields(detail)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return buildSkillFields(detail)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return buildMcpFields(detail, latestCall)
|
|
|
|
|
|
}
|
|
|
|
|
|
return buildTaskFields(detail, latestRun)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildPromptSections(detail, typeKey, latestRun, latestCall) {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '输入参数',
|
|
|
|
|
|
intent: '技能入口',
|
|
|
|
|
|
content: Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('\n') : '未配置输入参数。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '输出参数',
|
|
|
|
|
|
intent: '技能产出',
|
|
|
|
|
|
content: Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('\n') : '未配置输出参数。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '依赖能力',
|
|
|
|
|
|
intent: '外部依赖',
|
|
|
|
|
|
content:
|
|
|
|
|
|
Array.isArray(content.dependencies) && content.dependencies.length
|
|
|
|
|
|
? content.dependencies.join('\n')
|
|
|
|
|
|
: '当前技能未声明外部依赖。'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '服务类型',
|
|
|
|
|
|
intent: '协议说明',
|
|
|
|
|
|
content: normalizeText(content.service_type) || '未配置服务类型。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '鉴权方式',
|
|
|
|
|
|
intent: '安全要求',
|
|
|
|
|
|
content: normalizeText(content.auth_mode) || '未配置鉴权方式。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '降级策略',
|
|
|
|
|
|
intent: '失败处理',
|
|
|
|
|
|
content: normalizeText(content.degrade_strategy) || '未配置降级策略。'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '任务场景',
|
|
|
|
|
|
intent: '调度目标',
|
|
|
|
|
|
content: formatScenarioList(detail.scenario_json)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '执行 Agent',
|
|
|
|
|
|
intent: '运行主体',
|
|
|
|
|
|
content: normalizeText(content.target_agent) || normalizeText(detail.config_json?.agent) || '未配置执行 Agent。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '最近执行结果',
|
|
|
|
|
|
intent: '运行反馈',
|
|
|
|
|
|
content: latestRun?.result_summary || latestRun?.error_message || '暂无执行记录。'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildOutputRules(detail, typeKey, latestRun, latestCall) {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'rules') {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (isSpreadsheetRuleSource(detail)) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'规则详情页以内联 Excel 表格作为主载体,管理员可直接编辑当前版本。',
|
|
|
|
|
|
'上传新的 Excel 文件后,会自动生成新的规则版本快照。',
|
|
|
|
|
|
'切换到历史版本时仅支持预览,不允许直接覆盖历史版本。',
|
|
|
|
|
|
'规则表发生变更后,仍需完成审核才能再次正式上线。'
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
return [
|
2026-05-15 06:57:07 +00:00
|
|
|
|
'规则使用固定模板落 Markdown,并配套维护 runtime_rule JSON。',
|
|
|
|
|
|
'保存 Markdown 或 JSON 都会生成新版本快照。',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
'未审核通过的规则版本不能正式上线。',
|
|
|
|
|
|
'版本切换当前只影响前端展示内容,不会直接回滚后端版本。'
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
`输入参数:${Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('、') : '未配置'}`,
|
|
|
|
|
|
`输出参数:${Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('、') : '未配置'}`,
|
|
|
|
|
|
`依赖能力:${Array.isArray(content.dependencies) && content.dependencies.length ? content.dependencies.join('、') : '未声明'}`
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
`服务地址:${normalizeText(detail.config_json?.endpoint) || '未配置'}`,
|
|
|
|
|
|
`鉴权方式:${normalizeText(content.auth_mode) || '未配置'}`,
|
|
|
|
|
|
`降级策略:${normalizeText(content.degrade_strategy) || '未配置'}`
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
`调度周期:${normalizeText(detail.config_json?.cron) || normalizeText(content.schedule) || '未配置'}`,
|
|
|
|
|
|
`执行 Agent:${normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置'}`,
|
|
|
|
|
|
`风险等级:${formatTaskRisk(detail.scenario_json)}`,
|
|
|
|
|
|
`最近执行结果:${latestRun?.status || '暂无执行记录'}`
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildTests(detail, typeKey, latestRun, latestCall) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status)
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '审核状态',
|
|
|
|
|
|
input: detail.latest_review?.version || detail.current_version || '暂无版本',
|
|
|
|
|
|
result: reviewMeta.label,
|
|
|
|
|
|
tone: reviewMeta.tone
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '上线状态',
|
|
|
|
|
|
input: detail.current_version || '暂无版本',
|
|
|
|
|
|
result: resolveStatusMeta(detail.status).label,
|
|
|
|
|
|
tone: resolveStatusMeta(detail.status).tone
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '输入数量',
|
|
|
|
|
|
input: detail.current_version || '暂无版本',
|
|
|
|
|
|
result: `${content.inputs?.length || 0} 项`,
|
|
|
|
|
|
tone: 'success'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '输出数量',
|
|
|
|
|
|
input: detail.current_version || '暂无版本',
|
|
|
|
|
|
result: `${content.outputs?.length || 0} 项`,
|
|
|
|
|
|
tone: 'success'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '最近调用状态',
|
|
|
|
|
|
input: latestCall?.run?.run_id || '暂无调用',
|
|
|
|
|
|
result: latestCall?.toolCall?.status || '未记录',
|
|
|
|
|
|
tone: latestCall?.toolCall?.status === 'failed' ? 'danger' : 'success'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '最近调用耗时',
|
|
|
|
|
|
input: latestCall?.toolCall?.tool_name || '暂无调用',
|
|
|
|
|
|
result:
|
|
|
|
|
|
typeof latestCall?.toolCall?.duration_ms === 'number'
|
|
|
|
|
|
? `${latestCall.toolCall.duration_ms} ms`
|
|
|
|
|
|
: '未记录',
|
|
|
|
|
|
tone: 'success'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '最近运行状态',
|
|
|
|
|
|
input: latestRun?.run_id || '暂无运行',
|
|
|
|
|
|
result: latestRun?.status || '未记录',
|
|
|
|
|
|
tone: latestRun?.status === 'failed' || latestRun?.status === 'blocked' ? 'danger' : 'success'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '结果摘要',
|
|
|
|
|
|
input: latestRun?.agent || normalizeText(detail.config_json?.agent) || '未配置',
|
|
|
|
|
|
result: latestRun?.result_summary || '暂无摘要',
|
|
|
|
|
|
tone: 'success'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildTools(detail, typeKey, latestRun, latestCall) {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return (content.dependencies || []).map((item) => ({
|
|
|
|
|
|
name: item,
|
|
|
|
|
|
scope: '技能依赖',
|
|
|
|
|
|
mode: '读取',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: normalizeText(content.service_type) || '未配置服务类型',
|
|
|
|
|
|
scope: '服务类型',
|
|
|
|
|
|
mode: 'MCP',
|
|
|
|
|
|
tone: 'active'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: normalizeText(content.auth_mode) || '未配置鉴权方式',
|
|
|
|
|
|
scope: '鉴权',
|
|
|
|
|
|
mode: '安全',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: latestCall?.run?.run_id || '暂无调用记录',
|
|
|
|
|
|
scope: '最近 Run',
|
|
|
|
|
|
mode: latestCall?.toolCall?.status || '未执行',
|
|
|
|
|
|
tone: latestCall?.toolCall?.status === 'failed' ? 'danger' : 'active'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置 Agent',
|
|
|
|
|
|
scope: '执行 Agent',
|
|
|
|
|
|
mode: '调度',
|
|
|
|
|
|
tone: 'active'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: latestRun?.run_id || '暂无执行记录',
|
|
|
|
|
|
scope: '最近 Run',
|
|
|
|
|
|
mode: latestRun?.status || '未执行',
|
|
|
|
|
|
tone: latestRun?.status === 'failed' || latestRun?.status === 'blocked' ? 'danger' : 'active'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: latestRun?.permission_level || '未记录',
|
|
|
|
|
|
scope: '权限级别',
|
|
|
|
|
|
mode: 'Trace',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildPublishDescription(detail, typeKey) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (detail.published_version && detail.working_version && detail.published_version !== detail.working_version) {
|
|
|
|
|
|
return '当前存在尚未上线的工作版本,系统运行仍以线上版本为准。'
|
|
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
if (detail.status === 'active') {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
return '当前规则线上版本已生效,仍可继续保存新的工作版本并重新走审核。'
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
return '当前规则需要先完成审核,再调用上线接口正式激活。'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DETAIL_TITLES[typeKey].publishDesc
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildDetailViewModel(detail, runs) {
|
|
|
|
|
|
const typeKey = resolveTypeKey(detail.asset_type)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const tabId = resolveTabId(detail, typeKey) || typeKey
|
|
|
|
|
|
const tabMeta = resolveTabMeta(tabId, typeKey)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const latestRun = typeKey === 'tasks' ? findLatestTaskRun(runs, detail.id) : null
|
|
|
|
|
|
const latestCall = typeKey === 'mcp' ? findLatestMcpCall(runs, detail.code) : null
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const configJson = readConfigJson(detail)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const statusMeta = resolveStatusMeta(detail.status)
|
|
|
|
|
|
const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status)
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const history = buildHistory(detail.recent_versions || [], detail)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const previewVersion = history.find((item) => item.isWorking) || history[0] || null
|
|
|
|
|
|
const usesSpreadsheetRule = typeKey === 'rules' && isSpreadsheetRuleSource(detail)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const usesJsonRiskRule = typeKey === 'rules' && isJsonRiskRuleSource(detail)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const ruleDocument = readRuleDocumentMeta(detail)
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const previewRawMarkdown =
|
2026-05-11 06:32:38 +00:00
|
|
|
|
detail.current_version_content_type === 'markdown'
|
|
|
|
|
|
? String(previewVersion?.content ?? detail.current_version_content ?? '')
|
|
|
|
|
|
: ''
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const previewRuntimeRule = resolveRuntimeRuleForVersion(
|
|
|
|
|
|
detail,
|
|
|
|
|
|
previewRawMarkdown,
|
|
|
|
|
|
previewVersion?.runtimeRule || configJson.runtime_rule
|
|
|
|
|
|
)
|
|
|
|
|
|
const previewMarkdown = stripRuntimeRuleBlock(previewRawMarkdown)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const titles = DETAIL_TITLES[typeKey]
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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'
|
2026-05-11 06:32:38 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: detail.id,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
tabId,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
type: typeKey,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
typeLabel: tabMeta.typeLabel,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
short: makeShort(detail.name),
|
|
|
|
|
|
name: detail.name,
|
|
|
|
|
|
code: detail.code,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
summary: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description,
|
|
|
|
|
|
listSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : normalizeText(detail.description),
|
2026-05-11 06:32:38 +00:00
|
|
|
|
owner: detail.owner,
|
|
|
|
|
|
reviewer: detail.reviewer || detail.latest_review?.reviewer || '待分配',
|
|
|
|
|
|
category: resolveDomainLabel(detail.domain),
|
2026-05-19 20:23:58 +08:00
|
|
|
|
scope: usesJsonRiskRule ? resolveRiskRuleCategory(detail) || '通用' : formatScenarioList(detail.scenario_json),
|
2026-05-18 02:51:25 +00:00
|
|
|
|
version: detail.working_version || detail.current_version || '-',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
currentVersion: detail.current_version || '-',
|
2026-05-18 02:51:25 +00:00
|
|
|
|
publishedVersion: detail.published_version || '-',
|
|
|
|
|
|
workingVersion: detail.working_version || detail.current_version || '-',
|
|
|
|
|
|
displayVersion: previewVersion?.version || detail.working_version || detail.current_version || '-',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
status: statusMeta.label,
|
|
|
|
|
|
statusValue: detail.status,
|
|
|
|
|
|
statusTone: statusMeta.tone,
|
|
|
|
|
|
hitRate: buildRowMetric(detail, typeKey),
|
|
|
|
|
|
updatedAt: formatDateTime(detail.updated_at),
|
2026-05-18 02:51:25 +00:00
|
|
|
|
badgeTone: tabMeta.badgeTone,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
configJson,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
usesSpreadsheetRule,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
usesJsonRiskRule,
|
|
|
|
|
|
riskRuleJsonText: '{}',
|
|
|
|
|
|
riskRuleSummary: null,
|
|
|
|
|
|
riskRuleDescription: '',
|
|
|
|
|
|
riskRuleSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : '',
|
|
|
|
|
|
riskRuleSourceRef: '',
|
|
|
|
|
|
riskCategory: usesJsonRiskRule ? resolveRiskRuleCategory(detail) : '',
|
2026-05-18 02:51:25 +00:00
|
|
|
|
ruleDocument,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
scenarioList: Array.isArray(detail.scenario_json) ? [...detail.scenario_json] : [],
|
2026-05-11 06:32:38 +00:00
|
|
|
|
markdownContent: previewMarkdown,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
runtimeRuleText: stringifyRuntimeRule(previewRuntimeRule),
|
|
|
|
|
|
ruleTemplateKey,
|
|
|
|
|
|
ruleTemplateLabel,
|
|
|
|
|
|
runtimeKind,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
currentVersionContentType: detail.current_version_content_type,
|
|
|
|
|
|
currentVersionChangeNote: detail.current_version_change_note || '无版本说明',
|
2026-05-15 06:57:07 +00:00
|
|
|
|
displayVersionChangeNote: previewChangeNote,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
reviewStatusLabel: reviewMeta.label,
|
|
|
|
|
|
reviewStatusTone: reviewMeta.tone,
|
|
|
|
|
|
reviewStatusValue: detail.latest_review?.review_status || '',
|
|
|
|
|
|
reviewTimeLabel: formatDateTime(detail.latest_review?.reviewed_at),
|
|
|
|
|
|
reviewNote: detail.latest_review?.review_note || '',
|
|
|
|
|
|
latestRun,
|
|
|
|
|
|
latestCall,
|
|
|
|
|
|
fields: buildFields(detail, typeKey, latestRun, latestCall),
|
|
|
|
|
|
promptSections:
|
|
|
|
|
|
typeKey === 'rules' ? [] : buildPromptSections(detail, typeKey, latestRun, latestCall),
|
|
|
|
|
|
outputRules: buildOutputRules(detail, typeKey, latestRun, latestCall),
|
|
|
|
|
|
tests: buildTests(detail, typeKey, latestRun, latestCall),
|
|
|
|
|
|
triggers: detail.scenario_json?.length ? detail.scenario_json.map((item) => SCENARIO_LABELS[item] || item) : ['未配置场景'],
|
|
|
|
|
|
tools:
|
|
|
|
|
|
typeKey === 'rules'
|
|
|
|
|
|
? [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: detail.latest_review?.reviewer || '待分配审核人',
|
|
|
|
|
|
scope: '审核负责人',
|
|
|
|
|
|
mode: reviewMeta.label,
|
|
|
|
|
|
tone: reviewMeta.tone
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-05-18 02:51:25 +00:00
|
|
|
|
name: detail.published_version || '暂无版本',
|
|
|
|
|
|
scope: '线上版本',
|
|
|
|
|
|
mode: '正式生效',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: detail.working_version || detail.current_version || '暂无版本',
|
|
|
|
|
|
scope: '工作版本',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
mode: detail.current_version_change_note || '无版本说明',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
: buildTools(detail, typeKey, latestRun, latestCall),
|
2026-05-09 15:46:16 +00:00
|
|
|
|
history,
|
|
|
|
|
|
configTitle: titles.configTitle,
|
|
|
|
|
|
configDesc: titles.configDesc,
|
|
|
|
|
|
detailTitle: titles.detailTitle,
|
|
|
|
|
|
detailDesc: titles.detailDesc,
|
|
|
|
|
|
outputTitle: titles.outputTitle,
|
|
|
|
|
|
outputDesc: titles.outputDesc,
|
|
|
|
|
|
ruleListTitle: titles.ruleListTitle,
|
|
|
|
|
|
checkListTitle: titles.checkListTitle,
|
|
|
|
|
|
triggerTitle: titles.triggerTitle,
|
|
|
|
|
|
triggerDesc: titles.triggerDesc,
|
|
|
|
|
|
toolTitle: titles.toolTitle,
|
|
|
|
|
|
toolDesc: titles.toolDesc,
|
|
|
|
|
|
historyTitle: titles.historyTitle,
|
|
|
|
|
|
historyDesc: titles.historyDesc,
|
|
|
|
|
|
publishTitle: titles.publishTitle,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
publishDesc: buildPublishDescription(detail, typeKey),
|
|
|
|
|
|
publishMeta:
|
|
|
|
|
|
typeKey === 'rules'
|
|
|
|
|
|
? `最近保存:${formatDateTime(detail.updated_at)}`
|
|
|
|
|
|
: latestRun
|
|
|
|
|
|
? `最近运行:${formatDateTime(latestRun.started_at)}`
|
|
|
|
|
|
: `最近更新:${formatDateTime(detail.updated_at)}`,
|
|
|
|
|
|
publishState: statusMeta.label,
|
|
|
|
|
|
latestReviewVersion: detail.latest_review?.version || detail.current_version || '-',
|
|
|
|
|
|
loading: false
|
2026-05-09 15:46:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
function incrementVersion(version) {
|
|
|
|
|
|
const normalized = normalizeText(version).replace(/^v/i, '')
|
|
|
|
|
|
const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)$/)
|
|
|
|
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
|
return 'v1.0.0'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const major = Number(match[1])
|
|
|
|
|
|
const minor = Number(match[2])
|
|
|
|
|
|
const patch = Number(match[3]) + 1
|
|
|
|
|
|
return `v${major}.${minor}.${patch}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewNote(status) {
|
|
|
|
|
|
if (status === 'approved') {
|
|
|
|
|
|
return '通过任务规则中心审核。'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (status === 'rejected') {
|
|
|
|
|
|
return '在任务规则中心驳回当前版本。'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '提交任务规则中心待审核。'
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
2026-05-09 15:46:16 +00:00
|
|
|
|
name: 'AuditView',
|
2026-05-13 03:35:44 +00:00
|
|
|
|
components: {
|
2026-05-13 06:52:30 +00:00
|
|
|
|
ConfirmDialog,
|
|
|
|
|
|
TableEmptyState
|
2026-05-13 03:35:44 +00:00
|
|
|
|
},
|
2026-05-09 16:16:56 +00:00
|
|
|
|
emits: ['detail-open-change'],
|
2026-05-11 06:32:38 +00:00
|
|
|
|
setup(_, { emit }) {
|
|
|
|
|
|
const { toast } = useToast()
|
|
|
|
|
|
const { currentUser } = useSystemState()
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const tabs = Object.entries(TAB_META).map(([id, meta]) => ({
|
2026-05-11 06:32:38 +00:00
|
|
|
|
id,
|
|
|
|
|
|
label: meta.label
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const activeType = ref('financialRules')
|
2026-05-06 11:00:38 +08:00
|
|
|
|
const selectedSkill = ref(null)
|
2026-05-09 15:46:16 +00:00
|
|
|
|
const versionSwitchTarget = ref(null)
|
2026-05-11 01:53:30 +00:00
|
|
|
|
const keyword = ref('')
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const activeFilterPopover = ref('')
|
|
|
|
|
|
const selectedDomain = ref('')
|
|
|
|
|
|
const selectedOwner = ref('')
|
|
|
|
|
|
const selectedStatus = ref('')
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const selectedRiskScenario = ref('')
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const errorMessage = ref('')
|
|
|
|
|
|
const detailLoading = ref(false)
|
|
|
|
|
|
const detailError = ref('')
|
|
|
|
|
|
const actionState = ref('')
|
2026-05-18 09:42:23 +00:00
|
|
|
|
const reviewSubmitOpen = ref(false)
|
|
|
|
|
|
const reviewSubmitVersion = ref('')
|
|
|
|
|
|
const reviewSubmitReviewer = ref('')
|
|
|
|
|
|
const reviewSubmitReviewerLoading = ref(false)
|
|
|
|
|
|
const reviewSubmitReviewerOptions = ref([])
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const runLoading = ref(false)
|
|
|
|
|
|
const runs = ref([])
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const spreadsheetUploadInput = ref(null)
|
|
|
|
|
|
const spreadsheetOnlyOfficeLoading = ref(false)
|
|
|
|
|
|
const spreadsheetOnlyOfficeError = ref('')
|
|
|
|
|
|
const spreadsheetOnlyOfficeEditor = ref(null)
|
|
|
|
|
|
const spreadsheetOnlyOfficeReady = ref(false)
|
|
|
|
|
|
const spreadsheetOnlyOfficeHostId = ref('audit-rule-onlyoffice')
|
|
|
|
|
|
const versionTimelineOpen = ref(false)
|
|
|
|
|
|
const versionTimelineLoading = ref(false)
|
|
|
|
|
|
const versionTimelineError = ref('')
|
|
|
|
|
|
const versionTimelineItems = ref([])
|
|
|
|
|
|
const versionCompareOpen = ref(false)
|
|
|
|
|
|
const versionCompareLoading = ref(false)
|
|
|
|
|
|
const versionCompareError = ref('')
|
|
|
|
|
|
const versionComparePayload = ref(null)
|
|
|
|
|
|
const compareBaseVersion = ref('')
|
|
|
|
|
|
const compareTargetVersion = ref('')
|
2026-05-18 09:42:23 +00:00
|
|
|
|
const spreadsheetChangeRecordsByAsset = ref({})
|
|
|
|
|
|
const spreadsheetChangeDetailOpen = ref(false)
|
|
|
|
|
|
const selectedSpreadsheetChangeRecord = ref(null)
|
|
|
|
|
|
let spreadsheetOnlyOfficeMountSeq = 0
|
|
|
|
|
|
let spreadsheetOnlyOfficeLoadTimer = null
|
|
|
|
|
|
let spreadsheetOnlyOfficeHadLocalEdits = false
|
|
|
|
|
|
let spreadsheetOnlyOfficeSyncSeq = 0
|
|
|
|
|
|
let spreadsheetOnlyOfficeVersionPollTimer = null
|
|
|
|
|
|
let spreadsheetOnlyOfficeRefreshTimer = null
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const assetBuckets = ref({
|
2026-05-18 02:51:25 +00:00
|
|
|
|
financialRules: [],
|
|
|
|
|
|
riskRules: [],
|
2026-05-11 06:32:38 +00:00
|
|
|
|
skills: [],
|
|
|
|
|
|
mcp: [],
|
|
|
|
|
|
tasks: []
|
|
|
|
|
|
})
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const isAdmin = computed(() => isManagerUser(currentUser.value))
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const isFinance = computed(() => isFinanceUser(currentUser.value))
|
|
|
|
|
|
const activeMeta = computed(() => TAB_META[activeType.value])
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const activeTabLabel = computed(() => activeMeta.value.label)
|
|
|
|
|
|
const currentAssets = computed(() => assetBuckets.value[activeType.value] || [])
|
|
|
|
|
|
const searchPlaceholder = computed(() => activeMeta.value.searchPlaceholder)
|
2026-05-09 15:46:16 +00:00
|
|
|
|
const createButtonLabel = computed(() => activeMeta.value.createButtonLabel)
|
|
|
|
|
|
const hintText = computed(() => activeMeta.value.hintText)
|
|
|
|
|
|
const tableColumns = computed(() => activeMeta.value.tableColumns)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const showRuntimeColumn = computed(() => activeMeta.value.showRuntimeColumn !== false)
|
2026-05-11 06:33:46 +00:00
|
|
|
|
const showMetricColumn = computed(() => activeMeta.value.showMetricColumn !== false)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
|
|
|
|
|
|
const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const selectedSkillUsesSpreadsheet = computed(
|
|
|
|
|
|
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule)
|
|
|
|
|
|
)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const selectedSkillUsesJsonRisk = computed(
|
|
|
|
|
|
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesJsonRiskRule)
|
|
|
|
|
|
)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const canManageSelected = computed(
|
|
|
|
|
|
() => isAdmin.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
|
|
|
|
|
|
)
|
|
|
|
|
|
const canEditSelected = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
Boolean(selectedSkill.value) &&
|
|
|
|
|
|
!selectedSkill.value?.isPreviewMock &&
|
|
|
|
|
|
(isAdmin.value || isFinance.value)
|
|
|
|
|
|
)
|
|
|
|
|
|
const canEditMarkdown = computed(() => canEditSelected.value && selectedSkillIsRule.value)
|
|
|
|
|
|
const isDisplayingWorkingVersion = computed(
|
|
|
|
|
|
() => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
|
|
|
|
|
|
)
|
|
|
|
|
|
const canSubmitReview = computed(
|
|
|
|
|
|
() => canEditSelected.value && selectedSkillIsRule.value && isDisplayingWorkingVersion.value
|
|
|
|
|
|
)
|
2026-05-18 09:42:23 +00:00
|
|
|
|
const hasReviewSubmitReviewers = computed(() => reviewSubmitReviewerOptions.value.length > 0)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const canReviewSelected = computed(
|
|
|
|
|
|
() => canManageSelected.value && selectedSkillIsRule.value && isDisplayingWorkingVersion.value
|
|
|
|
|
|
)
|
|
|
|
|
|
const canUploadSpreadsheet = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
canEditSelected.value &&
|
|
|
|
|
|
selectedSkillUsesSpreadsheet.value &&
|
|
|
|
|
|
!detailBusy.value &&
|
|
|
|
|
|
selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
|
|
|
|
|
|
)
|
|
|
|
|
|
const canDownloadSpreadsheet = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
selectedSkillUsesSpreadsheet.value &&
|
|
|
|
|
|
Boolean(selectedSkill.value?.id) &&
|
|
|
|
|
|
!detailBusy.value
|
|
|
|
|
|
)
|
|
|
|
|
|
const canEditSpreadsheetInline = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
selectedSkillUsesSpreadsheet.value &&
|
|
|
|
|
|
selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion &&
|
|
|
|
|
|
(selectedSkill.value?.isPreviewMock || canEditSelected.value)
|
|
|
|
|
|
)
|
|
|
|
|
|
const selectedDisplayHistory = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
selectedSkill.value?.history?.find((item) => item.version === selectedSkill.value?.displayVersion) || null
|
|
|
|
|
|
)
|
|
|
|
|
|
const selectedSpreadsheetFileName = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
normalizeText(
|
|
|
|
|
|
selectedDisplayHistory.value?.spreadsheetMeta?.file_name || selectedSkill.value?.ruleDocument?.file_name
|
|
|
|
|
|
) || '未上传规则表'
|
|
|
|
|
|
)
|
|
|
|
|
|
const selectedSpreadsheetVersionModeLabel = computed(() => {
|
|
|
|
|
|
if (selectedSkill.value?.isPreviewMock) {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
return canEditSpreadsheetInline.value ? 'ONLYOFFICE 可编辑' : 'ONLYOFFICE 预览'
|
2026-05-18 02:51:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
return selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
|
2026-05-18 09:42:23 +00:00
|
|
|
|
? '在线可编辑'
|
|
|
|
|
|
: '只读预览'
|
2026-05-18 02:51:25 +00:00
|
|
|
|
})
|
|
|
|
|
|
const selectedVersionTimelineItems = computed(() =>
|
|
|
|
|
|
versionTimelineItems.value.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
meta: resolveTimelineEventMeta(item.event_type),
|
|
|
|
|
|
timeLabel: formatDateTime(item.event_time)
|
|
|
|
|
|
}))
|
|
|
|
|
|
)
|
2026-05-18 09:42:23 +00:00
|
|
|
|
const selectedSpreadsheetChangeRecords = computed(() => {
|
|
|
|
|
|
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
return (spreadsheetChangeRecordsByAsset.value[selectedSkill.value.id] || [])
|
|
|
|
|
|
.filter((item) => item?.changed_at)
|
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
time: formatDateTime(item.changed_at),
|
|
|
|
|
|
previewChanges: Array.isArray(item.cell_changes) ? item.cell_changes.slice(0, 3) : [],
|
|
|
|
|
|
remainingChangeCount: Math.max((item.changed_cell_count || 0) - 3, 0)
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
const selectedSpreadsheetChangeSheetRows = computed(() =>
|
|
|
|
|
|
Array.isArray(selectedSpreadsheetChangeRecord.value?.sheet_changes)
|
|
|
|
|
|
? selectedSpreadsheetChangeRecord.value.sheet_changes.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
meta: resolveDiffChangeMeta(item.change_type)
|
|
|
|
|
|
}))
|
|
|
|
|
|
: []
|
|
|
|
|
|
)
|
|
|
|
|
|
const selectedSpreadsheetChangeCellRows = computed(() =>
|
|
|
|
|
|
Array.isArray(selectedSpreadsheetChangeRecord.value?.cell_changes)
|
|
|
|
|
|
? selectedSpreadsheetChangeRecord.value.cell_changes.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
meta: resolveDiffChangeMeta(item.change_type)
|
|
|
|
|
|
}))
|
|
|
|
|
|
: []
|
2026-05-18 02:51:25 +00:00
|
|
|
|
)
|
|
|
|
|
|
const versionCompareCellRows = computed(() =>
|
|
|
|
|
|
Array.isArray(versionComparePayload.value?.cell_changes)
|
|
|
|
|
|
? versionComparePayload.value.cell_changes.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
meta: resolveDiffChangeMeta(item.change_type)
|
|
|
|
|
|
}))
|
|
|
|
|
|
: []
|
|
|
|
|
|
)
|
|
|
|
|
|
const versionCompareSheetRows = computed(() =>
|
|
|
|
|
|
Array.isArray(versionComparePayload.value?.sheet_changes)
|
|
|
|
|
|
? versionComparePayload.value.sheet_changes.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
meta: resolveDiffChangeMeta(item.change_type)
|
|
|
|
|
|
}))
|
|
|
|
|
|
: []
|
|
|
|
|
|
)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const detailBusy = computed(() => Boolean(actionState.value))
|
|
|
|
|
|
const showReviewNote = computed(
|
|
|
|
|
|
() => selectedSkillIsRule.value && (selectedSkill.value?.reviewNote || selectedSkill.value?.reviewTimeLabel)
|
|
|
|
|
|
)
|
|
|
|
|
|
const domainOptions = computed(() => {
|
|
|
|
|
|
const uniqueValues = [...new Set(currentAssets.value.map((item) => item.domainValue).filter(Boolean))]
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ value: '', label: '全部业务域' },
|
|
|
|
|
|
...uniqueValues.map((value) => ({
|
|
|
|
|
|
value,
|
|
|
|
|
|
label: resolveDomainLabel(value)
|
|
|
|
|
|
}))
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
const ownerOptions = computed(() => {
|
|
|
|
|
|
const uniqueOwners = [...new Set(currentAssets.value.map((item) => item.owner).filter(Boolean))]
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ value: '', label: '全部负责人' },
|
|
|
|
|
|
...uniqueOwners.map((value) => ({
|
|
|
|
|
|
value,
|
|
|
|
|
|
label: value
|
|
|
|
|
|
}))
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
const selectedDomainLabel = computed(
|
|
|
|
|
|
() => domainOptions.value.find((item) => item.value === selectedDomain.value)?.label || '业务域'
|
|
|
|
|
|
)
|
|
|
|
|
|
const selectedOwnerLabel = computed(
|
|
|
|
|
|
() => ownerOptions.value.find((item) => item.value === selectedOwner.value)?.label || '负责人'
|
|
|
|
|
|
)
|
|
|
|
|
|
const selectedStatusLabel = computed(
|
|
|
|
|
|
() => STATUS_OPTIONS.find((item) => item.value === selectedStatus.value)?.label || '状态'
|
|
|
|
|
|
)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const showRiskScenarioFilter = computed(() => activeType.value === 'riskRules')
|
|
|
|
|
|
const showStatusFilter = computed(() => activeType.value !== 'riskRules')
|
|
|
|
|
|
const selectedRiskScenarioLabel = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
|
|
|
|
|
|
'使用场景'
|
|
|
|
|
|
)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const activeFilterTokens = computed(() => {
|
|
|
|
|
|
const tokens = []
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedDomain.value) {
|
|
|
|
|
|
tokens.push(`业务域:${resolveDomainLabel(selectedDomain.value)}`)
|
|
|
|
|
|
}
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if (showRiskScenarioFilter.value && selectedRiskScenario.value) {
|
|
|
|
|
|
tokens.push(`使用场景:${selectedRiskScenario.value}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showStatusFilter.value && selectedStatus.value) {
|
2026-05-11 06:32:38 +00:00
|
|
|
|
tokens.push(`状态:${resolveStatusMeta(selectedStatus.value).label}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selectedOwner.value) {
|
|
|
|
|
|
tokens.push(`负责人:${selectedOwner.value}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (keyword.value.trim()) {
|
|
|
|
|
|
tokens.push(`搜索:${keyword.value.trim()}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return tokens
|
|
|
|
|
|
})
|
2026-05-13 06:52:30 +00:00
|
|
|
|
const auditEmptyState = computed(() => {
|
|
|
|
|
|
const hasFilters = activeFilterTokens.value.length > 0
|
|
|
|
|
|
|
|
|
|
|
|
if (!currentAssets.value.length) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
eyebrow: `${activeTabLabel.value}资产`,
|
|
|
|
|
|
title: `${activeTabLabel.value}列表暂时还是空的`,
|
|
|
|
|
|
desc: `当前环境里还没有可展示的${activeTabLabel.value}资产。完成接入或同步后,会统一展示在这里。`,
|
|
|
|
|
|
icon: 'mdi mdi-database-search-outline',
|
2026-05-18 02:51:25 +00:00
|
|
|
|
actionLabel: '',
|
|
|
|
|
|
actionIcon: '',
|
2026-05-13 06:52:30 +00:00
|
|
|
|
tone: 'amber',
|
|
|
|
|
|
artLabel: 'ASSET',
|
2026-05-19 20:23:58 +08:00
|
|
|
|
tips:
|
|
|
|
|
|
activeType.value === 'riskRules'
|
|
|
|
|
|
? ['切换页签可查看其他资产类型', '支持按业务域、负责人和使用场景做过滤']
|
|
|
|
|
|
: ['切换页签可查看其他资产类型', '支持按业务域、负责人和状态做过滤']
|
2026-05-13 06:52:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
eyebrow: '筛选结果为空',
|
|
|
|
|
|
title: `没有找到匹配的${activeTabLabel.value}`,
|
|
|
|
|
|
desc: hasFilters
|
2026-05-19 20:23:58 +08:00
|
|
|
|
? showRiskScenarioFilter.value
|
|
|
|
|
|
? '试试清空业务域、负责人、使用场景或关键词筛选,再重新查看。'
|
|
|
|
|
|
: '试试清空业务域、负责人、状态或关键词筛选,再重新查看。'
|
2026-05-13 06:52:30 +00:00
|
|
|
|
: `当前列表中还没有满足展示条件的${activeTabLabel.value}资产。`,
|
|
|
|
|
|
icon: hasFilters ? 'mdi mdi-tune-variant' : 'mdi mdi-view-grid-outline',
|
2026-05-18 02:51:25 +00:00
|
|
|
|
actionLabel: hasFilters ? '清空筛选' : '',
|
|
|
|
|
|
actionIcon: hasFilters ? 'mdi mdi-filter-remove-outline' : '',
|
2026-05-13 06:52:30 +00:00
|
|
|
|
tone: hasFilters ? 'emerald' : 'slate',
|
|
|
|
|
|
artLabel: hasFilters ? 'FILTER' : 'QUEUE',
|
|
|
|
|
|
tips: hasFilters
|
2026-05-19 20:23:58 +08:00
|
|
|
|
? showRiskScenarioFilter.value
|
|
|
|
|
|
? ['业务域、负责人、使用场景与关键词会叠加过滤', '可以换个规则名称或场景分类继续搜索']
|
|
|
|
|
|
: ['业务域、负责人、状态与关键词会叠加过滤', '可以换个编码、名称或负责人关键词继续搜索']
|
2026-05-13 06:52:30 +00:00
|
|
|
|
: ['列表展示来自真实资产 API', '切换资产类型后会自动重新拉取数据']
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const canActivateSelected = computed(() => {
|
|
|
|
|
|
if (!selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
return (
|
|
|
|
|
|
isDisplayingWorkingVersion.value &&
|
|
|
|
|
|
selectedSkill.value?.reviewStatusValue === 'approved' &&
|
|
|
|
|
|
selectedSkill.value?.workingVersion !== selectedSkill.value?.publishedVersion
|
|
|
|
|
|
)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
})
|
|
|
|
|
|
const activateBlockedReason = computed(() => {
|
|
|
|
|
|
if (!selectedSkillIsRule.value) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (selectedSkill.value?.isPreviewMock) {
|
|
|
|
|
|
return '当前为页面预览态,暂不执行真实审核和上线。'
|
|
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
if (!canManageSelected.value) {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
return '仅高级管理人员可执行审核和上线。'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!isDisplayingWorkingVersion.value) {
|
|
|
|
|
|
return '请先切回当前工作版本,再执行审核或上线。'
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (selectedSkill.value?.workingVersion === selectedSkill.value?.publishedVersion) {
|
|
|
|
|
|
return '当前工作版本已经是线上版本。'
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (selectedSkill.value?.reviewStatusValue !== 'approved') {
|
|
|
|
|
|
return '当前规则版本未审核通过,不能上线。'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
2026-05-11 01:53:30 +00:00
|
|
|
|
})
|
|
|
|
|
|
const visibleSkills = computed(() => {
|
|
|
|
|
|
const normalizedKeyword = keyword.value.trim().toLowerCase()
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
return currentAssets.value.filter((item) => {
|
|
|
|
|
|
const matchesKeyword = normalizedKeyword
|
|
|
|
|
|
? [item.name, item.code, item.summary, item.owner, item.scope]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.some((value) => String(value).toLowerCase().includes(normalizedKeyword))
|
|
|
|
|
|
: true
|
|
|
|
|
|
const matchesDomain = selectedDomain.value ? item.domainValue === selectedDomain.value : true
|
|
|
|
|
|
const matchesOwner = selectedOwner.value ? item.owner === selectedOwner.value : true
|
2026-05-19 20:23:58 +08:00
|
|
|
|
const matchesStatus = showStatusFilter.value
|
|
|
|
|
|
? selectedStatus.value
|
|
|
|
|
|
? item.statusValue === selectedStatus.value
|
|
|
|
|
|
: true
|
|
|
|
|
|
: true
|
|
|
|
|
|
const matchesRiskScenario = showRiskScenarioFilter.value
|
|
|
|
|
|
? selectedRiskScenario.value
|
|
|
|
|
|
? item.riskCategory === selectedRiskScenario.value
|
|
|
|
|
|
: true
|
|
|
|
|
|
: true
|
2026-05-11 01:53:30 +00:00
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
return matchesKeyword && matchesDomain && matchesOwner && matchesStatus && matchesRiskScenario
|
2026-05-11 06:32:38 +00:00
|
|
|
|
})
|
2026-05-11 01:53:30 +00:00
|
|
|
|
})
|
2026-05-09 15:46:16 +00:00
|
|
|
|
|
2026-05-09 16:16:56 +00:00
|
|
|
|
watch(
|
|
|
|
|
|
selectedSkill,
|
|
|
|
|
|
(value) => {
|
|
|
|
|
|
emit('detail-open-change', Boolean(value))
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
watch(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
selectedSkill.value?.id || '',
|
|
|
|
|
|
selectedSkill.value?.displayVersion || '',
|
|
|
|
|
|
selectedSkill.value?.loading ? '1' : '0',
|
|
|
|
|
|
selectedSkill.value?.usesSpreadsheetRule ? '1' : '0'
|
|
|
|
|
|
],
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
if (!selectedSkillUsesSpreadsheet.value || selectedSkill.value?.loading) {
|
|
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
spreadsheetOnlyOfficeError.value = ''
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await mountSpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
watch(activeType, () => {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
2026-05-18 02:51:25 +00:00
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
2026-05-11 06:32:38 +00:00
|
|
|
|
selectedSkill.value = null
|
|
|
|
|
|
versionSwitchTarget.value = null
|
|
|
|
|
|
resetFilters()
|
|
|
|
|
|
loadAssets({ force: true }).catch((error) => {
|
|
|
|
|
|
errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。'
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function resetFilters() {
|
|
|
|
|
|
keyword.value = ''
|
|
|
|
|
|
selectedDomain.value = ''
|
|
|
|
|
|
selectedOwner.value = ''
|
|
|
|
|
|
selectedStatus.value = ''
|
2026-05-19 20:23:58 +08:00
|
|
|
|
selectedRiskScenario.value = ''
|
2026-05-11 06:32:38 +00:00
|
|
|
|
activeFilterPopover.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 06:52:30 +00:00
|
|
|
|
function handleAuditEmptyAction() {
|
|
|
|
|
|
if (!currentAssets.value.length || !activeFilterTokens.value.length) {
|
|
|
|
|
|
loadAssets({ force: true }).catch(() => {})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resetFilters()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
function toggleFilterPopover(name) {
|
|
|
|
|
|
activeFilterPopover.value = activeFilterPopover.value === name ? '' : name
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeFilterPopover() {
|
|
|
|
|
|
activeFilterPopover.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectFilter(name, value) {
|
|
|
|
|
|
if (name === 'domain') {
|
|
|
|
|
|
selectedDomain.value = value
|
|
|
|
|
|
}
|
|
|
|
|
|
if (name === 'owner') {
|
|
|
|
|
|
selectedOwner.value = value
|
|
|
|
|
|
}
|
|
|
|
|
|
if (name === 'status') {
|
|
|
|
|
|
selectedStatus.value = value
|
|
|
|
|
|
}
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if (name === 'riskScenario') {
|
|
|
|
|
|
selectedRiskScenario.value = value
|
|
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
closeFilterPopover()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDocumentClick(event) {
|
|
|
|
|
|
const target = event.target
|
|
|
|
|
|
if (!(target instanceof Element)) {
|
|
|
|
|
|
closeFilterPopover()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!target.closest('.picker-filter')) {
|
|
|
|
|
|
closeFilterPopover()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveActor() {
|
|
|
|
|
|
return currentUser.value?.name || currentUser.value?.username || 'system'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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() }
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
function destroySpreadsheetOnlyOfficeEditor() {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
if (spreadsheetOnlyOfficeLoadTimer) {
|
|
|
|
|
|
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
|
|
|
|
|
spreadsheetOnlyOfficeLoadTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
stopSpreadsheetOnlyOfficeVersionSync()
|
|
|
|
|
|
clearSpreadsheetPendingChangeRecord(selectedSkill.value?.id, selectedSkill.value?.displayVersion)
|
|
|
|
|
|
spreadsheetOnlyOfficeHadLocalEdits = false
|
|
|
|
|
|
spreadsheetOnlyOfficeSyncSeq += 1
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (spreadsheetOnlyOfficeEditor.value?.destroyEditor) {
|
|
|
|
|
|
spreadsheetOnlyOfficeEditor.value.destroyEditor()
|
|
|
|
|
|
}
|
|
|
|
|
|
spreadsheetOnlyOfficeEditor.value = null
|
|
|
|
|
|
spreadsheetOnlyOfficeReady.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:42:23 +00:00
|
|
|
|
function appendSpreadsheetChangeRecord(record) {
|
|
|
|
|
|
const assetId = normalizeText(record?.assetId)
|
|
|
|
|
|
const version = normalizeText(record?.version)
|
|
|
|
|
|
if (!assetId || !version) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nextRecord = {
|
|
|
|
|
|
version,
|
|
|
|
|
|
operationLabel: normalizeText(record?.operationLabel) || '表格修改',
|
|
|
|
|
|
operationActor: normalizeText(record?.operationActor) || resolveActor(),
|
|
|
|
|
|
note: normalizeText(record?.note) || '用户修改了表格内容。',
|
|
|
|
|
|
time: record?.time || new Date().toISOString(),
|
|
|
|
|
|
isWorking: record?.isWorking !== false,
|
|
|
|
|
|
isPendingLocalEdit: Boolean(record?.isPendingLocalEdit),
|
|
|
|
|
|
disabledReason: normalizeText(record?.disabledReason)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const current = spreadsheetChangeRecordsByAsset.value[assetId] || []
|
|
|
|
|
|
const deduped = current.filter(
|
|
|
|
|
|
(item) =>
|
|
|
|
|
|
!(
|
|
|
|
|
|
item.version === nextRecord.version &&
|
|
|
|
|
|
item.operationLabel === nextRecord.operationLabel &&
|
|
|
|
|
|
item.note === nextRecord.note
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
spreadsheetChangeRecordsByAsset.value = {
|
|
|
|
|
|
...spreadsheetChangeRecordsByAsset.value,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
[assetId]: [nextRecord, ...deduped].slice(0, 30)
|
2026-05-18 09:42:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearSpreadsheetPendingChangeRecord(assetId, version) {
|
|
|
|
|
|
const normalizedAssetId = normalizeText(assetId)
|
|
|
|
|
|
const normalizedVersion = normalizeText(version)
|
|
|
|
|
|
if (!normalizedAssetId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const current = spreadsheetChangeRecordsByAsset.value[normalizedAssetId] || []
|
|
|
|
|
|
spreadsheetChangeRecordsByAsset.value = {
|
|
|
|
|
|
...spreadsheetChangeRecordsByAsset.value,
|
|
|
|
|
|
[normalizedAssetId]: current.filter(
|
|
|
|
|
|
(item) => !(item.isPendingLocalEdit && (!normalizedVersion || item.version === normalizedVersion))
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function markSpreadsheetPendingChange(assetId, version) {
|
|
|
|
|
|
const normalizedAssetId = normalizeText(assetId)
|
|
|
|
|
|
const normalizedVersion = normalizeText(version)
|
|
|
|
|
|
if (!normalizedAssetId || !normalizedVersion) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearSpreadsheetPendingChangeRecord(normalizedAssetId, normalizedVersion)
|
|
|
|
|
|
appendSpreadsheetChangeRecord({
|
|
|
|
|
|
assetId: normalizedAssetId,
|
|
|
|
|
|
version: normalizedVersion,
|
|
|
|
|
|
operationLabel: '编辑中',
|
|
|
|
|
|
operationActor: resolveActor(),
|
|
|
|
|
|
note: '检测到未保存的表格改动,保存后会生成新版本并可查看差异。',
|
|
|
|
|
|
time: new Date().toISOString(),
|
|
|
|
|
|
isWorking: true,
|
|
|
|
|
|
isPendingLocalEdit: true,
|
|
|
|
|
|
disabledReason: '当前是本地未保存修改,保存后才会生成可对比的版本。'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopSpreadsheetOnlyOfficeVersionSync() {
|
|
|
|
|
|
if (spreadsheetOnlyOfficeVersionPollTimer) {
|
|
|
|
|
|
window.clearTimeout(spreadsheetOnlyOfficeVersionPollTimer)
|
|
|
|
|
|
spreadsheetOnlyOfficeVersionPollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopSpreadsheetOnlyOfficeDeferredRefresh() {
|
|
|
|
|
|
if (spreadsheetOnlyOfficeRefreshTimer) {
|
|
|
|
|
|
window.clearTimeout(spreadsheetOnlyOfficeRefreshTimer)
|
|
|
|
|
|
spreadsheetOnlyOfficeRefreshTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getLatestSpreadsheetChangeKey(assetId) {
|
|
|
|
|
|
const records = spreadsheetChangeRecordsByAsset.value[assetId] || []
|
|
|
|
|
|
const latest = records.find((item) => item?.changed_at)
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if (!latest) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const previewSignature = Array.isArray(latest.cell_changes)
|
|
|
|
|
|
? latest.cell_changes
|
|
|
|
|
|
.slice(0, 8)
|
|
|
|
|
|
.map((item) =>
|
|
|
|
|
|
[
|
|
|
|
|
|
item?.sheet_name,
|
|
|
|
|
|
item?.cell,
|
|
|
|
|
|
item?.change_type,
|
|
|
|
|
|
item?.before_value,
|
|
|
|
|
|
item?.after_value
|
|
|
|
|
|
]
|
|
|
|
|
|
.map((value) => normalizeText(value))
|
|
|
|
|
|
.join(':')
|
|
|
|
|
|
)
|
|
|
|
|
|
.join('|')
|
|
|
|
|
|
: ''
|
|
|
|
|
|
return [
|
|
|
|
|
|
latest.id,
|
|
|
|
|
|
latest.changed_at,
|
|
|
|
|
|
latest.actor,
|
|
|
|
|
|
latest.summary,
|
|
|
|
|
|
latest.changed_sheet_count,
|
|
|
|
|
|
latest.changed_cell_count,
|
|
|
|
|
|
previewSignature
|
|
|
|
|
|
]
|
|
|
|
|
|
.map((value) => normalizeText(value))
|
|
|
|
|
|
.join('-')
|
2026-05-18 09:42:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshSpreadsheetChangeRecordsAfterSave(assetId, previousLatestKey = '', attempt = 0) {
|
|
|
|
|
|
const normalizedAssetId = normalizeText(assetId)
|
|
|
|
|
|
if (!normalizedAssetId || selectedSkill.value?.id !== normalizedAssetId) {
|
2026-05-19 20:23:58 +08:00
|
|
|
|
return false
|
2026-05-18 09:42:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await loadSpreadsheetChangeRecords(normalizedAssetId)
|
|
|
|
|
|
const nextLatestKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
|
|
|
|
|
if (nextLatestKey && nextLatestKey !== previousLatestKey) {
|
2026-05-19 20:23:58 +08:00
|
|
|
|
return true
|
2026-05-18 09:42:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (attempt >= 9) {
|
2026-05-19 20:23:58 +08:00
|
|
|
|
return false
|
2026-05-18 09:42:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
await new Promise((resolve) => window.setTimeout(resolve, 800))
|
|
|
|
|
|
return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scheduleSpreadsheetEditorRefreshAfterSave(assetId, savedVersion) {
|
|
|
|
|
|
const normalizedAssetId = normalizeText(assetId)
|
|
|
|
|
|
const normalizedSavedVersion = normalizeText(savedVersion)
|
|
|
|
|
|
if (!normalizedAssetId || !normalizedSavedVersion) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
|
|
|
|
|
spreadsheetOnlyOfficeRefreshTimer = window.setTimeout(async () => {
|
|
|
|
|
|
spreadsheetOnlyOfficeRefreshTimer = null
|
|
|
|
|
|
if (
|
|
|
|
|
|
selectedSkill.value?.id !== normalizedAssetId ||
|
|
|
|
|
|
selectedSkill.value?.displayVersion === normalizedSavedVersion
|
|
|
|
|
|
) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await loadSelectedAssetDetail(normalizedAssetId)
|
|
|
|
|
|
}, 3200)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scheduleSpreadsheetOnlyOfficeVersionSync(assetId, version, attempt = 0) {
|
|
|
|
|
|
const normalizedAssetId = normalizeText(assetId)
|
|
|
|
|
|
const normalizedVersion = normalizeText(version)
|
|
|
|
|
|
if (!normalizedAssetId || !normalizedVersion) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const syncSeq = ++spreadsheetOnlyOfficeSyncSeq
|
|
|
|
|
|
stopSpreadsheetOnlyOfficeVersionSync()
|
|
|
|
|
|
const previousLatestChangeKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
|
|
|
|
|
|
|
|
|
|
|
const runSync = async () => {
|
|
|
|
|
|
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const detail = await fetchAgentAssetDetail(normalizedAssetId)
|
|
|
|
|
|
const nextWorkingVersion = normalizeText(detail?.working_version || detail?.current_version)
|
|
|
|
|
|
if (nextWorkingVersion && nextWorkingVersion !== normalizedVersion) {
|
|
|
|
|
|
clearSpreadsheetPendingChangeRecord(normalizedAssetId, normalizedVersion)
|
|
|
|
|
|
await refreshCurrentAssets()
|
|
|
|
|
|
await refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestChangeKey)
|
|
|
|
|
|
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// ONLYOFFICE 的保存回调刚结束时立即销毁并重挂编辑器,偶发会让新文档会话
|
|
|
|
|
|
// 还没完全就绪就被再次打开,表现为“加载超时”。先刷新右侧修改记录,再留
|
|
|
|
|
|
// 一个很短的缓冲窗口后切换到新工作版本,用户无需退出重进。
|
|
|
|
|
|
scheduleSpreadsheetEditorRefreshAfterSave(normalizedAssetId, nextWorkingVersion)
|
|
|
|
|
|
stopSpreadsheetOnlyOfficeVersionSync()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-19 20:23:58 +08:00
|
|
|
|
|
|
|
|
|
|
const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
|
|
|
|
|
|
normalizedAssetId,
|
|
|
|
|
|
previousLatestChangeKey
|
|
|
|
|
|
)
|
|
|
|
|
|
if (changeRecordRefreshed) {
|
|
|
|
|
|
clearSpreadsheetPendingChangeRecord(normalizedAssetId, normalizedVersion)
|
|
|
|
|
|
await refreshCurrentAssets()
|
|
|
|
|
|
stopSpreadsheetOnlyOfficeVersionSync()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-18 09:42:23 +00:00
|
|
|
|
} catch {
|
|
|
|
|
|
// Ignore transient polling failures and continue retrying within the window.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (attempt >= 29) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
spreadsheetOnlyOfficeVersionPollTimer = window.setTimeout(() => {
|
|
|
|
|
|
scheduleSpreadsheetOnlyOfficeVersionSync(normalizedAssetId, normalizedVersion, attempt + 1)
|
|
|
|
|
|
}, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
spreadsheetOnlyOfficeVersionPollTimer = window.setTimeout(() => {
|
|
|
|
|
|
runSync().catch(() => {})
|
|
|
|
|
|
}, attempt === 0 ? 800 : 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
mountSeq !== spreadsheetOnlyOfficeMountSeq ||
|
|
|
|
|
|
!selectedSkillUsesSpreadsheet.value ||
|
|
|
|
|
|
selectedSkill.value?.id !== assetId ||
|
|
|
|
|
|
selectedSkill.value?.displayVersion !== version ||
|
|
|
|
|
|
selectedSkill.value?.loading
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function mountSpreadsheetOnlyOfficeEditor(retryAttempt = 0) {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id || selectedSkill.value?.loading) {
|
|
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:42:23 +00:00
|
|
|
|
const mountSeq = ++spreadsheetOnlyOfficeMountSeq
|
|
|
|
|
|
const assetId = selectedSkill.value.id
|
|
|
|
|
|
const version = selectedSkill.value.displayVersion
|
|
|
|
|
|
const editable = canEditSpreadsheetInline.value
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
spreadsheetOnlyOfficeLoading.value = true
|
|
|
|
|
|
spreadsheetOnlyOfficeError.value = ''
|
|
|
|
|
|
spreadsheetOnlyOfficeReady.value = false
|
|
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
const payload = await fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId, version)
|
|
|
|
|
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
await loadOnlyOfficeApi(payload.documentServerUrl)
|
2026-05-18 09:42:23 +00:00
|
|
|
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (!window.DocsAPI?.DocEditor) {
|
|
|
|
|
|
throw new Error('ONLYOFFICE 编辑器未正确加载。')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:42:23 +00:00
|
|
|
|
// Host id must be unique for every mount. ONLYOFFICE mutates its host DOM
|
|
|
|
|
|
// during lifecycle teardown; reusing the same element can leave the next
|
|
|
|
|
|
// DocEditor instance with a dead container even though config loading succeeds.
|
|
|
|
|
|
spreadsheetOnlyOfficeHostId.value = `audit-rule-onlyoffice-${assetId}-${version}-${mountSeq}`
|
2026-05-18 02:51:25 +00:00
|
|
|
|
await nextTick()
|
2026-05-18 09:42:23 +00:00
|
|
|
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const config = buildOnlyOfficeEditorConfig(payload.config, {
|
|
|
|
|
|
viewportHeight: window.innerHeight,
|
2026-05-18 09:42:23 +00:00
|
|
|
|
editable,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
fillContainer: true
|
|
|
|
|
|
})
|
|
|
|
|
|
const upstreamEvents = config.events || {}
|
2026-05-18 09:42:23 +00:00
|
|
|
|
spreadsheetOnlyOfficeLoadTimer = window.setTimeout(() => {
|
|
|
|
|
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (retryAttempt < 1) {
|
|
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading.value = true
|
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
|
mountSpreadsheetOnlyOfficeEditor(retryAttempt + 1).catch(() => {})
|
|
|
|
|
|
}, 600)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
spreadsheetOnlyOfficeError.value = 'ONLYOFFICE 加载超时,请重新切换版本后重试。'
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading.value = false
|
|
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
}, 15000)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
config.events = {
|
|
|
|
|
|
...upstreamEvents,
|
|
|
|
|
|
onAppReady(event) {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (spreadsheetOnlyOfficeLoadTimer) {
|
|
|
|
|
|
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
|
|
|
|
|
spreadsheetOnlyOfficeLoadTimer = null
|
|
|
|
|
|
}
|
2026-05-18 02:51:25 +00:00
|
|
|
|
spreadsheetOnlyOfficeReady.value = true
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading.value = false
|
|
|
|
|
|
upstreamEvents.onAppReady?.(event)
|
|
|
|
|
|
},
|
|
|
|
|
|
onError(event) {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (spreadsheetOnlyOfficeLoadTimer) {
|
|
|
|
|
|
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
|
|
|
|
|
spreadsheetOnlyOfficeLoadTimer = null
|
|
|
|
|
|
}
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const errorCode = event?.data?.errorCode
|
|
|
|
|
|
const errorDescription = event?.data?.errorDescription
|
|
|
|
|
|
spreadsheetOnlyOfficeError.value = errorDescription
|
|
|
|
|
|
? `ONLYOFFICE 加载失败:${errorDescription}`
|
|
|
|
|
|
: `ONLYOFFICE 加载失败${errorCode ? `(错误码 ${errorCode})` : '。'}`
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading.value = false
|
|
|
|
|
|
upstreamEvents.onError?.(event)
|
2026-05-18 09:42:23 +00:00
|
|
|
|
},
|
|
|
|
|
|
onDocumentStateChange(event) {
|
|
|
|
|
|
const hasChanges = Boolean(event?.data)
|
|
|
|
|
|
if (hasChanges) {
|
|
|
|
|
|
spreadsheetOnlyOfficeHadLocalEdits = true
|
|
|
|
|
|
markSpreadsheetPendingChange(assetId, version)
|
|
|
|
|
|
if (!spreadsheetOnlyOfficeVersionPollTimer) {
|
|
|
|
|
|
scheduleSpreadsheetOnlyOfficeVersionSync(assetId, version)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (
|
|
|
|
|
|
spreadsheetOnlyOfficeHadLocalEdits &&
|
|
|
|
|
|
editable &&
|
|
|
|
|
|
!isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)
|
|
|
|
|
|
) {
|
|
|
|
|
|
spreadsheetOnlyOfficeHadLocalEdits = false
|
|
|
|
|
|
scheduleSpreadsheetOnlyOfficeVersionSync(assetId, version)
|
|
|
|
|
|
}
|
|
|
|
|
|
upstreamEvents.onDocumentStateChange?.(event)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
spreadsheetOnlyOfficeEditor.value = new window.DocsAPI.DocEditor(
|
|
|
|
|
|
spreadsheetOnlyOfficeHostId.value,
|
|
|
|
|
|
config
|
|
|
|
|
|
)
|
2026-05-18 09:42:23 +00:00
|
|
|
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
|
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
}
|
2026-05-18 02:51:25 +00:00
|
|
|
|
} catch (error) {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-18 02:51:25 +00:00
|
|
|
|
spreadsheetOnlyOfficeError.value = error?.message || '规则表加载失败,请稍后重试。'
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading.value = false
|
|
|
|
|
|
toast(spreadsheetOnlyOfficeError.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function triggerSpreadsheetUpload() {
|
|
|
|
|
|
if (!canUploadSpreadsheet.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
spreadsheetUploadInput.value?.click()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function downloadSpreadsheetFile() {
|
|
|
|
|
|
if (!canDownloadSpreadsheet.value || !selectedSkill.value?.id) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actionState.value = 'download-spreadsheet'
|
|
|
|
|
|
try {
|
|
|
|
|
|
const blob = await fetchAgentAssetSpreadsheetBlob(
|
|
|
|
|
|
selectedSkill.value.id,
|
|
|
|
|
|
selectedSkill.value.displayVersion,
|
|
|
|
|
|
'attachment'
|
|
|
|
|
|
)
|
|
|
|
|
|
const objectUrl = URL.createObjectURL(blob)
|
|
|
|
|
|
const anchor = document.createElement('a')
|
|
|
|
|
|
anchor.href = objectUrl
|
|
|
|
|
|
anchor.download = selectedSpreadsheetFileName.value || '规则表.xlsx'
|
|
|
|
|
|
document.body.appendChild(anchor)
|
|
|
|
|
|
anchor.click()
|
|
|
|
|
|
anchor.remove()
|
|
|
|
|
|
URL.revokeObjectURL(objectUrl)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '规则表下载失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadSpreadsheetFile(file) {
|
|
|
|
|
|
if (!file || !selectedSkill.value?.id || !canUploadSpreadsheet.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actionState.value = 'upload-spreadsheet'
|
|
|
|
|
|
try {
|
|
|
|
|
|
await importAgentAssetSpreadsheetContent(selectedSkill.value.id, file, {
|
|
|
|
|
|
actor: resolveActor()
|
|
|
|
|
|
})
|
|
|
|
|
|
await refreshCurrentAssets()
|
|
|
|
|
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
2026-05-18 09:42:23 +00:00
|
|
|
|
await loadSpreadsheetChangeRecords(selectedSkill.value.id)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
toast(`已导入 ${file.name} 的表格内容,并生成新版本。`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '规则表内容导入失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
|
if (spreadsheetUploadInput.value) {
|
|
|
|
|
|
spreadsheetUploadInput.value.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSpreadsheetFileInput(event) {
|
|
|
|
|
|
await uploadSpreadsheetFile(event?.target?.files?.[0] || null)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
async function loadRuns(options = {}) {
|
|
|
|
|
|
if (runLoading.value && !options.force) {
|
2026-05-09 15:46:16 +00:00
|
|
|
|
return
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
runLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = await fetchAgentRuns({ limit: 50 })
|
|
|
|
|
|
runs.value = Array.isArray(payload) ? payload : []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
runLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAssets(options = {}) {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
errorMessage.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = await fetchAgentAssets({ assetType: activeMeta.value.assetType })
|
2026-05-18 02:51:25 +00:00
|
|
|
|
const items = Array.isArray(payload) ? payload.map(buildListItem).filter(Boolean) : []
|
|
|
|
|
|
|
|
|
|
|
|
if (activeMeta.value.assetType === 'rule') {
|
|
|
|
|
|
const nextBuckets = {
|
|
|
|
|
|
financialRules: [],
|
|
|
|
|
|
riskRules: []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
|
if (item?.tabId === 'financialRules' || item?.tabId === 'riskRules') {
|
|
|
|
|
|
nextBuckets[item.tabId].push(item)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
assetBuckets.value = {
|
|
|
|
|
|
...assetBuckets.value,
|
|
|
|
|
|
...nextBuckets
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
assetBuckets.value = {
|
|
|
|
|
|
...assetBuckets.value,
|
|
|
|
|
|
[activeType.value]: items
|
|
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (activeMeta.value.assetType === 'rule') {
|
|
|
|
|
|
assetBuckets.value = {
|
|
|
|
|
|
...assetBuckets.value,
|
|
|
|
|
|
financialRules:
|
|
|
|
|
|
activeType.value === 'financialRules' ? [] : assetBuckets.value.financialRules,
|
|
|
|
|
|
riskRules: []
|
|
|
|
|
|
}
|
|
|
|
|
|
errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
assetBuckets.value = {
|
|
|
|
|
|
...assetBuckets.value,
|
|
|
|
|
|
[activeType.value]: []
|
|
|
|
|
|
}
|
|
|
|
|
|
errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。'
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (!options.silent) {
|
|
|
|
|
|
toast(errorMessage.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshCurrentAssets() {
|
|
|
|
|
|
await loadAssets({ force: true, silent: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadSelectedAssetDetail(assetId) {
|
|
|
|
|
|
detailLoading.value = true
|
|
|
|
|
|
detailError.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!runs.value.length) {
|
|
|
|
|
|
await loadRuns()
|
|
|
|
|
|
}
|
|
|
|
|
|
const detail = await fetchAgentAssetDetail(assetId)
|
|
|
|
|
|
selectedSkill.value = buildDetailViewModel(detail, runs.value)
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (selectedSkill.value?.type === 'rules') {
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if (!selectedSkill.value.usesJsonRiskRule) {
|
|
|
|
|
|
loadVersionTimeline(assetId, { silent: true }).catch(() => {})
|
|
|
|
|
|
}
|
2026-05-18 09:42:23 +00:00
|
|
|
|
if (selectedSkill.value.usesSpreadsheetRule) {
|
|
|
|
|
|
loadSpreadsheetChangeRecords(assetId).catch(() => {})
|
|
|
|
|
|
}
|
2026-05-19 20:23:58 +08:00
|
|
|
|
if (selectedSkill.value.usesJsonRiskRule) {
|
|
|
|
|
|
await loadRiskRuleJson(assetId)
|
|
|
|
|
|
}
|
2026-05-18 02:51:25 +00:00
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
detailError.value = error?.message || '资产详情加载失败,请稍后重试。'
|
|
|
|
|
|
toast(detailError.value)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
detailLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 20:23:58 +08:00
|
|
|
|
async function loadRiskRuleJson(assetId) {
|
|
|
|
|
|
if (!assetId || !selectedSkill.value?.usesJsonRiskRule) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const payload = await fetchAgentAssetRuleJson(assetId)
|
|
|
|
|
|
const rulePayload = payload?.payload && typeof payload.payload === 'object' ? payload.payload : payload
|
|
|
|
|
|
selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, rulePayload, payload)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveRiskRuleJson() {
|
|
|
|
|
|
if (!selectedSkill.value?.id || !canEditMarkdown.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
actionState.value = 'save-risk-json'
|
|
|
|
|
|
detailBusy.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
|
|
|
|
|
|
const saved = await saveAgentAssetRuleJson(selectedSkill.value.id, { payload: parsed })
|
|
|
|
|
|
const rulePayload = saved?.payload && typeof saved.payload === 'object' ? saved.payload : saved
|
|
|
|
|
|
selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, rulePayload, saved)
|
|
|
|
|
|
toast('风险规则 JSON 已保存。')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '风险规则 JSON 保存失败。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
detailBusy.value = false
|
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatRiskRuleJson() {
|
|
|
|
|
|
if (!selectedSkill.value?.usesJsonRiskRule) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
|
|
|
|
|
|
selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, parsed, {
|
|
|
|
|
|
name: selectedSkill.value.name,
|
|
|
|
|
|
description: resolveRiskRuleDescription(parsed)
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || 'JSON 格式无效,无法格式化。')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function downloadRiskRuleJson() {
|
|
|
|
|
|
if (!selectedSkill.value?.usesJsonRiskRule) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const blob = new Blob([String(selectedSkill.value.riskRuleJsonText || '{}')], {
|
|
|
|
|
|
type: 'application/json;charset=utf-8'
|
|
|
|
|
|
})
|
|
|
|
|
|
const fileName =
|
|
|
|
|
|
selectedSkill.value.ruleDocument?.file_name ||
|
|
|
|
|
|
`${selectedSkill.value.code || 'risk-rule'}.json`
|
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
|
link.href = URL.createObjectURL(blob)
|
|
|
|
|
|
link.download = fileName
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
URL.revokeObjectURL(link.href)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:42:23 +00:00
|
|
|
|
async function loadSpreadsheetChangeRecords(assetId) {
|
|
|
|
|
|
if (!assetId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const payload = await fetchAgentAssetSpreadsheetChangeRecords(assetId, 30)
|
|
|
|
|
|
spreadsheetChangeRecordsByAsset.value = {
|
|
|
|
|
|
...spreadsheetChangeRecordsByAsset.value,
|
|
|
|
|
|
[assetId]: Array.isArray(payload) ? payload : []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openSpreadsheetChangeDetail(item) {
|
|
|
|
|
|
if (!item?.changed_at) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedSpreadsheetChangeRecord.value = item
|
|
|
|
|
|
spreadsheetChangeDetailOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeSpreadsheetChangeDetail() {
|
|
|
|
|
|
spreadsheetChangeDetailOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
function openAssetDetail(asset) {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
2026-05-18 02:51:25 +00:00
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
spreadsheetOnlyOfficeError.value = ''
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading.value = false
|
|
|
|
|
|
if (asset?.isPreviewMock) {
|
|
|
|
|
|
selectedSkill.value = buildPreviewRuleDetail()
|
|
|
|
|
|
detailError.value = ''
|
|
|
|
|
|
detailLoading.value = false
|
|
|
|
|
|
versionSwitchTarget.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-11 06:32:38 +00:00
|
|
|
|
selectedSkill.value = {
|
|
|
|
|
|
...asset,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
configJson: {},
|
2026-05-18 02:51:25 +00:00
|
|
|
|
isPreviewMock: false,
|
|
|
|
|
|
usesSpreadsheetRule: false,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
usesJsonRiskRule: false,
|
|
|
|
|
|
riskRuleJsonText: '{}',
|
|
|
|
|
|
riskRuleSummary: null,
|
|
|
|
|
|
riskRuleDescription: '',
|
|
|
|
|
|
riskRuleSourceRef: '',
|
2026-05-18 02:51:25 +00:00
|
|
|
|
ruleDocument: null,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
scenarioList: [],
|
2026-05-11 06:32:38 +00:00
|
|
|
|
fields: [],
|
|
|
|
|
|
promptSections: [],
|
|
|
|
|
|
outputRules: [],
|
|
|
|
|
|
tests: [],
|
|
|
|
|
|
triggers: [],
|
|
|
|
|
|
tools: [],
|
|
|
|
|
|
history: [],
|
|
|
|
|
|
markdownContent: '',
|
2026-05-15 06:57:07 +00:00
|
|
|
|
runtimeRuleText: '',
|
|
|
|
|
|
ruleTemplateKey: '',
|
|
|
|
|
|
ruleTemplateLabel: '',
|
|
|
|
|
|
runtimeKind: 'policy_rule_draft',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
displayVersion: asset.version,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
displayVersionChangeNote: '无版本说明',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
loading: true,
|
|
|
|
|
|
reviewStatusLabel: '加载中',
|
|
|
|
|
|
reviewStatusTone: 'draft'
|
|
|
|
|
|
}
|
|
|
|
|
|
versionSwitchTarget.value = null
|
|
|
|
|
|
loadSelectedAssetDetail(asset.id).catch(() => {})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeDetail() {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
2026-05-18 02:51:25 +00:00
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
|
|
|
|
|
spreadsheetOnlyOfficeError.value = ''
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading.value = false
|
2026-05-11 06:32:38 +00:00
|
|
|
|
selectedSkill.value = null
|
|
|
|
|
|
detailError.value = ''
|
|
|
|
|
|
detailLoading.value = false
|
|
|
|
|
|
versionSwitchTarget.value = null
|
2026-05-18 02:51:25 +00:00
|
|
|
|
versionTimelineOpen.value = false
|
|
|
|
|
|
versionCompareOpen.value = false
|
|
|
|
|
|
versionTimelineItems.value = []
|
|
|
|
|
|
versionComparePayload.value = null
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openVersionSwitch(version) {
|
|
|
|
|
|
if (!selectedSkill.value || version.version === selectedSkill.value.displayVersion) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-09 15:46:16 +00:00
|
|
|
|
versionSwitchTarget.value = version
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cancelVersionSwitch() {
|
|
|
|
|
|
versionSwitchTarget.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function confirmVersionSwitch() {
|
|
|
|
|
|
if (!selectedSkill.value || !versionSwitchTarget.value) {
|
|
|
|
|
|
return
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
2026-05-09 15:46:16 +00:00
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
selectedSkill.value.displayVersion = versionSwitchTarget.value.version
|
2026-05-15 06:57:07 +00:00
|
|
|
|
selectedSkill.value.displayVersionChangeNote = versionSwitchTarget.value.note || '无版本说明'
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (selectedSkill.value.usesSpreadsheetRule) {
|
|
|
|
|
|
versionSwitchTarget.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-15 06:57:07 +00:00
|
|
|
|
if (typeof versionSwitchTarget.value.markdownContent === 'string') {
|
|
|
|
|
|
selectedSkill.value.markdownContent = versionSwitchTarget.value.markdownContent
|
2026-05-11 06:32:38 +00:00
|
|
|
|
}
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
versionSwitchTarget.value = null
|
|
|
|
|
|
}
|
2026-05-09 15:46:16 +00:00
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
async function saveRuleMarkdown() {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (
|
|
|
|
|
|
!selectedSkill.value ||
|
|
|
|
|
|
!selectedSkillIsRule.value ||
|
|
|
|
|
|
selectedSkill.value.usesSpreadsheetRule ||
|
|
|
|
|
|
!canEditMarkdown.value ||
|
|
|
|
|
|
detailBusy.value
|
|
|
|
|
|
) {
|
2026-05-11 06:32:38 +00:00
|
|
|
|
return
|
2026-05-09 15:46:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
if (!normalizeText(selectedSkill.value.markdownContent)) {
|
|
|
|
|
|
toast('规则 Markdown 内容不能为空。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
|
|
|
|
|
|
if (!runtimeRule) {
|
|
|
|
|
|
toast('运行时 JSON 必须是合法的对象。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
|
|
|
|
|
|
actionState.value = 'save-markdown'
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await createAgentAssetVersion(
|
|
|
|
|
|
selectedSkill.value.id,
|
|
|
|
|
|
{
|
|
|
|
|
|
version: nextVersion,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
|
2026-05-11 06:32:38 +00:00
|
|
|
|
content_type: 'markdown',
|
2026-05-15 06:57:07 +00:00
|
|
|
|
change_note: '通过任务规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
|
2026-05-11 06:32:38 +00:00
|
|
|
|
created_by: resolveActor()
|
|
|
|
|
|
},
|
|
|
|
|
|
{ actor: resolveActor() }
|
|
|
|
|
|
)
|
2026-05-15 06:57:07 +00:00
|
|
|
|
await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
|
2026-05-11 06:32:38 +00:00
|
|
|
|
await refreshCurrentAssets()
|
|
|
|
|
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
|
|
|
|
|
toast(`规则 Markdown 已保存为 ${nextVersion}。`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '规则 Markdown 保存失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
async function saveRuleRuntimeJson() {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (
|
|
|
|
|
|
!selectedSkill.value ||
|
|
|
|
|
|
!selectedSkillIsRule.value ||
|
|
|
|
|
|
selectedSkill.value.usesSpreadsheetRule ||
|
|
|
|
|
|
!canEditMarkdown.value ||
|
|
|
|
|
|
detailBusy.value
|
|
|
|
|
|
) {
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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 = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
async function reviewSelectedRule(reviewStatus) {
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (!selectedSkill.value || !selectedSkillIsRule.value || detailBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (reviewStatus === 'pending' && !canSubmitReview.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (reviewStatus !== 'pending' && !canReviewSelected.value) {
|
2026-05-11 06:32:38 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actionState.value = `review-${reviewStatus}`
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await createAgentAssetReview(
|
|
|
|
|
|
selectedSkill.value.id,
|
|
|
|
|
|
{
|
2026-05-18 02:51:25 +00:00
|
|
|
|
version: selectedSkill.value.workingVersion,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
reviewer: resolveActor(),
|
|
|
|
|
|
review_status: reviewStatus,
|
|
|
|
|
|
review_note: buildReviewNote(reviewStatus)
|
|
|
|
|
|
},
|
|
|
|
|
|
{ actor: resolveActor() }
|
|
|
|
|
|
)
|
|
|
|
|
|
await refreshCurrentAssets()
|
|
|
|
|
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
|
|
|
|
|
toast(`当前规则版本已标记为${resolveReviewMeta(reviewStatus).label}。`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '规则审核提交失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:42:23 +00:00
|
|
|
|
async function loadReviewSubmitReviewers() {
|
|
|
|
|
|
reviewSubmitReviewerLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const employees = await fetchEmployees()
|
|
|
|
|
|
reviewSubmitReviewerOptions.value = (Array.isArray(employees) ? employees : [])
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
(item) =>
|
|
|
|
|
|
item.status === '在职' &&
|
|
|
|
|
|
Array.isArray(item.roleCodes) &&
|
|
|
|
|
|
item.roleCodes.includes('manager')
|
|
|
|
|
|
)
|
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
|
value: item.name,
|
|
|
|
|
|
label: `${item.name} · ${item.position || '高级管理员'}`
|
|
|
|
|
|
}))
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
reviewSubmitReviewerOptions.value = []
|
|
|
|
|
|
toast(error?.message || '审核人列表加载失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
reviewSubmitReviewerLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function openSubmitReviewDialog() {
|
|
|
|
|
|
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
reviewSubmitVersion.value = selectedSkill.value.workingVersion || selectedSkill.value.displayVersion || ''
|
|
|
|
|
|
reviewSubmitReviewer.value = selectedSkill.value.reviewer || ''
|
|
|
|
|
|
reviewSubmitOpen.value = true
|
|
|
|
|
|
await loadReviewSubmitReviewers()
|
|
|
|
|
|
if (
|
|
|
|
|
|
!reviewSubmitReviewerOptions.value.some(
|
|
|
|
|
|
(item) => item.value === reviewSubmitReviewer.value
|
|
|
|
|
|
)
|
|
|
|
|
|
) {
|
|
|
|
|
|
reviewSubmitReviewer.value = reviewSubmitReviewerOptions.value[0]?.value || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeSubmitReviewDialog() {
|
|
|
|
|
|
if (detailBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
reviewSubmitOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitSelectedRuleForReview() {
|
|
|
|
|
|
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const version = normalizeText(reviewSubmitVersion.value)
|
|
|
|
|
|
const reviewer = normalizeText(reviewSubmitReviewer.value)
|
|
|
|
|
|
if (!version) {
|
|
|
|
|
|
toast('请输入送审版本号。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!reviewer) {
|
|
|
|
|
|
toast('请选择审核人。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actionState.value = 'review-pending'
|
|
|
|
|
|
try {
|
|
|
|
|
|
await createAgentAssetReview(
|
|
|
|
|
|
selectedSkill.value.id,
|
|
|
|
|
|
{
|
|
|
|
|
|
version,
|
|
|
|
|
|
reviewer,
|
|
|
|
|
|
review_status: 'pending',
|
|
|
|
|
|
review_note: buildReviewNote('pending')
|
|
|
|
|
|
},
|
|
|
|
|
|
{ actor: resolveActor() }
|
|
|
|
|
|
)
|
|
|
|
|
|
reviewSubmitOpen.value = false
|
|
|
|
|
|
await refreshCurrentAssets()
|
|
|
|
|
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
|
|
|
|
|
toast(`规则版本 ${version} 已提交给 ${reviewer} 审核。`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '规则审核提交失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
async function activateSelectedRule() {
|
|
|
|
|
|
if (!selectedSkill.value || !selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actionState.value = 'activate'
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await activateAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
|
|
|
|
|
|
await refreshCurrentAssets()
|
|
|
|
|
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
|
|
|
|
|
toast('规则已正式上线。')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '规则上线失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
|
}
|
2026-05-09 15:46:16 +00:00
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
async function restoreSelectedVersion(version) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
!selectedSkill.value ||
|
|
|
|
|
|
!selectedSkillIsRule.value ||
|
|
|
|
|
|
!canManageSelected.value ||
|
|
|
|
|
|
detailBusy.value ||
|
|
|
|
|
|
!version
|
|
|
|
|
|
) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actionState.value = `restore-${version}`
|
|
|
|
|
|
try {
|
|
|
|
|
|
await restoreAgentAssetVersion(selectedSkill.value.id, version, { actor: resolveActor() })
|
|
|
|
|
|
await refreshCurrentAssets()
|
|
|
|
|
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
|
|
|
|
|
toast(`已基于 ${version} 生成新的工作版本。`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '历史版本恢复失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionState.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadVersionTimeline(assetId = selectedSkill.value?.id, options = {}) {
|
|
|
|
|
|
if (!assetId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
versionTimelineLoading.value = true
|
|
|
|
|
|
versionTimelineError.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = await fetchAgentAssetVersionTimeline(assetId)
|
|
|
|
|
|
versionTimelineItems.value = Array.isArray(payload) ? payload : []
|
|
|
|
|
|
} catch (error) {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
versionTimelineError.value = error?.message || '操作记录加载失败,请稍后重试。'
|
2026-05-18 02:51:25 +00:00
|
|
|
|
if (!options.silent) {
|
|
|
|
|
|
toast(versionTimelineError.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
versionTimelineLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function openVersionTimeline() {
|
|
|
|
|
|
if (!selectedSkill.value?.id) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
versionTimelineOpen.value = true
|
|
|
|
|
|
await loadVersionTimeline(selectedSkill.value.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeVersionTimeline() {
|
|
|
|
|
|
versionTimelineOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function openVersionCompare(options = {}) {
|
|
|
|
|
|
if (!selectedSkill.value?.id) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const defaultBase =
|
|
|
|
|
|
options.baseVersion || selectedSkill.value.publishedVersion || selectedSkill.value.workingVersion || ''
|
|
|
|
|
|
let defaultTarget =
|
|
|
|
|
|
options.targetVersion || selectedSkill.value.workingVersion || selectedSkill.value.publishedVersion || ''
|
|
|
|
|
|
if (!options.targetVersion && defaultBase === defaultTarget) {
|
|
|
|
|
|
defaultTarget =
|
|
|
|
|
|
selectedSkill.value.history.find((item) => item.version !== defaultBase)?.version || defaultTarget
|
|
|
|
|
|
}
|
|
|
|
|
|
compareBaseVersion.value = defaultBase
|
|
|
|
|
|
compareTargetVersion.value = defaultTarget
|
|
|
|
|
|
versionCompareOpen.value = true
|
|
|
|
|
|
await loadVersionCompare()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:42:23 +00:00
|
|
|
|
function openSpreadsheetChangeRecord(item) {
|
|
|
|
|
|
if (selectedSkill.value?.isPreviewMock) {
|
|
|
|
|
|
toast('预览数据暂不支持真实的线上差异对比。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const publishedVersion = normalizeText(selectedSkill.value?.publishedVersion)
|
|
|
|
|
|
if (!selectedSkill.value?.id || !publishedVersion || publishedVersion === '-') {
|
|
|
|
|
|
toast('当前还没有线上版本,暂时无法查看与线上差异。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
openVersionCompare({
|
|
|
|
|
|
baseVersion: publishedVersion,
|
|
|
|
|
|
targetVersion: item.version
|
|
|
|
|
|
}).catch(() => {})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 02:51:25 +00:00
|
|
|
|
function closeVersionCompare() {
|
|
|
|
|
|
versionCompareOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadVersionCompare() {
|
|
|
|
|
|
if (!selectedSkill.value?.id || !compareBaseVersion.value || !compareTargetVersion.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
versionCompareLoading.value = true
|
|
|
|
|
|
versionCompareError.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
versionComparePayload.value = await compareAgentAssetSpreadsheetVersions(
|
|
|
|
|
|
selectedSkill.value.id,
|
|
|
|
|
|
compareBaseVersion.value,
|
|
|
|
|
|
compareTargetVersion.value
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
versionComparePayload.value = null
|
|
|
|
|
|
versionCompareError.value = error?.message || '版本差异对比失败,请稍后重试。'
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
versionCompareLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 06:32:38 +00:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
document.addEventListener('click', handleDocumentClick)
|
|
|
|
|
|
loadAssets({ force: true }).catch(() => {})
|
|
|
|
|
|
loadRuns().catch(() => {})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
2026-05-18 09:42:23 +00:00
|
|
|
|
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
2026-05-18 02:51:25 +00:00
|
|
|
|
destroySpreadsheetOnlyOfficeEditor()
|
2026-05-11 06:32:38 +00:00
|
|
|
|
document.removeEventListener('click', handleDocumentClick)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
|
return {
|
|
|
|
|
|
tabs,
|
2026-05-09 15:46:16 +00:00
|
|
|
|
activeType,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
activeTabLabel,
|
|
|
|
|
|
selectedSkill,
|
|
|
|
|
|
versionSwitchTarget,
|
|
|
|
|
|
keyword,
|
2026-05-09 15:46:16 +00:00
|
|
|
|
createButtonLabel,
|
|
|
|
|
|
hintText,
|
2026-05-11 01:53:30 +00:00
|
|
|
|
searchPlaceholder,
|
2026-05-09 15:46:16 +00:00
|
|
|
|
tableColumns,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
showRuntimeColumn,
|
|
|
|
|
|
showVersionColumn,
|
2026-05-11 06:33:46 +00:00
|
|
|
|
showMetricColumn,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
showStatusColumn,
|
2026-05-09 15:46:16 +00:00
|
|
|
|
visibleSkills,
|
2026-05-13 06:52:30 +00:00
|
|
|
|
auditEmptyState,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
loading,
|
|
|
|
|
|
errorMessage,
|
|
|
|
|
|
detailLoading,
|
|
|
|
|
|
detailError,
|
|
|
|
|
|
selectedDomain,
|
|
|
|
|
|
selectedOwner,
|
|
|
|
|
|
selectedStatus,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
selectedRiskScenario,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
selectedDomainLabel,
|
|
|
|
|
|
selectedOwnerLabel,
|
|
|
|
|
|
selectedStatusLabel,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
selectedRiskScenarioLabel,
|
|
|
|
|
|
showRiskScenarioFilter,
|
|
|
|
|
|
showStatusFilter,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
domainOptions,
|
|
|
|
|
|
ownerOptions,
|
|
|
|
|
|
statusOptions: STATUS_OPTIONS,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
riskScenarioOptions: RISK_SCENARIO_OPTIONS,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
activeFilterPopover,
|
|
|
|
|
|
activeFilterTokens,
|
|
|
|
|
|
canManageSelected,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
canEditSelected,
|
|
|
|
|
|
canSubmitReview,
|
2026-05-18 09:42:23 +00:00
|
|
|
|
hasReviewSubmitReviewers,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
canReviewSelected,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
canEditMarkdown,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
canUploadSpreadsheet,
|
|
|
|
|
|
canDownloadSpreadsheet,
|
|
|
|
|
|
canEditSpreadsheetInline,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
canActivateSelected,
|
|
|
|
|
|
activateBlockedReason,
|
|
|
|
|
|
selectedSkillIsRule,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
selectedSkillUsesSpreadsheet,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
selectedSkillUsesJsonRisk,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
selectedSpreadsheetFileName,
|
|
|
|
|
|
selectedSpreadsheetVersionModeLabel,
|
|
|
|
|
|
selectedVersionTimelineItems,
|
2026-05-18 09:42:23 +00:00
|
|
|
|
selectedSpreadsheetChangeRecords,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
detailBusy,
|
|
|
|
|
|
actionState,
|
2026-05-18 09:42:23 +00:00
|
|
|
|
reviewSubmitOpen,
|
|
|
|
|
|
reviewSubmitVersion,
|
|
|
|
|
|
reviewSubmitReviewer,
|
|
|
|
|
|
reviewSubmitReviewerLoading,
|
|
|
|
|
|
reviewSubmitReviewerOptions,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
showReviewNote,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
spreadsheetUploadInput,
|
|
|
|
|
|
spreadsheetOnlyOfficeLoading,
|
|
|
|
|
|
spreadsheetOnlyOfficeError,
|
|
|
|
|
|
spreadsheetOnlyOfficeReady,
|
|
|
|
|
|
spreadsheetOnlyOfficeHostId,
|
|
|
|
|
|
versionTimelineOpen,
|
|
|
|
|
|
versionTimelineLoading,
|
|
|
|
|
|
versionTimelineError,
|
|
|
|
|
|
versionCompareOpen,
|
|
|
|
|
|
versionCompareLoading,
|
|
|
|
|
|
versionCompareError,
|
|
|
|
|
|
versionComparePayload,
|
|
|
|
|
|
versionCompareCellRows,
|
|
|
|
|
|
versionCompareSheetRows,
|
2026-05-18 09:42:23 +00:00
|
|
|
|
spreadsheetChangeDetailOpen,
|
|
|
|
|
|
selectedSpreadsheetChangeRecord,
|
|
|
|
|
|
selectedSpreadsheetChangeSheetRows,
|
|
|
|
|
|
selectedSpreadsheetChangeCellRows,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
compareBaseVersion,
|
|
|
|
|
|
compareTargetVersion,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
openAssetDetail,
|
|
|
|
|
|
closeDetail,
|
|
|
|
|
|
resetFilters,
|
2026-05-13 06:52:30 +00:00
|
|
|
|
handleAuditEmptyAction,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
toggleFilterPopover,
|
|
|
|
|
|
selectFilter,
|
|
|
|
|
|
closeFilterPopover,
|
2026-05-09 15:46:16 +00:00
|
|
|
|
openVersionSwitch,
|
|
|
|
|
|
cancelVersionSwitch,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
confirmVersionSwitch,
|
|
|
|
|
|
saveRuleMarkdown,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
saveRuleRuntimeJson,
|
2026-05-19 20:23:58 +08:00
|
|
|
|
saveRiskRuleJson,
|
|
|
|
|
|
formatRiskRuleJson,
|
|
|
|
|
|
downloadRiskRuleJson,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
triggerSpreadsheetUpload,
|
|
|
|
|
|
downloadSpreadsheetFile,
|
|
|
|
|
|
handleSpreadsheetFileInput,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
reviewSelectedRule,
|
2026-05-18 09:42:23 +00:00
|
|
|
|
openSubmitReviewDialog,
|
|
|
|
|
|
closeSubmitReviewDialog,
|
|
|
|
|
|
submitSelectedRuleForReview,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
activateSelectedRule,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
restoreSelectedVersion,
|
|
|
|
|
|
openVersionTimeline,
|
|
|
|
|
|
closeVersionTimeline,
|
2026-05-18 09:42:23 +00:00
|
|
|
|
openSpreadsheetChangeRecord,
|
|
|
|
|
|
openSpreadsheetChangeDetail,
|
|
|
|
|
|
closeSpreadsheetChangeDetail,
|
2026-05-18 02:51:25 +00:00
|
|
|
|
openVersionCompare,
|
|
|
|
|
|
closeVersionCompare,
|
|
|
|
|
|
loadVersionCompare,
|
2026-05-11 06:32:38 +00:00
|
|
|
|
loadAssets
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|