feat: 新增数字员工管理页面与工作台首页重构

后端优化 agent 资产种子初始化和常量配置,前端新增数字员工
视图和调度对话框组件,重构个人工作台首页布局和洞察面板,
完善审计页面数字员工详情和运行时模型,优化侧边栏导航和图
标配置,新增工作台摘要和工作台数据模块,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-28 09:30:34 +08:00
parent d4d5d40569
commit 04cd6d0f81
38 changed files with 3413 additions and 1301 deletions

View File

@@ -1,224 +1,230 @@
<template>
<div class="app" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
<div class="app-sidebar">
<SidebarRail
:nav-items="filteredNavItems"
:active-view="activeView"
:company-name="companyProfile.name"
:company-logo="companyProfile.logo"
:current-user="currentUser"
:collapsed="sidebarCollapsed"
@navigate="handleNavigate"
@open-chat="openSmartEntry"
@logout="handleLogout"
@toggle-collapse="toggleSidebarCollapsed"
/>
</div>
<main
class="main"
:class="{
<template>
<div class="app" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
<div class="app-sidebar">
<SidebarRail
:nav-items="filteredNavItems"
:active-view="activeView"
:company-name="companyProfile.name"
:company-logo="companyProfile.logo"
:current-user="currentUser"
:collapsed="sidebarCollapsed"
@navigate="handleNavigate"
@open-chat="openSmartEntry"
@logout="handleLogout"
@toggle-collapse="toggleSidebarCollapsed"
/>
</div>
<main
class="main"
:class="{
'overview-main': activeView === 'overview',
'workbench-main': activeView === 'workbench',
'documents-main': activeView === 'documents',
'budget-main': activeView === 'budget',
'policies-main': activeView === 'policies',
'audit-main': activeView === 'audit',
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
'logs-main': activeView === 'logs',
'employees-main': activeView === 'employees',
'settings-main': activeView === 'settings'
}"
>
<TopBar
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen)"
:current-view="topBarView"
:search="search"
:active-view="activeView"
:ranges="ranges"
:active-range="activeRange"
:employee-summary="employeeSummary"
:knowledge-summary="knowledgeSummary"
:logs-summary="logsSummary"
:request-summary="requestSummary"
:document-summary="documentSummary"
:workbench-summary="workbenchSummary"
:detail-mode="detailMode"
:log-detail-mode="logDetailMode"
:detail-alerts="detailAlerts"
:custom-range="customRange"
@update:search="search = $event"
@update:active-range="activeRange = $event"
@update:custom-range="customRange = $event"
@batch-approve="toast('已批量通过 23 条审批任务')"
@new-application="openExpenseApplicationCreate"
/>
<FilterBar
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'documents' && activeView !== 'budget' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'logs' && activeView !== 'employees' && activeView !== 'settings'"
:compact="activeView === 'overview'"
:filters="filters"
:ranges="ranges"
:active-range="activeRange"
@update:active-range="activeRange = $event"
/>
<section
class="workarea"
:class="{
'audit-main': activeView === 'audit',
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
'digital-employees-main': activeView === 'digitalEmployees',
'logs-main': activeView === 'logs',
'employees-main': activeView === 'employees',
'settings-main': activeView === 'settings'
}"
>
<TopBar
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen)"
:current-view="topBarView"
:search="search"
:active-view="activeView"
:ranges="ranges"
:active-range="activeRange"
:employee-summary="employeeSummary"
:knowledge-summary="knowledgeSummary"
:logs-summary="logsSummary"
:request-summary="requestSummary"
:document-summary="documentSummary"
:workbench-summary="workbenchSummary"
:detail-mode="detailMode"
:log-detail-mode="logDetailMode"
:detail-alerts="detailAlerts"
:custom-range="customRange"
@update:search="search = $event"
@update:active-range="activeRange = $event"
@update:custom-range="customRange = $event"
@batch-approve="toast('已批量通过 23 条审批任务')"
@new-application="openExpenseApplicationCreate"
/>
<FilterBar
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'documents' && activeView !== 'budget' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'digitalEmployees' && activeView !== 'logs' && activeView !== 'employees' && activeView !== 'settings'"
:compact="activeView === 'overview'"
:filters="filters"
:ranges="ranges"
:active-range="activeRange"
@update:active-range="activeRange = $event"
/>
<section
class="workarea"
:class="{
'documents-workarea': activeView === 'documents',
'workbench-workarea': activeView === 'workbench',
'budget-workarea': activeView === 'budget',
'policies-workarea': activeView === 'policies',
'audit-workarea': activeView === 'audit',
'logs-workarea': activeView === 'logs',
'employees-workarea': activeView === 'employees',
'settings-workarea': activeView === 'settings'
}"
>
<OverviewView
v-if="activeView === 'overview'"
:filtered-requests="filteredRequests"
@approve="handleApprove"
@reject="handleReject"
/>
<PersonalWorkbenchView
v-else-if="activeView === 'workbench'"
:assistant-modal-open="smartEntryOpen"
@open-assistant="openSmartEntry"
/>
'audit-workarea': activeView === 'audit',
'digital-employees-workarea': activeView === 'digitalEmployees',
'logs-workarea': activeView === 'logs',
'employees-workarea': activeView === 'employees',
'settings-workarea': activeView === 'settings'
}"
>
<OverviewView
v-if="activeView === 'overview'"
:filtered-requests="filteredRequests"
@approve="handleApprove"
@reject="handleReject"
/>
<PersonalWorkbenchView
v-else-if="activeView === 'workbench'"
:assistant-modal-open="smartEntryOpen"
:workbench-summary="workbenchSummary"
@open-assistant="openSmartEntry"
/>
<TravelRequestDetailView
v-else-if="activeView === 'documents' && detailMode && selectedRequest"
:request="selectedRequest"
back-label="返回单据中心"
@back-to-requests="closeRequestDetail"
@open-assistant="openSmartEntry"
@request-updated="handleRequestUpdated"
@request-deleted="handleRequestDeleted"
/>
<DocumentsCenterView
v-else-if="activeView === 'documents'"
:filtered-requests="filteredRequests"
:has-data="requests.length > 0"
:loading="requestsLoading"
:error="requestsError"
@open-document="openRequestDetail"
@create-request="openTravelCreate"
@create-application="openExpenseApplicationCreate"
@reload="reloadRequests"
@summary-change="documentSummary = $event"
/>
@open-assistant="openSmartEntry"
@request-updated="handleRequestUpdated"
@request-deleted="handleRequestDeleted"
/>
<DocumentsCenterView
v-else-if="activeView === 'documents'"
:filtered-requests="filteredRequests"
:has-data="requests.length > 0"
:loading="requestsLoading"
:error="requestsError"
@open-document="openRequestDetail"
@create-request="openTravelCreate"
@create-application="openExpenseApplicationCreate"
@reload="reloadRequests"
@summary-change="documentSummary = $event"
/>
<BudgetCenterView
v-else-if="activeView === 'budget'"
:current-user="currentUser"
@open-assistant="openSmartEntry"
/>
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" />
<LogDetailView v-else-if="activeView === 'logs' && logDetailMode" />
<LogsView v-else-if="activeView === 'logs'" @summary-change="logsSummary = $event" />
<EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" />
<SettingsView v-else />
</section>
</main>
<TravelReimbursementCreateView
v-if="smartEntryOpen"
:key="smartEntrySessionId"
:initial-prompt="smartEntryContext.prompt"
:initial-files="smartEntryContext.files"
:initial-conversation="smartEntryContext.conversation"
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" />
<DigitalEmployeesView v-else-if="activeView === 'digitalEmployees'" />
<LogDetailView v-else-if="activeView === 'logs' && logDetailMode" />
<LogsView v-else-if="activeView === 'logs'" @summary-change="logsSummary = $event" />
<EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" />
<SettingsView v-else />
</section>
</main>
<TravelReimbursementCreateView
v-if="smartEntryOpen"
:key="smartEntrySessionId"
:initial-prompt="smartEntryContext.prompt"
:initial-files="smartEntryContext.files"
:initial-conversation="smartEntryContext.conversation"
:entry-source="smartEntryContext.source"
:request-context="smartEntryContext.request"
:invalidated-draft-claim-id="smartEntryInvalidatedDraftClaimId"
:reopen-token="smartEntryRevealToken"
@close="closeSmartEntry"
@draft-saved="handleDraftSaved"
/>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import SidebarRail from '../components/layout/SidebarRail.vue'
import TopBar from '../components/layout/TopBar.vue'
import FilterBar from '../components/layout/FilterBar.vue'
import OverviewView from './OverviewView.vue'
import PersonalWorkbenchView from './PersonalWorkbenchView.vue'
/>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import SidebarRail from '../components/layout/SidebarRail.vue'
import TopBar from '../components/layout/TopBar.vue'
import FilterBar from '../components/layout/FilterBar.vue'
import OverviewView from './OverviewView.vue'
import PersonalWorkbenchView from './PersonalWorkbenchView.vue'
import TravelReimbursementCreateView from './TravelReimbursementCreateView.vue'
import TravelRequestDetailView from './TravelRequestDetailView.vue'
import DocumentsCenterView from './DocumentsCenterView.vue'
import BudgetCenterView from './BudgetCenterView.vue'
import PoliciesView from './PoliciesView.vue'
import AuditView from './AuditView.vue'
import LogsView from './LogsView.vue'
import LogDetailView from './LogDetailView.vue'
import EmployeeManagementView from './EmployeeManagementView.vue'
import SettingsView from './SettingsView.vue'
import { useAppShell } from '../composables/useAppShell.js'
import { useSystemState } from '../composables/useSystemState.js'
import { filterNavItemsByAccess } from '../utils/accessControl.js'
const employeeSummary = ref(null)
const knowledgeSummary = ref(null)
const logsSummary = ref(null)
const documentSummary = ref(null)
const auditDetailOpen = ref(false)
const sidebarCollapsed = ref(true)
function toggleSidebarCollapsed() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
const {
activeRange,
activeView,
closeRequestDetail,
closeSmartEntry,
customRange,
detailAlerts,
detailMode,
logDetailMode,
filteredRequests,
filters,
handleApprove,
handleDraftSaved,
handleNavigate,
handleReject,
handleRequestDeleted,
handleRequestUpdated,
navItems,
openExpenseApplicationCreate,
openRequestDetail,
openSmartEntry,
openTravelCreate,
ranges,
requestSummary,
workbenchSummary,
requestsError,
requestsLoading,
reloadRequests,
requests,
search,
selectedRequest,
smartEntryContext,
import AuditView from './AuditView.vue'
import DigitalEmployeesView from './DigitalEmployeesView.vue'
import LogsView from './LogsView.vue'
import LogDetailView from './LogDetailView.vue'
import EmployeeManagementView from './EmployeeManagementView.vue'
import SettingsView from './SettingsView.vue'
import { useAppShell } from '../composables/useAppShell.js'
import { useSystemState } from '../composables/useSystemState.js'
import { filterNavItemsByAccess } from '../utils/accessControl.js'
const employeeSummary = ref(null)
const knowledgeSummary = ref(null)
const logsSummary = ref(null)
const documentSummary = ref(null)
const auditDetailOpen = ref(false)
const sidebarCollapsed = ref(false)
function toggleSidebarCollapsed() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
const {
activeRange,
activeView,
closeRequestDetail,
closeSmartEntry,
customRange,
detailAlerts,
detailMode,
logDetailMode,
filteredRequests,
filters,
handleApprove,
handleDraftSaved,
handleNavigate,
handleReject,
handleRequestDeleted,
handleRequestUpdated,
navItems,
openExpenseApplicationCreate,
openRequestDetail,
openSmartEntry,
openTravelCreate,
ranges,
requestSummary,
workbenchSummary,
requestsError,
requestsLoading,
reloadRequests,
requests,
search,
selectedRequest,
smartEntryContext,
smartEntryInvalidatedDraftClaimId,
smartEntryOpen,
smartEntryRevealToken,
smartEntrySessionId,
toast,
topBarView
} = useAppShell()
const { companyProfile, currentUser, logout } = useSystemState()
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
function handleLogout() {
logout('manual')
}
</script>
toast,
topBarView
} = useAppShell()
const { companyProfile, currentUser, logout } = useSystemState()
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
function handleLogout() {
logout('manual')
}
</script>

