774 lines
26 KiB
Vue
774 lines
26 KiB
Vue
<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 json-risk-skill-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 digital-employees-list"
|
||
:class="{ 'panel': !workRecordDetailOpen }"
|
||
>
|
||
<nav v-if="!workRecordDetailOpen" class="status-tabs" aria-label="数字员工页签">
|
||
<button
|
||
type="button"
|
||
:class="{ active: activeSection === 'skills' }"
|
||
@click="activeSection = 'skills'"
|
||
>
|
||
数字员工
|
||
</button>
|
||
<button
|
||
type="button"
|
||
:class="{ active: activeSection === 'workRecords' }"
|
||
@click="activeSection = 'workRecords'"
|
||
>
|
||
工作记录
|
||
</button>
|
||
</nav>
|
||
|
||
<template v-if="activeSection === 'skills'">
|
||
<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>
|
||
</template>
|
||
|
||
<DigitalEmployeeWorkRecords
|
||
v-else
|
||
class="digital-work-records-section"
|
||
@summary-change="emit('summary-change', $event)"
|
||
@detail-open-change="workRecordDetailOpen = $event"
|
||
/>
|
||
</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, watch } from 'vue'
|
||
|
||
import AuditDigitalEmployeeDetail from '../components/audit/AuditDigitalEmployeeDetail.vue'
|
||
import AuditPickerFilter from '../components/audit/AuditPickerFilter.vue'
|
||
import DigitalEmployeeScheduleDialog from '../components/audit/DigitalEmployeeScheduleDialog.vue'
|
||
import DigitalEmployeeWorkRecords from '../components/audit/DigitalEmployeeWorkRecords.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 emit = defineEmits(['summary-change', 'detail-open-change'])
|
||
|
||
const employees = ref([])
|
||
const selectedEmployee = ref(null)
|
||
const selectedEmployeeId = ref('')
|
||
const activeSection = ref('skills')
|
||
const workRecordDetailOpen = ref(false)
|
||
const isDetailOpen = computed(() => Boolean(selectedEmployee.value) || (activeSection.value === 'workRecords' && workRecordDetailOpen.value))
|
||
|
||
watch(isDetailOpen, (newVal) => {
|
||
emit('detail-open-change', newVal)
|
||
}, { immediate: true })
|
||
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>
|