| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- <template>
- <v-container fluid class="agent-page pa-6">
- <div class="page-head mb-5">
- <div>
- <h1 class="text-h4 font-weight-bold mb-2">Agent 治理</h1>
- <p class="text-body-1 text--secondary mb-0">
- 统一管理平台内部 Agent 的机器身份、最小授权、短期凭证和决策证据。
- </p>
- </div>
- <div class="head-actions">
- <v-btn outlined color="primary" :loading="loading" @click="loadAll">
- <v-icon left>mdi-refresh</v-icon>刷新
- </v-btn>
- <v-btn v-if="canManage" color="primary" depressed @click="registerDialog = true">
- <v-icon left>mdi-robot-outline</v-icon>登记 Agent
- </v-btn>
- </div>
- </div>
- <v-alert outlined color="deep-orange" icon="mdi-shield-alert-outline" class="mb-6">
- <strong>执行边界:</strong>高风险和关键风险操作即使通过双人审批,也只转为人工执行;
- 平台不会自动执行。凭证最长 15 分钟、仅签发时显示一次,数据库只保存摘要。
- </v-alert>
- <v-row class="mb-4">
- <v-col v-for="item in metrics" :key="item.label" cols="6" md="3">
- <div class="metric-block">
- <div class="text-caption text--secondary">{{ item.label }}</div>
- <div class="metric-value">{{ item.value }}</div>
- </div>
- </v-col>
- </v-row>
- <v-card outlined class="mb-6">
- <v-card-title class="d-flex justify-space-between align-center px-5">
- <span>身份与授权台账</span>
- <v-chip small outlined color="primary">版本化登记</v-chip>
- </v-card-title>
- <v-data-table :headers="agentHeaders" :items="agents" :loading="loading" :items-per-page="10">
- <template v-slot:[`item.identity`]="{ item }">
- <div class="font-weight-medium">{{ item.name }}</div>
- <div class="caption mono text--secondary">{{ item.code }}</div>
- </template>
- <template v-slot:[`item.autonomy_level`]="{ item }">
- <v-chip small outlined :color="autonomy(item.autonomy_level).color">
- {{ autonomy(item.autonomy_level).label }}
- </v-chip>
- </template>
- <template v-slot:[`item.scope`]="{ item }">
- <div>{{ (item.environments || []).join(' / ') }}</div>
- <div class="caption text--secondary">{{ (item.business_domain_uids || []).length }} 个业务域</div>
- </template>
- <template v-slot:[`item.status`]="{ item }">
- <v-chip x-small dark :color="statusColor(item.status)">{{ item.status }}</v-chip>
- </template>
- <template v-slot:[`item.actions`]="{ item }">
- <v-btn text x-small color="primary" @click="openDetail(item)">授权</v-btn>
- <v-btn v-if="canOperate && item.status === 'active'" text x-small color="primary" @click="issueCredential(item)">凭证</v-btn>
- <v-btn v-if="canOperate && item.status === 'draft'" text x-small color="success" @click="transition(item, 'activate')">启用</v-btn>
- <v-btn v-if="canOperate && item.status === 'active'" text x-small color="warning" @click="transition(item, 'suspend')">暂停</v-btn>
- </template>
- <template v-slot:no-data>
- <div class="empty-state py-10">
- <v-icon size="38" color="grey lighten-1">mdi-robot-off-outline</v-icon>
- <div class="mt-3">尚未登记受治理的 Agent</div>
- </div>
- </template>
- </v-data-table>
- </v-card>
- <v-card outlined>
- <v-card-title class="px-5">最近策略判定</v-card-title>
- <v-data-table :headers="actionHeaders" :items="actions" :loading="loading" :items-per-page="10">
- <template v-slot:[`item.tool`]="{ item }">
- <div class="mono">{{ item.tool_name }}</div>
- <div class="caption text--secondary">{{ item.interface_type }} · {{ item.action }}</div>
- </template>
- <template v-slot:[`item.decision`]="{ item }">
- <v-chip x-small outlined :color="decisionColor(item.decision)">{{ item.decision }}</v-chip>
- <div class="caption mt-1">{{ item.reason_code }}</div>
- </template>
- <template v-slot:[`item.safety`]="{ item }">
- <span>{{ item.risk_level }}</span>
- <div class="caption text--secondary">
- {{ item.automatic_execution_allowed ? '仅低风险自动' : '不自动执行' }}
- </div>
- </template>
- <template v-slot:[`item.actions`]="{ item }">
- <v-btn text x-small color="primary" @click="showReplay(item)">回放证据</v-btn>
- <v-btn v-if="canManage && item.decision === 'pending_approval'" text x-small color="primary" @click="reconcile(item)">同步审批</v-btn>
- </template>
- </v-data-table>
- </v-card>
- <v-dialog v-model="registerDialog" max-width="680">
- <v-card>
- <v-card-title>登记受治理 Agent</v-card-title>
- <v-card-text>
- <v-text-field v-model.trim="form.code" label="Agent 编码" outlined dense />
- <v-text-field v-model.trim="form.name" label="名称" outlined dense />
- <v-textarea v-model.trim="form.purpose" label="用途说明" outlined dense rows="2" />
- <v-text-field v-model.trim="form.owner_uid" label="责任人 UID" outlined dense />
- <v-text-field v-model.trim="form.domain_uid" label="业务域 UID" outlined dense />
- <v-select v-model="form.autonomy_level" :items="autonomyOptions" label="自主等级" outlined dense />
- <v-select v-model="form.environment" :items="environmentOptions" label="环境" outlined dense />
- <div class="caption text--secondary">建议型 Agent 强制携带证据引用;首批界面不提供自动执行登记。</div>
- </v-card-text>
- <v-card-actions>
- <v-spacer />
- <v-btn text @click="registerDialog = false">取消</v-btn>
- <v-btn color="primary" depressed :loading="saving" @click="saveAgent">保存</v-btn>
- </v-card-actions>
- </v-card>
- </v-dialog>
- <v-dialog v-model="grantDialog" max-width="720">
- <v-card>
- <v-card-title>工具授权 · {{ selectedAgent && selectedAgent.name }}</v-card-title>
- <v-card-text>
- <v-alert dense outlined type="info">授权严格绑定接口、工具、动作、业务域和环境。</v-alert>
- <v-data-table :headers="grantHeaders" :items="selectedGrants" dense hide-default-footer />
- <template v-if="canOperate">
- <v-divider class="my-4" />
- <v-row dense>
- <v-col cols="12" md="4"><v-select v-model="grant.interface_type" :items="['api', 'mcp']" label="接口" outlined dense /></v-col>
- <v-col cols="12" md="8"><v-text-field v-model.trim="grant.tool_name" label="工具名" outlined dense /></v-col>
- <v-col cols="12" md="4"><v-select v-model="grant.action" :items="allowedActions" label="动作" outlined dense /></v-col>
- <v-col cols="12" md="4"><v-select v-model="grant.risk_level" :items="['low', 'medium', 'high', 'critical']" label="风险" outlined dense /></v-col>
- <v-col cols="12" md="4"><v-switch v-model="grant.requires_approval" label="需要审批" inset /></v-col>
- </v-row>
- </template>
- </v-card-text>
- <v-card-actions>
- <v-spacer />
- <v-btn text @click="grantDialog = false">关闭</v-btn>
- <v-btn v-if="canOperate" color="primary" depressed :loading="saving" @click="saveGrant">新增授权</v-btn>
- </v-card-actions>
- </v-card>
- </v-dialog>
- <v-dialog v-model="credentialDialog" max-width="720" persistent>
- <v-card>
- <v-card-title>一次性机器凭证</v-card-title>
- <v-card-text>
- <v-alert type="warning" outlined>关闭后无法再次查看。请使用企业密钥系统安全保存,禁止写入日志或源码。</v-alert>
- <v-textarea :value="issuedCredential.token" readonly outlined rows="5" class="mono" label="X-Agent-Credential" />
- <div class="caption">失效时间:{{ formatTime(issuedCredential.expires_at) }}</div>
- </v-card-text>
- <v-card-actions><v-spacer /><v-btn color="primary" depressed @click="closeCredential">我已安全保存</v-btn></v-card-actions>
- </v-card>
- </v-dialog>
- <v-dialog v-model="replayDialog" max-width="800">
- <v-card>
- <v-card-title>决策证据回放</v-card-title>
- <v-card-text><pre class="replay-box">{{ replayText }}</pre></v-card-text>
- <v-card-actions><v-spacer /><v-btn text @click="replayDialog = false">关闭</v-btn></v-card-actions>
- </v-card>
- </v-dialog>
- </v-container>
- </template>
- <script>
- import {
- createAgentToolGrant,
- getAgentGovernanceDashboard,
- getGovernedAgent,
- issueAgentCredential,
- listAgentActions,
- listGovernedAgents,
- reconcileAgentAction,
- registerGovernedAgent,
- replayAgentAction,
- transitionGovernedAgent
- } from '@/api/agentGovernance'
- const unwrap = response => response && response.data && response.data.data !== undefined ? response.data.data : (response && response.data) || response
- export default {
- name: 'AgentGovernance',
- data: () => ({
- loading: false,
- saving: false,
- registerDialog: false,
- grantDialog: false,
- credentialDialog: false,
- replayDialog: false,
- dashboard: {},
- agents: [],
- actions: [],
- selectedAgent: null,
- selectedGrants: [],
- issuedCredential: {},
- replayText: '',
- form: { code: '', name: '', purpose: '', owner_uid: '', domain_uid: '', autonomy_level: 'read_only', environment: 'test' },
- grant: { interface_type: 'mcp', tool_name: 'knowledge.search', action: 'read', risk_level: 'low', requires_approval: false },
- autonomyOptions: [{ text: '只读', value: 'read_only' }, { text: '建议', value: 'suggestion' }],
- environmentOptions: [{ text: '开发', value: 'development' }, { text: '测试', value: 'test' }, { text: '生产', value: 'production' }],
- agentHeaders: [{ text: 'Agent', value: 'identity' }, { text: '自主等级', value: 'autonomy_level' }, { text: '范围', value: 'scope' }, { text: '版本', value: 'current_version' }, { text: '状态', value: 'status' }, { text: '操作', value: 'actions', sortable: false }],
- actionHeaders: [{ text: '工具与动作', value: 'tool' }, { text: '环境', value: 'environment' }, { text: '判定', value: 'decision' }, { text: '安全边界', value: 'safety' }, { text: '证据', value: 'actions', sortable: false }],
- grantHeaders: [{ text: '接口', value: 'interface_type' }, { text: '工具', value: 'tool_name' }, { text: '动作', value: 'action' }, { text: '环境', value: 'environment' }, { text: '风险', value: 'risk_level' }]
- }),
- computed: {
- permissions () { return (this.$store.state.user && this.$store.state.user.permissions) || [] },
- canOperate () { return this.permissions.includes('agents:operate') },
- canManage () { return this.permissions.includes('agents:manage') },
- metrics () {
- return [
- { label: '登记总数', value: this.dashboard.agent_count || 0 },
- { label: '当前启用', value: this.dashboard.active_count || 0 },
- { label: '已拒绝越权', value: this.dashboard.denied_count || 0 },
- { label: '等待审批', value: this.dashboard.pending_approval_count || 0 }
- ]
- },
- allowedActions () { return this.selectedAgent && this.selectedAgent.autonomy_level === 'suggestion' ? ['read', 'suggest'] : ['read'] }
- },
- created () { this.loadAll() },
- methods: {
- autonomy (value) { return ({ read_only: { label: '只读', color: 'blue-grey' }, suggestion: { label: '建议', color: 'primary' }, approval_execution: { label: '审批执行', color: 'warning' }, low_risk_automatic: { label: '低风险自动', color: 'deep-orange' } })[value] || { label: value, color: 'grey' } },
- statusColor (value) { return ({ active: 'success', suspended: 'warning', retired: 'grey', draft: 'blue-grey' })[value] || 'grey' },
- decisionColor (value) { return value === 'denied' ? 'error' : value === 'pending_approval' ? 'warning' : 'success' },
- formatTime (value) { return value ? new Date(value).toLocaleString() : '-' },
- async loadAll () { this.loading = true; try { const [d, a, r] = await Promise.all([getAgentGovernanceDashboard(), listGovernedAgents(), listAgentActions()]); this.dashboard = unwrap(d) || {}; this.agents = unwrap(a) || []; this.actions = unwrap(r) || [] } finally { this.loading = false } },
- async saveAgent () { this.saving = true; try { await registerGovernedAgent({ code: this.form.code, name: this.form.name, purpose: this.form.purpose, owner_uid: this.form.owner_uid, business_domain_uids: [this.form.domain_uid], environments: [this.form.environment], autonomy_level: this.form.autonomy_level, prompt_policy: { trusted_instruction_sources: ['platform_system'], untrusted_context_mode: 'quote_only', citation_required: this.form.autonomy_level === 'suggestion' } }); this.registerDialog = false; await this.loadAll() } finally { this.saving = false } },
- async openDetail (item) { this.selectedAgent = item; const response = await getGovernedAgent(item.uid); const detail = unwrap(response); this.selectedAgent = detail; this.selectedGrants = detail.grants || []; this.grantDialog = true },
- async saveGrant () { this.saving = true; try { await createAgentToolGrant(this.selectedAgent.uid, { ...this.grant, business_domain_uid: this.selectedAgent.business_domain_uids[0], environment: this.selectedAgent.environments[0] }); await this.openDetail(this.selectedAgent); await this.loadAll() } finally { this.saving = false } },
- async transition (item, action) { await transitionGovernedAgent(item.uid, { action, reason: 'Agent 治理台账操作' }, item.current_version); await this.loadAll() },
- async issueCredential (item) { const response = await issueAgentCredential(item.uid, 300); this.issuedCredential = unwrap(response) || {}; this.credentialDialog = true },
- closeCredential () { this.issuedCredential = {}; this.credentialDialog = false },
- async reconcile (item) { await reconcileAgentAction(item.uid, item.current_version); await this.loadAll() },
- async showReplay (item) { const response = await replayAgentAction(item.uid); this.replayText = JSON.stringify(unwrap(response), null, 2); this.replayDialog = true }
- }
- }
- </script>
- <style scoped>
- .agent-page { max-width: 1500px; margin: 0 auto; }
- .page-head { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; }
- .head-actions { display: flex; gap: 12px; flex-wrap: wrap; }
- .metric-block { border-top: 3px solid #1565c0; padding: 16px 4px 8px; }
- .metric-value { font-size: 28px; font-weight: 700; line-height: 1.2; margin-top: 5px; }
- .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
- .empty-state { text-align: center; color: #607d8b; }
- .replay-box { padding: 16px; max-height: 440px; overflow: auto; background: #f5f7f9; border: 1px solid #dce3e8; border-radius: 8px; white-space: pre-wrap; overflow-wrap: anywhere; }
- @media (max-width: 767px) { .page-head { flex-direction: column; } .head-actions { width: 100%; } }
- </style>
|