View File

@@ -112,7 +112,6 @@
:risk-rule-test-passed="riskRuleTestPassed"
/>
<div
v-else
class="detail-grid"

View File

@@ -0,0 +1,737 @@
<template>
<section class="digital-employees-view skill-center">
<Transition name="skill-view" mode="out-in">
<article
v-if="selectedEmployee"
key="detail"
class="skill-detail digital-employee-detail"
>
<div class="detail-scroll">
<section v-if="detailError" class="detail-inline-state panel error">
<i class="mdi mdi-alert-circle-outline"></i>
<div>
<strong>数字员工详情加载失败</strong>
<p>{{ detailError }}</p>
</div>
</section>
<TableLoadingState
v-else-if="detailLoading && selectedEmployee.loading"
class="detail-loading-state panel"
variant="panel"
title="正在加载数字员工详情"
message="列表数据已就绪,正在补充 Skills 源文件和执行配置"
icon="mdi mdi-account-cog-outline"
:show-skeleton="false"
/>
<AuditDigitalEmployeeDetail
v-else
:selected-skill="selectedEmployee"
:can-edit="canEditDigitalEmployeeSource"
:detail-busy="detailBusy"
:action-state="actionState"
@save-source="saveDigitalEmployeeSource"
/>
</div>
<footer class="detail-actions digital-employee-detail-actions">
<button class="back-action" type="button" @click="closeEmployeeDetail">
<i class="mdi mdi-arrow-left"></i>
<span>返回数字员工列表</span>
</button>
<div class="detail-action-group">
<button
class="minor-action enable-action"
:class="{ 'is-on': selectedEmployee.statusValue === 'active' }"
type="button"
:disabled="!canOperateDigitalEmployee || detailBusy"
@click="toggleDigitalEmployeeRunning(selectedEmployee)"
>
<i :class="selectedEmployee.statusValue === 'active' ? 'mdi mdi-toggle-switch' : 'mdi mdi-toggle-switch-off-outline'"></i>
<span>{{ selectedEmployee.statusValue === 'active' ? '停止运行' : '启用运行' }}</span>
</button>
<button
class="minor-action"
type="button"
:disabled="!canOperateDigitalEmployee || detailBusy"
@click="openDigitalEmployeeSchedule(selectedEmployee)"
>
<i class="mdi mdi-clock-edit-outline"></i>
<span>定时设置</span>
</button>
<button
class="minor-action success-action"
type="button"
:disabled="!canOperateDigitalEmployee || detailBusy"
@click="runDigitalEmployeeNow(selectedEmployee)"
>
<i class="mdi mdi-play-circle-outline"></i>
<span>{{ actionBusy(selectedEmployee.id, 'run-digital-now') ? '运行中...' : '立即运行' }}</span>
</button>
</div>
</footer>
</article>
<article v-else key="list" class="skill-list panel digital-employees-list">
<nav class="status-tabs" aria-label="数字员工类型">
<button class="active" type="button">数字员工</button>
</nav>
<div class="list-toolbar">
<div class="filter-set">
<label class="search-filter">
<i class="mdi mdi-magnify"></i>
<input v-model="keyword" type="search" placeholder="搜索技能名称、编号、执行计划或维护人" />
</label>
<AuditPickerFilter
id="status"
title="选择资产状态"
close-label="关闭资产状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedStatusLabel"
:options="statusOptions"
:selected-value="selectedStatus"
@toggle="toggleFilterPopover"
@close="closeFilterPopover"
@select="selectFilter('status', $event)"
/>
<AuditPickerFilter
id="enabled"
title="选择启动状态"
close-label="关闭启动状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedEnabledLabel"
:options="enabledStateOptions"
:selected-value="selectedEnabledState"
@toggle="toggleFilterPopover"
@close="closeFilterPopover"
@select="selectFilter('enabled', $event)"
/>
<AuditPickerFilter
id="executionMode"
title="选择执行方式"
close-label="关闭执行方式选择"
:active-filter-popover="activeFilterPopover"
:label="selectedExecutionModeLabel"
:options="executionModeOptions"
:selected-value="selectedExecutionMode"
@toggle="toggleFilterPopover"
@close="closeFilterPopover"
@select="selectFilter('executionMode', $event)"
/>
</div>
<div class="toolbar-actions">
<button
v-if="keyword || activeFilterTokens.length"
class="ghost-filter-btn"
type="button"
@click="resetFilters"
>
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<button
class="create-btn digital-refresh-action"
type="button"
:disabled="loading"
@click="loadEmployees"
>
<i class="mdi mdi-refresh"></i>
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
</button>
</div>
</div>
<p class="hint">
<i class="mdi mdi-information-outline"></i>
集中查看后台自动执行的技能执行计划和运行状态
</p>
<div v-if="activeFilterTokens.length" class="active-filter-strip">
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
{{ token }}
</span>
</div>
<div
class="table-wrap digital-table-wrap"
:class="{ 'is-empty': !loading && !errorMessage && !visibleEmployees.length }"
>
<div v-if="loading" class="table-state">
<TableLoadingState
variant="panel"
title="数字员工同步中"
message="正在读取后台自动执行技能列表"
icon="mdi mdi-account-cog-outline"
/>
</div>
<div v-else-if="errorMessage" class="table-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<strong>数字员工加载失败</strong>
<p>{{ errorMessage }}</p>
</div>
<TableEmptyState
v-else-if="!visibleEmployees.length"
eyebrow="数字员工"
title="暂无匹配的数字员工"
description="当前没有符合搜索条件的后台执行技能。"
icon="mdi mdi-account-cog-outline"
tone="theme"
art-label="STAFF"
:tips="['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']"
/>
<table v-else class="digital-employees-table">
<colgroup>
<col class="col-skill">
<col class="col-schedule">
<col class="col-mode">
<col class="col-skill-type">
<col class="col-status">
<col class="col-enabled">
<col class="col-updated">
</colgroup>
<thead>
<tr>
<th>技能名称</th>
<th>执行计划</th>
<th>触发方式</th>
<th>技能类型</th>
<th>资产状态</th>
<th>启动状态</th>
<th>最近更新</th>
</tr>
</thead>
<tbody>
<tr
v-for="employee in visibleEmployees"
:key="employee.id"
@click="openEmployeeDetail(employee)"
>
<td>
<div class="skill-name-cell">
<span class="skill-avatar" :class="employee.badgeTone">{{ employee.short }}</span>
<div>
<strong>{{ employee.name }}</strong>
<span class="skill-list-subtitle">{{ employee.code }}</span>
</div>
</div>
</td>
<td><span class="scope-pill">{{ employee.scope }}</span></td>
<td>{{ employee.executionMode }}</td>
<td><span class="scope-pill skill-type-pill">{{ employee.skillCategory }}</span></td>
<td>
<span :class="['status-pill', employee.statusTone]">{{ employee.status }}</span>
</td>
<td><span :class="['status-pill', employee.enabledTone]">{{ employee.enabledLabel }}</span></td>
<td>{{ employee.updatedAt || '-' }}</td>
</tr>
</tbody>
</table>
</div>
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot">
<span class="page-summary">当前展示 {{ visibleEmployees.length }} 条数字员工</span>
</footer>
</article>
</Transition>
<DigitalEmployeeScheduleDialog
v-model="scheduleForm"
:open="scheduleEditorOpen"
:target-name="scheduleTarget?.name || ''"
:preview-label="schedulePreviewLabel"
:error-message="scheduleEditorError"
:busy="scheduleEditorBusy"
:can-save="canOperateDigitalEmployee"
@close="closeDigitalEmployeeSchedule"
@save="saveDigitalEmployeeSchedule"
/>
</section>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import AuditDigitalEmployeeDetail from '../components/audit/AuditDigitalEmployeeDetail.vue'
import AuditPickerFilter from '../components/audit/AuditPickerFilter.vue'
import DigitalEmployeeScheduleDialog from '../components/audit/DigitalEmployeeScheduleDialog.vue'
import TableEmptyState from '../components/shared/TableEmptyState.vue'
import TableLoadingState from '../components/shared/TableLoadingState.vue'
import { useSystemState } from '../composables/useSystemState.js'
import { useToast } from '../composables/useToast.js'
import {
activateAgentAsset,
createAgentAssetVersion,
fetchAgentAssetDetail,
fetchAgentAssets,
updateAgentAsset
} from '../services/agentAssets.js'
import { runOrchestrator } from '../services/orchestrator.js'
import { isPlatformAdminUser } from '../utils/accessControl.js'
import {
DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
buildDigitalEmployeeDetailMeta,
buildDigitalEmployeeListMeta,
formatDigitalEmployeeCron,
isDigitalEmployeeAsset
} from './scripts/auditViewDigitalEmployeeModel.js'
import {
buildDigitalEmployeeScheduleConfig,
buildDigitalEmployeeScheduleCron,
createDigitalEmployeeScheduleForm,
resolveDigitalEmployeeScheduleValue
} from './scripts/digitalEmployeeScheduleModel.js'
import { incrementVersion } from './scripts/auditViewRuntimeModel.js'
import {
ENABLED_STATE_OPTIONS,
formatDateTime,
normalizeText,
resolveStatusMeta,
STATUS_OPTIONS
} from './scripts/auditViewModel.js'
const { currentUser } = useSystemState()
const { toast } = useToast()
const employees = ref([])
const selectedEmployee = ref(null)
const selectedEmployeeId = ref('')
const keyword = ref('')
const selectedStatus = ref('')
const selectedEnabledState = ref('')
const selectedExecutionMode = ref('')
const activeFilterPopover = ref('')
const loading = ref(false)
const detailLoading = ref(false)
const errorMessage = ref('')
const detailError = ref('')
const actionState = ref('')
const busyEmployeeId = ref('')
const scheduleEditorOpen = ref(false)
const scheduleTarget = ref(null)
const scheduleForm = ref(createDigitalEmployeeScheduleForm())
const scheduleEditorError = ref('')
const isAdmin = computed(() => isPlatformAdminUser(currentUser.value))
const detailBusy = computed(() => Boolean(detailLoading.value || actionState.value))
const canOperateDigitalEmployee = computed(() => isAdmin.value && Boolean(selectedEmployee.value))
const canEditDigitalEmployeeSource = computed(() => canOperateDigitalEmployee.value)
const scheduleEditorBusy = computed(() => actionState.value === 'save-digital-schedule')
const statusOptions = STATUS_OPTIONS
const enabledStateOptions = ENABLED_STATE_OPTIONS
const executionModeOptions = [
{ value: '', label: '全部执行方式' },
{ value: 'timed', label: '定时执行' },
{ value: 'manual', label: '手动触发' }
]
const selectedStatusLabel = computed(() =>
statusOptions.find((item) => item.value === selectedStatus.value)?.label || '全部状态'
)
const selectedEnabledLabel = computed(() =>
enabledStateOptions.find((item) => item.value === selectedEnabledState.value)?.label || '全部启动状态'
)
const selectedExecutionModeLabel = computed(() =>
executionModeOptions.find((item) => item.value === selectedExecutionMode.value)?.label || '全部执行方式'
)
const activeFilterTokens = computed(() => {
const tokens = []
if (selectedStatus.value) {
tokens.push(`资产状态:${selectedStatusLabel.value}`)
}
if (selectedEnabledState.value) {
tokens.push(`启动状态:${selectedEnabledLabel.value}`)
}
if (selectedExecutionMode.value) {
tokens.push(`执行方式:${selectedExecutionModeLabel.value}`)
}
return tokens
})
const schedulePreviewLabel = computed(() => {
try {
return formatDigitalEmployeeCron(buildDigitalEmployeeScheduleCron(scheduleForm.value))
} catch {
return '表达式待确认'
}
})
const visibleEmployees = computed(() => {
const searchText = normalizeText(keyword.value).toLowerCase()
return employees.value.filter((item) => {
const matchesKeyword = searchText
? [
item.name,
item.code,
item.summary,
item.owner,
item.scope,
item.executionMode,
item.skillCategory,
item.status,
item.enabledLabel
]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(searchText))
: true
const matchesStatus = selectedStatus.value ? item.statusValue === selectedStatus.value : true
const matchesEnabled = selectedEnabledState.value
? (selectedEnabledState.value === 'enabled') === Boolean(item.isEnabledValue)
: true
const matchesExecutionMode = selectedExecutionMode.value
? item.executionModeValue === selectedExecutionMode.value
: true
return matchesKeyword && matchesStatus && matchesEnabled && matchesExecutionMode
})
})
function toggleFilterPopover(id) {
activeFilterPopover.value = activeFilterPopover.value === id ? '' : id
}
function closeFilterPopover() {
activeFilterPopover.value = ''
}
function selectFilter(type, value) {
if (type === 'status') {
selectedStatus.value = value
}
if (type === 'enabled') {
selectedEnabledState.value = value
}
if (type === 'executionMode') {
selectedExecutionMode.value = value
}
closeFilterPopover()
}
function resetFilters() {
keyword.value = ''
selectedStatus.value = ''
selectedEnabledState.value = ''
selectedExecutionMode.value = ''
closeFilterPopover()
}
function resolveActor() {
const user = currentUser.value || {}
return normalizeText(user.name) || normalizeText(user.username) || 'system'
}
function buildEmployeeListItem(asset) {
const meta = buildDigitalEmployeeListMeta(asset)
const statusMeta = resolveStatusMeta(asset.status)
const displayName = meta.name || '数字员工技能'
return {
id: asset.id,
rawCode: asset.code,
short: displayName.slice(0, 2),
badgeTone: 'blue',
name: displayName,
code: meta.code,
summary: meta.summary,
owner: meta.owner,
scope: meta.scope,
executionMode: meta.executionMode,
executionModeValue: meta.executionMode === '定时执行' ? 'timed' : 'manual',
skillCategory: meta.skillCategory,
version: asset.working_version || asset.current_version || '-',
currentVersion: asset.current_version || '-',
status: statusMeta.label,
statusValue: asset.status,
statusTone: statusMeta.tone,
enabledLabel: meta.enabledLabel,
enabledTone: meta.enabledTone,
isEnabledValue: meta.enabled,
configJson: asset.config_json || {},
updatedAt: formatDateTime(asset.updated_at),
updatedAtRaw: asset.updated_at || '',
digitalEmployee: meta
}
}
function buildEmployeePlaceholder(employee) {
return {
...employee,
type: 'digitalEmployees',
typeLabel: '数字员工',
currentVersion: employee.currentVersion || employee.version || '-',
workingVersion: employee.version || '-',
markdownContent: '',
loading: true
}
}
function buildEmployeeDetail(asset) {
const meta = buildDigitalEmployeeDetailMeta({
...asset,
updated_at: formatDateTime(asset.updated_at)
})
const statusMeta = resolveStatusMeta(asset.status)
return {
id: asset.id,
type: 'digitalEmployees',
typeLabel: '数字员工',
rawCode: asset.code,
short: meta.name.slice(0, 2),
name: meta.name,
code: meta.code,
summary: meta.description,
owner: meta.owner,
reviewer: meta.reviewer,
category: meta.category,
scope: meta.scope,
version: asset.working_version || asset.current_version || '-',
currentVersion: asset.current_version || '-',
workingVersion: asset.working_version || asset.current_version || '-',
status: statusMeta.label,
statusValue: asset.status,
statusTone: statusMeta.tone,
configJson: asset.config_json || {},
updatedAt: formatDateTime(asset.updated_at),
markdownContent: meta.sourceMarkdown,
digitalEmployee: meta,
loading: false
}
}
function sortEmployees(items) {
return [...items].sort((left, right) =>
String(right.updatedAtRaw || '').localeCompare(String(left.updatedAtRaw || ''))
)
}
async function loadEmployees() {
loading.value = true
errorMessage.value = ''
try {
const payload = await fetchAgentAssets({ assetType: 'task' })
const items = Array.isArray(payload)
? payload.filter(isDigitalEmployeeAsset).map(buildEmployeeListItem)
: []
employees.value = sortEmployees(items)
} catch (error) {
errorMessage.value = error?.message || '数字员工数据加载失败,请稍后重试。'
toast(errorMessage.value)
} finally {
loading.value = false
}
}
async function loadEmployeeDetail(assetId, placeholder = null, options = {}) {
if (!assetId) {
return
}
selectedEmployeeId.value = assetId
selectedEmployee.value = placeholder
? buildEmployeePlaceholder(placeholder)
: selectedEmployee.value || { id: assetId, name: '数字员工', loading: true, digitalEmployee: {} }
detailLoading.value = true
detailError.value = ''
try {
const detail = await fetchAgentAssetDetail(assetId)
selectedEmployee.value = buildEmployeeDetail(detail)
} catch (error) {
detailError.value = error?.message || '数字员工详情加载失败,请稍后重试。'
if (!options.silent) {
toast(detailError.value)
}
} finally {
detailLoading.value = false
}
}
function openEmployeeDetail(employee) {
loadEmployeeDetail(employee.id, employee).catch(() => {})
}
function closeEmployeeDetail() {
closeDigitalEmployeeSchedule()
selectedEmployee.value = null
selectedEmployeeId.value = ''
detailError.value = ''
detailLoading.value = false
}
async function refreshAfterMutation(assetId) {
await loadEmployees()
if (selectedEmployee.value && selectedEmployeeId.value === assetId) {
const placeholder = employees.value.find((item) => item.id === assetId) || selectedEmployee.value
await loadEmployeeDetail(assetId, placeholder, { silent: true })
}
}
function actionBusy(assetId, action) {
return busyEmployeeId.value === assetId && actionState.value === action
}
async function toggleDigitalEmployeeRunning(employee) {
if (!employee || !canOperateDigitalEmployee.value || actionState.value) {
return
}
const assetId = employee.id
const shouldEnable = employee.statusValue !== 'active'
actionState.value = 'toggle-digital-running'
busyEmployeeId.value = assetId
try {
await (shouldEnable
? activateAgentAsset(assetId, { actor: resolveActor() })
: updateAgentAsset(assetId, { status: 'disabled' }, { actor: resolveActor() }))
await refreshAfterMutation(assetId)
toast(shouldEnable ? '已启用运行。' : '已停止运行。')
} catch (error) {
toast(error?.message || '运行状态更新失败,请稍后重试。')
} finally {
actionState.value = ''
busyEmployeeId.value = ''
}
}
async function saveDigitalEmployeeSource() {
if (!selectedEmployee.value || !canEditDigitalEmployeeSource.value || detailBusy.value) {
return
}
const markdown = normalizeText(selectedEmployee.value.markdownContent)
if (!markdown) {
toast('Skills Markdown 源文件不能为空。')
return
}
const nextVersion = incrementVersion(selectedEmployee.value.currentVersion)
actionState.value = 'save-digital-source'
busyEmployeeId.value = selectedEmployee.value.id
try {
await createAgentAssetVersion(
selectedEmployee.value.id,
{
version: nextVersion,
content: markdown,
content_type: 'markdown',
change_note: '通过数字员工页面更新 Skills Markdown 源文件。',
created_by: resolveActor()
},
{ actor: resolveActor() }
)
await refreshAfterMutation(selectedEmployee.value.id)
toast(`Skills 源文件已保存为 ${nextVersion}`)
} catch (error) {
toast(error?.message || 'Skills 源文件保存失败,请稍后重试。')
} finally {
actionState.value = ''
busyEmployeeId.value = ''
}
}
function openDigitalEmployeeSchedule(employee) {
if (!employee || !canOperateDigitalEmployee.value) {
return
}
scheduleTarget.value = employee
scheduleForm.value = createDigitalEmployeeScheduleForm(resolveDigitalEmployeeScheduleValue(employee))
scheduleEditorError.value = ''
scheduleEditorOpen.value = true
}
function closeDigitalEmployeeSchedule() {
if (scheduleEditorBusy.value) {
return
}
scheduleEditorOpen.value = false
scheduleTarget.value = null
scheduleEditorError.value = ''
}
async function saveDigitalEmployeeSchedule() {
const employee = scheduleTarget.value || selectedEmployee.value
if (!employee || !canOperateDigitalEmployee.value || actionState.value) {
return
}
let cron = ''
try {
cron = buildDigitalEmployeeScheduleCron(scheduleForm.value)
} catch (error) {
scheduleEditorError.value = error?.message || '定时计划格式不正确。'
return
}
const nextConfig = buildDigitalEmployeeScheduleConfig(employee.configJson, cron)
nextConfig.skill_category = employee.digitalEmployee?.skillCategory || employee.skillCategory || '整理'
nextConfig.skill_category_options = DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS
actionState.value = 'save-digital-schedule'
busyEmployeeId.value = employee.id
try {
await updateAgentAsset(employee.id, { config_json: nextConfig }, { actor: resolveActor() })
await refreshAfterMutation(employee.id)
scheduleEditorOpen.value = false
scheduleTarget.value = null
scheduleEditorError.value = ''
toast(cron ? `定时计划已更新为 ${formatDigitalEmployeeCron(cron)}` : '已改为手动触发。')
} catch (error) {
scheduleEditorError.value = error?.message || '定时计划保存失败,请稍后重试。'
toast(scheduleEditorError.value)
} finally {
actionState.value = ''
busyEmployeeId.value = ''
}
}
async function runDigitalEmployeeNow(employee) {
if (!employee || !canOperateDigitalEmployee.value || actionState.value) {
return
}
actionState.value = 'run-digital-now'
busyEmployeeId.value = employee.id
try {
const result = await runOrchestrator({
source: 'schedule',
task_id: employee.id,
message: employee.name,
context_json: {
manual_trigger: true,
entry: 'digital_employees'
}
})
toast(`已发起立即运行Run ID${result?.run_id || '-'}`)
} catch (error) {
toast(error?.message || '立即运行失败,请稍后重试。')
} finally {
actionState.value = ''
busyEmployeeId.value = ''
}
}
onMounted(() => {
loadEmployees().catch(() => {})
})
</script>
<style scoped src="../assets/styles/views/audit-view.css"></style>
<style scoped src="../assets/styles/views/audit-view-part2.css"></style>
<style scoped src="../assets/styles/views/digital-employees-view.css"></style>

View File

@@ -2,6 +2,7 @@
<PersonalWorkbench
:show-header="false"
:assistant-modal-open="assistantModalOpen"
:workbench-summary="workbenchSummary"
@open-assistant="emit('open-assistant', $event)"
/>
</template>
@@ -10,7 +11,8 @@
import PersonalWorkbench from '../components/business/PersonalWorkbench.vue'
defineProps({
assistantModalOpen: { type: Boolean, default: false }
assistantModalOpen: { type: Boolean, default: false },
workbenchSummary: { type: Object, default: () => ({}) }
})
const emit = defineEmits(['open-assistant'])

View File

@@ -70,11 +70,12 @@
<tr>
<th>文件名称</th>
<th>标签</th>
<th>上传时间 <i class="mdi mdi-arrow-down"></i></th>
<th>版本</th>
<th>状态</th>
<th>上传人</th>
<th>操作</th>
<th>上传时间 <i class="mdi mdi-arrow-down"></i></th>
<th>版本</th>
<th>状态</th>
<th>归纳时间</th>
<th>上传人</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@@ -96,40 +97,40 @@
</td>
<td>{{ doc.time }}</td>
<td>{{ doc.version }}</td>
<td>
<div class="state-cell">
<span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span>
<span v-if="doc.ingestTime" class="state-time">归纳时间{{ doc.ingestTime }}</span>
</div>
</td>
<td>{{ doc.owner }}</td>
<td>
<div class="row-actions" @click.stop>
<td>
<div class="state-cell">
<span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span>
</div>
</td>
<td class="ingest-time-cell">{{ doc.ingestTime || '—' }}</td>
<td>{{ doc.owner }}</td>
<td>
<div class="row-actions" @click.stop>
<button
class="more-btn"
type="button"
aria-label="下载文件"
title="归纳时间:?"
@click="handleDownload(doc)"
>
class="more-btn"
type="button"
aria-label="下载文件"
title="下载文件"
@click="handleDownload(doc)"
>
<i class="mdi mdi-download"></i>
</button>
<button
v-if="isAdmin"
class="more-btn danger"
type="button"
:disabled="deletingId === doc.id || Number(doc.stateCode || 0) === 2"
aria-label="删除文件"
title="归纳时间:?"
@click="handleDelete(doc)"
>
type="button"
:disabled="deletingId === doc.id || Number(doc.stateCode || 0) === 2"
aria-label="删除文件"
title="删除文件"
@click="handleDelete(doc)"
>
<i class="mdi mdi-delete-outline"></i>
</button>
</div>
</td>
</tr>
<tr v-if="loading && !visibleDocuments.length">
<td colspan="7" class="empty-row table-loading-row">
<td colspan="8" class="empty-row table-loading-row">
<TableLoadingState
title="知识库文件同步中"
message="正在加载当前文件夹的知识库文件"
@@ -138,7 +139,7 @@
</td>
</tr>
<tr v-else-if="!visibleDocuments.length">
<td colspan="7" class="empty-row">
<td colspan="8" class="empty-row">
当前文件夹暂无文件
</td>
</tr>

View File

@@ -154,7 +154,6 @@ export default {
const assetBuckets = ref({
financialRules: [],
riskRules: [],
skills: [],
mcp: []
})
@@ -173,7 +172,7 @@ export default {
const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
const showOnlineColumn = computed(() => false)
const showEnabledColumn = computed(() => false)
const showEnabledColumn = computed(() => activeMeta.value.showEnabledColumn === true)
const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
const selectedSkillUsesSpreadsheet = computed(
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule)
@@ -241,7 +240,7 @@ export default {
() => selectedSkillUsesJsonRisk.value && canManageSelected.value
)
const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
const canEditMarkdown = computed(() => canEditSelected.value && selectedSkillIsRule.value)
const canEditMarkdown = computed(() => selectedSkillIsRule.value && canEditSelected.value)
const isDisplayingWorkingVersion = computed(
() => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
)
@@ -1401,7 +1400,7 @@ export default {
version: nextVersion,
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
content_type: 'markdown',
change_note: '通过任务规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
change_note: '通过规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
created_by: resolveActor()
},
{ actor: resolveActor() }
@@ -1449,7 +1448,7 @@ export default {
version: nextVersion,
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
content_type: 'markdown',
change_note: '通过任务规则中心保存运行时 JSON 配置。',
change_note: '通过规则中心保存运行时 JSON 配置。',
created_by: resolveActor()
},
{ actor: resolveActor() }

View File

@@ -1,4 +1,5 @@
const DIGITAL_EMPLOYEE_AGENT = 'hermes'
export const DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS = ['积累', '升级', '整理', '评估']
const TASK_TYPE_LABELS = {
daily_risk_scan: '每日风险巡检',
@@ -7,11 +8,25 @@ const TASK_TYPE_LABELS = {
weekly_expense_report: '周度费用洞察',
rule_review_digest: '规则待审摘要',
knowledge_index_sync: '知识库归集',
llm_wiki_rule_formation: '知识库归集',
x_financial_callback: '任务回调上报'
}
const TASK_TYPE_SKILL_CATEGORIES = {
daily_risk_scan: '评估',
global_risk_scan: '评估',
weekly_ar_summary: '整理',
weekly_expense_report: '整理',
rule_review_digest: '升级',
knowledge_index_sync: '积累',
llm_wiki_rule_formation: '积累',
x_financial_callback: '升级'
}
const CONTENT_LABELS = {
task_type: '技能类型',
task_type: '任务类型',
skill_category: '技能类型',
skill_category_options: '技能类型范围',
schedule: '执行计划',
cron: '调度表达式',
folder: '归集范围',
@@ -45,6 +60,14 @@ export function sanitizeDigitalEmployeeText(value, fallback = '') {
return text || fallback
}
export function sanitizeDigitalEmployeeSource(value, fallback = '') {
const text = normalizeDigitalEmployeeText(value)
.replace(/hermes/gi, '数字员工')
.replace(/赫尔墨斯/g, '数字员工')
.trim()
return text || fallback
}
export function sanitizeDigitalEmployeeName(value, fallback = '数字员工技能') {
const text = sanitizeDigitalEmployeeText(value, fallback)
.replace(/^数字员工[\s·:-]*/i, '')
@@ -80,6 +103,22 @@ export function resolveDigitalEmployeeTaskType(source = {}, content = {}) {
return raw.replace(/[-.]/g, '_')
}
export function resolveDigitalEmployeeSkillCategory(source = {}, content = {}) {
const config = source.config_json || source.configJson || {}
const taskType = resolveDigitalEmployeeTaskType(source, content)
const explicitCategory =
normalizeDigitalEmployeeText(config.skill_category) ||
normalizeDigitalEmployeeText(config.skillCategory) ||
normalizeDigitalEmployeeText(content.skill_category) ||
normalizeDigitalEmployeeText(content.skillCategory)
if (DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS.includes(explicitCategory)) {
return explicitCategory
}
return TASK_TYPE_SKILL_CATEGORIES[taskType] || '整理'
}
export function isDigitalEmployeeAsset(source = {}) {
const config = source.config_json || source.configJson || {}
const haystack = [
@@ -145,10 +184,10 @@ export function formatDigitalEmployeeCron(value) {
export function resolveDigitalEmployeeSchedule(source = {}, content = {}) {
const config = source.config_json || source.configJson || {}
const raw =
normalizeDigitalEmployeeText(content.schedule) ||
normalizeDigitalEmployeeText(config.cron) ||
normalizeDigitalEmployeeText(config.schedule) ||
normalizeDigitalEmployeeText(config.cron_expression)
normalizeDigitalEmployeeText(config.cron_expression) ||
normalizeDigitalEmployeeText(content.schedule)
return {
value: raw,
label: formatDigitalEmployeeCron(raw)
@@ -205,9 +244,85 @@ export function buildDigitalEmployeeContentPreview(content = {}) {
return sanitizeDigitalEmployeeText(JSON.stringify(visiblePayload, null, 2))
}
function resolveDigitalEmployeeMarkdownFromContent(content = {}, config = {}) {
const candidates = [
content.skill_markdown,
content.skills_markdown,
content.source_markdown,
content.markdown,
content.skill_source,
config.skill_markdown,
config.skills_markdown,
config.source_markdown,
config.skill_source
]
return candidates.find((item) => normalizeDigitalEmployeeText(item)) || ''
}
function buildDefaultDigitalEmployeeSource(source = {}, listMeta = {}, schedule = {}) {
const name = listMeta.name || '数字员工技能'
const description =
listMeta.summary ||
sanitizeDigitalEmployeeText(source.description, '该技能用于后台自动执行指定任务。')
return [
'---',
`name: ${listMeta.code || 'digital.skill'}`,
`description: ${description}`,
'---',
'',
`# ${name}`,
'',
'## 功能说明',
'',
description,
'',
'## 执行方式',
'',
`- 技能类型:${listMeta.skillCategory || '整理'}`,
`- 可选类型:${DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS.join('、')}`,
`- 执行计划:${schedule.label || '手动触发'}`,
`- 触发方式:${listMeta.executionMode || '手动触发'}`,
'',
'## 操作要求',
'',
'- 按任务参数读取业务数据。',
'- 运行完成后写回业务结果或运行日志。'
].join('\n')
}
export function buildDigitalEmployeeSourceMarkdown(source = {}, content = {}, listMeta = {}) {
const config = source.config_json || source.configJson || {}
if (
normalizeDigitalEmployeeText(source.current_version_content_type) === 'markdown' &&
typeof source.current_version_content === 'string'
) {
return sanitizeDigitalEmployeeSource(source.current_version_content)
}
const schedule = resolveDigitalEmployeeSchedule(source, content)
const sourceMarkdown = resolveDigitalEmployeeMarkdownFromContent(content, config)
return sanitizeDigitalEmployeeSource(
sourceMarkdown,
buildDefaultDigitalEmployeeSource(source, listMeta, schedule)
)
}
function buildDigitalEmployeeBasicRows(source = {}, listMeta = {}, schedule = {}) {
return [
{ label: '技能编号', value: listMeta.code },
{ label: '技能类型', value: listMeta.skillCategory },
{ label: '维护人', value: listMeta.owner },
{ label: '执行计划', value: schedule.label },
{ label: '当前版本', value: source.working_version || source.current_version || '-' },
{ label: '最近更新', value: source.updated_at || '-' }
]
}
export function buildDigitalEmployeeListMeta(source = {}) {
const content = parseDigitalEmployeeContent(source.current_version_content)
const taskType = resolveDigitalEmployeeTaskType(source, content)
const skillCategory = resolveDigitalEmployeeSkillCategory(source, content)
const schedule = resolveDigitalEmployeeSchedule(source, content)
const enabled = resolveDigitalEmployeeEnabled(source)
const fallbackName = TASK_TYPE_LABELS[taskType] || '数字员工技能'
@@ -217,6 +332,8 @@ export function buildDigitalEmployeeListMeta(source = {}) {
code: resolveDigitalEmployeeDisplayCode(source, content),
summary: sanitizeDigitalEmployeeText(source.description, '面向后台自动执行的数字员工技能。'),
category: '数字员工',
skillCategory,
skillCategoryOptions: DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
owner: sanitizeDigitalEmployeeText(source.owner, '平台运营'),
reviewer: sanitizeDigitalEmployeeText(source.reviewer, '系统'),
scope: schedule.label,
@@ -237,6 +354,7 @@ export function buildDigitalEmployeeDetailMeta(source = {}) {
})
const schedule = resolveDigitalEmployeeSchedule(source, content)
const contentRows = buildDigitalEmployeeContentRows(content)
const sourceMarkdown = buildDigitalEmployeeSourceMarkdown(source, content, listMeta)
return {
...listMeta,
@@ -245,13 +363,16 @@ export function buildDigitalEmployeeDetailMeta(source = {}) {
source.description,
'该技能由后台数字员工按计划执行,并把结果沉淀到对应业务资产或运行日志中。'
),
sourceMarkdown,
basicRows: buildDigitalEmployeeBasicRows(source, listMeta, schedule),
contentRows,
contentPreview: buildDigitalEmployeeContentPreview(content),
scheduleRows: [
{ label: '执行计划', value: schedule.label },
{ label: '调度表达式', value: schedule.value || '手动触发' },
{ label: '启动状态', value: listMeta.enabledLabel, tone: listMeta.enabledTone },
{ label: '执行方式', value: listMeta.executionMode }
{ label: '执行方式', value: listMeta.executionMode },
{ label: '技能类型', value: listMeta.skillCategory }
],
overviewRows: [
{ label: '能力编号', value: listMeta.code },

View File

@@ -22,24 +22,6 @@ export const TYPE_META = {
typeLabel: '规则',
tableColumns: RULE_TABLE_COLUMNS
},
skills: {
assetType: 'skill',
label: '技能',
typeLabel: '技能',
createButtonLabel: '技能已接入',
hintText: '技能页签已接到真实资产 API可查看输入、输出、依赖和场景信息。',
searchPlaceholder: '搜索技能名称、编码或负责人',
showMetricColumn: false,
tableColumns: {
name: '技能名称',
category: '业务域',
owner: '负责人',
scope: '适用场景',
runtime: '输入摘要',
version: '当前版本',
metric: ''
}
},
mcp: {
assetType: 'mcp',
label: 'MCP',
@@ -87,41 +69,10 @@ export const TAB_META = {
showStatusColumn: true,
badgeTone: 'rose'
},
skills: {
...TYPE_META.skills,
typeKey: 'skills',
badgeTone: 'blue'
},
mcp: {
...TYPE_META.mcp,
typeKey: 'mcp',
badgeTone: 'amber'
},
digitalWorkers: {
assetType: 'task',
typeKey: 'digitalWorkers',
label: '数字员工',
typeLabel: '数字员工',
createButtonLabel: '数字员工已接入',
hintText: '归集后台自动执行的数字员工技能,可查看技能内容、执行计划、启动状态和最近版本。',
searchPlaceholder: '搜索数字员工技能、编号、执行计划或维护人',
showMetricColumn: true,
showRuntimeColumn: true,
showVersionColumn: true,
showStatusColumn: true,
showEnabledColumn: true,
tableColumns: {
name: '技能名称',
category: '归集标签',
owner: '维护归口',
scope: '执行计划',
runtime: '触发方式',
version: '当前版本',
status: '资产状态',
metric: '运行方式',
updatedAt: '最近更新'
},
badgeTone: 'violet'
}
}
@@ -199,24 +150,6 @@ export const DETAIL_TITLES = {
publishTitle: '上线控制',
publishDesc: '正式上线会调用后端激活接口,审核未通过时会被拦截。'
},
skills: {
configTitle: '技能配置',
configDesc: '展示技能编码、输入摘要、版本和业务域。',
detailTitle: '技能结构',
detailDesc: '按输入、输出和依赖组织技能定义。',
outputTitle: '输出契约',
outputDesc: '技能详情重点展示输入参数、输出参数和依赖能力。',
ruleListTitle: '输出要求',
checkListTitle: '当前快照',
triggerTitle: '适用场景',
triggerDesc: '当前技能注册到的场景标签',
toolTitle: '依赖能力',
toolDesc: '技能当前依赖的数据库或其他能力',
historyTitle: '版本历史',
historyDesc: '最近版本记录',
publishTitle: '发布状态',
publishDesc: '技能当前状态由资产中心统一管理。'
},
mcp: {
configTitle: 'MCP 连接配置',
configDesc: '展示服务地址、超时和调用方式。',
@@ -234,24 +167,6 @@ export const DETAIL_TITLES = {
historyDesc: '最近版本记录',
publishTitle: '服务状态',
publishDesc: 'MCP 资产已接入规则中心,但真实外部调用仍以后续链路集成为准。'
},
digitalWorkers: {
configTitle: '技能档案',
configDesc: '展示数字员工技能的编号、归口、执行计划和启停状态。',
detailTitle: '技能内容',
detailDesc: '展示当前版本记录的任务类型、调度范围和执行参数。',
outputTitle: '执行安排',
outputDesc: '展示什么时候执行、是否启动,以及当前运行方式。',
ruleListTitle: '技能参数',
checkListTitle: '启动状态',
triggerTitle: '执行计划',
triggerDesc: '当前技能的计划执行时间或触发方式。',
toolTitle: '运行归口',
toolDesc: '数字员工技能由后台调度执行,运行结果进入对应日志或业务资产。',
historyTitle: '版本记录',
historyDesc: '最近的技能配置快照。',
publishTitle: '启动状态',
publishDesc: '数字员工技能由资产状态和调度配置共同决定是否启动。'
}
}

View File

@@ -34,13 +34,6 @@ import {
resolveRiskRuleSeverity,
resolveRiskRuleSeverityLabel
} from './auditViewRiskRuleModel.js'
import {
buildDigitalEmployeeContentRows,
buildDigitalEmployeeDetailMeta,
buildDigitalEmployeeListMeta,
isDigitalEmployeeAsset,
sanitizeDigitalEmployeeText
} from './auditViewDigitalEmployeeModel.js'
const EXPENSE_TYPE_SCENARIO_LABELS = {
travel: '差旅费',
@@ -342,9 +335,6 @@ export function resolveTabId(source, typeKey) {
if (typeKey === 'rules') {
return resolveRuleTabId(source)
}
if (typeKey === 'digitalWorkers') {
return isDigitalEmployeeAsset(source) ? 'digitalWorkers' : ''
}
return typeKey
}
@@ -899,15 +889,9 @@ export function resolveTypeKey(assetType) {
if (assetType === 'rule') {
return 'rules'
}
if (assetType === 'skill') {
return 'skills'
}
if (assetType === 'mcp') {
return 'mcp'
}
if (assetType === 'task') {
return 'digitalWorkers'
}
return ''
}
@@ -965,15 +949,9 @@ export 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) || '未配置地址'
}
if (typeKey === 'digitalWorkers') {
return buildDigitalEmployeeListMeta(asset).executionMode
}
return ''
}
@@ -981,15 +959,9 @@ export function buildRowMetric(asset, typeKey) {
if (typeKey === 'rules') {
return normalizeText(asset.modified_by) || '未记录'
}
if (typeKey === 'skills') {
return '进入详情查看输出'
}
if (typeKey === 'mcp') {
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
}
if (typeKey === 'digitalWorkers') {
return buildDigitalEmployeeListMeta(asset).executionMode
}
return ''
}
@@ -1061,19 +1033,16 @@ export function buildListItem(asset) {
? resolveRiskRuleScoreLabel(asset.config_json, asset.config_json) || resolveRiskRuleSeverityLabel(asset.config_json)
: resolveRiskRuleSeverityLabel(asset.config_json)
: ''
const digitalMeta = typeKey === 'digitalWorkers' ? buildDigitalEmployeeListMeta(asset) : null
const displayName = digitalMeta?.name || asset.name
const displayCode = digitalMeta?.code || asset.code
const displaySummary = digitalMeta?.summary || listSubtitle
const displayOwner = digitalMeta?.owner || (isRiskRule ? creator : asset.owner)
const displayReviewer = digitalMeta?.reviewer || reviewer
const displayCategory = digitalMeta?.category || resolveDomainLabel(asset.domain)
const displayScope =
digitalMeta?.scope ||
(typeKey === 'rules' ? ruleScenarioCategory || '閫氱敤' : formatScenarioList(asset.scenario_json))
const displayEnabledValue = digitalMeta ? digitalMeta.enabled : isEnabledValue
const displayEnabledLabel = digitalMeta?.enabledLabel || (isEnabledValue ? '鏄? : '?)
const displayEnabledTone = digitalMeta?.enabledTone || (isEnabledValue ? 'success' : 'disabled')
const displayName = asset.name
const displayCode = asset.code
const displaySummary = listSubtitle
const displayOwner = isRiskRule ? creator : asset.owner
const displayReviewer = reviewer
const displayCategory = resolveDomainLabel(asset.domain)
const displayScope = typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json)
const displayEnabledValue = isEnabledValue
const displayEnabledLabel = isEnabledValue ? '是' : '否'
const displayEnabledTone = isEnabledValue ? 'success' : 'disabled'
return {
id: asset.id,
@@ -1093,7 +1062,6 @@ export function buildListItem(asset) {
category: displayCategory,
owner: displayOwner,
reviewer: displayReviewer,
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json),
scope: displayScope,
riskCategory: ruleScenarioCategory,
scenarioList: ruleScenarioList,
@@ -1117,9 +1085,6 @@ export function buildListItem(asset) {
isOnlineValue,
isOnlineLabel: onlineMeta.label,
isOnlineTone: onlineMeta.tone,
isEnabledValue,
isEnabledLabel: isEnabledValue ? '是' : '否',
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
isEnabledValue: displayEnabledValue,
isEnabledLabel: displayEnabledLabel,
isEnabledTone: displayEnabledTone,
@@ -1163,22 +1128,6 @@ export function buildRuleFields(detail) {
]
}
export 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('、') : '未配置'
}
]
}
export function buildMcpFields(detail, latestCall) {
const content = detail.current_version_content || {}
return [
@@ -1196,9 +1145,6 @@ export function buildFields(detail, typeKey, latestCall) {
if (typeKey === 'rules') {
return buildRuleFields(detail)
}
if (typeKey === 'skills') {
return buildSkillFields(detail)
}
if (typeKey === 'mcp') {
return buildMcpFields(detail, latestCall)
}
@@ -1208,29 +1154,6 @@ export function buildFields(detail, typeKey, latestCall) {
export function buildPromptSections(detail, typeKey) {
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 [
{
@@ -1274,14 +1197,6 @@ export function buildOutputRules(detail, typeKey) {
]
}
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) || '未配置'}`,
@@ -1312,24 +1227,6 @@ export function buildTests(detail, typeKey, latestCall) {
]
}
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 [
{
@@ -1356,15 +1253,6 @@ export function buildTests(detail, typeKey, latestCall) {
export function buildTools(detail, typeKey, 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 [
{
@@ -1454,40 +1342,32 @@ export function buildDetailViewModel(detail, runs) {
const initialRiskRuleScore = resolveRiskRuleScore(configJson, configJson)
const initialRiskRuleScoreLevel = resolveRiskRuleScoreLevel(configJson, configJson)
const initialRiskRuleSeverity = initialRiskRuleScoreLevel || resolveRiskRuleSeverity(configJson)
const digitalMeta = typeKey === 'digitalWorkers'
? buildDigitalEmployeeDetailMeta({
...detail,
updated_at: formatDateTime(detail.updated_at)
})
: null
const detailName = digitalMeta?.name || detail.name
const detailCode = digitalMeta?.code || detail.code
const detailSummary = digitalMeta?.description ||
(usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description)
const detailOwner = digitalMeta?.owner || detail.owner
const detailReviewer = digitalMeta?.reviewer || detail.reviewer || detail.latest_review?.reviewer || '寰呭垎閰?
const detailCategory = digitalMeta?.category || resolveDomainLabel(detail.domain)
const detailScope =
digitalMeta?.scope ||
(typeKey === 'rules' ? ruleScenarioCategory || '閫氱敤' : formatScenarioList(detail.scenario_json))
const detailEnabledValue = digitalMeta ? digitalMeta.enabled : isEnabledValue
const detailEnabledLabel = digitalMeta?.enabledLabel || (isEnabledValue ? '? : '鍚?)
const detailEnabledTone = digitalMeta?.enabledTone || (isEnabledValue ? 'success' : 'disabled')
const detailName = detail.name
const detailCode = detail.code
const detailSummary = usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description
const detailOwner = detail.owner
const detailReviewer = detail.reviewer || detail.latest_review?.reviewer || '待分配'
const detailCategory = resolveDomainLabel(detail.domain)
const detailScope = typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(detail.scenario_json)
const detailEnabledValue = isEnabledValue
const detailEnabledLabel = isEnabledValue ? '是' : '否'
const detailEnabledTone = isEnabledValue ? 'success' : 'disabled'
return {
id: detail.id,
tabId,
type: typeKey,
typeLabel: tabMeta.typeLabel,
short: makeShort(detail.name),
name: detail.name,
code: detail.code,
summary: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description,
listSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : normalizeText(detail.description),
owner: detail.owner,
reviewer: detail.reviewer || detail.latest_review?.reviewer || '待分配',
category: resolveDomainLabel(detail.domain),
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(detail.scenario_json),
short: makeShort(detailName),
name: detailName,
code: detailCode,
rawCode: detail.code,
summary: detailSummary,
listSubtitle: normalizeText(detailSummary),
owner: detailOwner,
reviewer: detailReviewer,
category: detailCategory,
scope: detailScope,
businessStageValue: businessStage.value,
businessStageLabel: businessStage.label,
version: detail.working_version || detail.current_version || '-',
@@ -1524,9 +1404,9 @@ export function buildDetailViewModel(detail, runs) {
isOnlineValue: onlineMeta.online,
isOnlineLabel: onlineMeta.label,
isOnlineTone: onlineMeta.tone,
isEnabledValue,
isEnabledLabel: isEnabledValue ? '' : '',
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
isEnabledValue: detailEnabledValue,
isEnabledLabel: detailEnabledLabel,
isEnabledTone: detailEnabledTone,
publisher:
detail.status === 'active'
? normalizeText(detail.published_by) ||

View File

@@ -16,12 +16,12 @@ export function incrementVersion(version) {
export function buildReviewNote(status) {
if (status === 'approved') {
return '通过任务规则中心审核。'
return '通过规则中心审核。'
}
if (status === 'rejected') {
return '在任务规则中心驳回当前版本。'
return '在规则中心驳回当前版本。'
}
return '提交任务规则中心待审核。'
return '提交规则中心待审核。'
}
export function buildRuleConfigPayload(asset, runtimeRule) {

View File

@@ -0,0 +1,122 @@
const DEFAULT_SCHEDULE_TIME = '00:00'
const CRON_TOKEN_PATTERN = /^[\d*/,?\-]+$/
export const DIGITAL_EMPLOYEE_SCHEDULE_MODES = [
{ value: 'manual', label: '手动触发' },
{ value: 'daily', label: '每天执行' },
{ value: 'weekly', label: '每周执行' },
{ value: 'custom', label: 'Cron 表达式' }
]
export const DIGITAL_EMPLOYEE_WEEKDAY_OPTIONS = [
{ value: '1', label: '周一' },
{ value: '2', label: '周二' },
{ value: '3', label: '周三' },
{ value: '4', label: '周四' },
{ value: '5', label: '周五' },
{ value: '6', label: '周六' },
{ value: '0', label: '周日' }
]
function normalizeText(value) {
return String(value ?? '').trim()
}
function normalizeTime(value) {
const raw = normalizeText(value)
return /^\d{2}:\d{2}$/.test(raw) ? raw : DEFAULT_SCHEDULE_TIME
}
function toTime(hour, minute) {
const hourNumber = Number(hour)
const minuteNumber = Number(minute)
if (!Number.isInteger(hourNumber) || hourNumber < 0 || hourNumber > 23) {
return DEFAULT_SCHEDULE_TIME
}
if (!Number.isInteger(minuteNumber) || minuteNumber < 0 || minuteNumber > 59) {
return DEFAULT_SCHEDULE_TIME
}
return `${String(hourNumber).padStart(2, '0')}:${String(minuteNumber).padStart(2, '0')}`
}
export function createDigitalEmployeeScheduleForm(cron = '') {
const normalizedCron = normalizeText(cron)
if (!normalizedCron) {
return { mode: 'manual', time: DEFAULT_SCHEDULE_TIME, weekday: '1', cron: '' }
}
const parts = normalizedCron.split(/\s+/)
if (parts.length !== 5) {
return { mode: 'custom', time: DEFAULT_SCHEDULE_TIME, weekday: '1', cron: normalizedCron }
}
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts
const time = toTime(hour, minute)
if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
return { mode: 'daily', time, weekday: '1', cron: normalizedCron }
}
if (dayOfMonth === '*' && month === '*' && dayOfWeek !== '*') {
return { mode: 'weekly', time, weekday: dayOfWeek || '1', cron: normalizedCron }
}
return { mode: 'custom', time, weekday: '1', cron: normalizedCron }
}
export function resolveDigitalEmployeeScheduleValue(employee = {}) {
const config = employee.configJson || {}
return (
normalizeText(employee.digitalEmployee?.scheduleValue) ||
normalizeText(config.cron) ||
normalizeText(config.schedule) ||
normalizeText(config.cron_expression)
)
}
export function buildDigitalEmployeeScheduleCron(form = {}) {
const mode = normalizeText(form.mode) || 'manual'
if (mode === 'manual') {
return ''
}
if (mode === 'custom') {
const cron = normalizeText(form.cron)
const parts = cron.split(/\s+/).filter(Boolean)
if (parts.length !== 5 || !parts.every((part) => CRON_TOKEN_PATTERN.test(part))) {
throw new Error('请输入 5 段 Cron 表达式。')
}
return parts.join(' ')
}
const [hour, minute] = normalizeTime(form.time).split(':')
if (mode === 'daily') {
return `${Number(minute)} ${Number(hour)} * * *`
}
if (mode === 'weekly') {
const weekday = normalizeText(form.weekday) || '1'
return `${Number(minute)} ${Number(hour)} * * ${weekday}`
}
throw new Error('请选择有效的执行方式。')
}
export function buildDigitalEmployeeScheduleConfig(config = {}, cron = '') {
const nextConfig = config && typeof config === 'object' && !Array.isArray(config)
? { ...config }
: {}
const normalizedCron = normalizeText(cron)
if (normalizedCron) {
nextConfig.cron = normalizedCron
nextConfig.schedule = normalizedCron
nextConfig.cron_expression = normalizedCron
return nextConfig
}
delete nextConfig.cron
delete nextConfig.schedule
delete nextConfig.cron_expression
return nextConfig
}