index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <template>
  2. <v-container fluid class="agent-page pa-6">
  3. <div class="page-head mb-5">
  4. <div>
  5. <h1 class="text-h4 font-weight-bold mb-2">Agent 治理</h1>
  6. <p class="text-body-1 text--secondary mb-0">
  7. 统一管理平台内部 Agent 的机器身份、最小授权、短期凭证和决策证据。
  8. </p>
  9. </div>
  10. <div class="head-actions">
  11. <v-btn outlined color="primary" :loading="loading" @click="loadAll">
  12. <v-icon left>mdi-refresh</v-icon>刷新
  13. </v-btn>
  14. <v-btn v-if="canManage" color="primary" depressed @click="registerDialog = true">
  15. <v-icon left>mdi-robot-outline</v-icon>登记 Agent
  16. </v-btn>
  17. </div>
  18. </div>
  19. <v-alert outlined color="deep-orange" icon="mdi-shield-alert-outline" class="mb-6">
  20. <strong>执行边界:</strong>高风险和关键风险操作即使通过双人审批,也只转为人工执行;
  21. 平台不会自动执行。凭证最长 15 分钟、仅签发时显示一次,数据库只保存摘要。
  22. </v-alert>
  23. <v-row class="mb-4">
  24. <v-col v-for="item in metrics" :key="item.label" cols="6" md="3">
  25. <div class="metric-block">
  26. <div class="text-caption text--secondary">{{ item.label }}</div>
  27. <div class="metric-value">{{ item.value }}</div>
  28. </div>
  29. </v-col>
  30. </v-row>
  31. <v-card outlined class="mb-6">
  32. <v-card-title class="d-flex justify-space-between align-center px-5">
  33. <span>身份与授权台账</span>
  34. <v-chip small outlined color="primary">版本化登记</v-chip>
  35. </v-card-title>
  36. <v-data-table :headers="agentHeaders" :items="agents" :loading="loading" :items-per-page="10">
  37. <template v-slot:[`item.identity`]="{ item }">
  38. <div class="font-weight-medium">{{ item.name }}</div>
  39. <div class="caption mono text--secondary">{{ item.code }}</div>
  40. </template>
  41. <template v-slot:[`item.autonomy_level`]="{ item }">
  42. <v-chip small outlined :color="autonomy(item.autonomy_level).color">
  43. {{ autonomy(item.autonomy_level).label }}
  44. </v-chip>
  45. </template>
  46. <template v-slot:[`item.scope`]="{ item }">
  47. <div>{{ (item.environments || []).join(' / ') }}</div>
  48. <div class="caption text--secondary">{{ (item.business_domain_uids || []).length }} 个业务域</div>
  49. </template>
  50. <template v-slot:[`item.status`]="{ item }">
  51. <v-chip x-small dark :color="statusColor(item.status)">{{ item.status }}</v-chip>
  52. </template>
  53. <template v-slot:[`item.actions`]="{ item }">
  54. <v-btn text x-small color="primary" @click="openDetail(item)">授权</v-btn>
  55. <v-btn v-if="canOperate && item.status === 'active'" text x-small color="primary" @click="issueCredential(item)">凭证</v-btn>
  56. <v-btn v-if="canOperate && item.status === 'draft'" text x-small color="success" @click="transition(item, 'activate')">启用</v-btn>
  57. <v-btn v-if="canOperate && item.status === 'active'" text x-small color="warning" @click="transition(item, 'suspend')">暂停</v-btn>
  58. </template>
  59. <template v-slot:no-data>
  60. <div class="empty-state py-10">
  61. <v-icon size="38" color="grey lighten-1">mdi-robot-off-outline</v-icon>
  62. <div class="mt-3">尚未登记受治理的 Agent</div>
  63. </div>
  64. </template>
  65. </v-data-table>
  66. </v-card>
  67. <v-card outlined>
  68. <v-card-title class="px-5">最近策略判定</v-card-title>
  69. <v-data-table :headers="actionHeaders" :items="actions" :loading="loading" :items-per-page="10">
  70. <template v-slot:[`item.tool`]="{ item }">
  71. <div class="mono">{{ item.tool_name }}</div>
  72. <div class="caption text--secondary">{{ item.interface_type }} · {{ item.action }}</div>
  73. </template>
  74. <template v-slot:[`item.decision`]="{ item }">
  75. <v-chip x-small outlined :color="decisionColor(item.decision)">{{ item.decision }}</v-chip>
  76. <div class="caption mt-1">{{ item.reason_code }}</div>
  77. </template>
  78. <template v-slot:[`item.safety`]="{ item }">
  79. <span>{{ item.risk_level }}</span>
  80. <div class="caption text--secondary">
  81. {{ item.automatic_execution_allowed ? '仅低风险自动' : '不自动执行' }}
  82. </div>
  83. </template>
  84. <template v-slot:[`item.actions`]="{ item }">
  85. <v-btn text x-small color="primary" @click="showReplay(item)">回放证据</v-btn>
  86. <v-btn v-if="canManage && item.decision === 'pending_approval'" text x-small color="primary" @click="reconcile(item)">同步审批</v-btn>
  87. </template>
  88. </v-data-table>
  89. </v-card>
  90. <v-dialog v-model="registerDialog" max-width="680">
  91. <v-card>
  92. <v-card-title>登记受治理 Agent</v-card-title>
  93. <v-card-text>
  94. <v-text-field v-model.trim="form.code" label="Agent 编码" outlined dense />
  95. <v-text-field v-model.trim="form.name" label="名称" outlined dense />
  96. <v-textarea v-model.trim="form.purpose" label="用途说明" outlined dense rows="2" />
  97. <v-text-field v-model.trim="form.owner_uid" label="责任人 UID" outlined dense />
  98. <v-text-field v-model.trim="form.domain_uid" label="业务域 UID" outlined dense />
  99. <v-select v-model="form.autonomy_level" :items="autonomyOptions" label="自主等级" outlined dense />
  100. <v-select v-model="form.environment" :items="environmentOptions" label="环境" outlined dense />
  101. <div class="caption text--secondary">建议型 Agent 强制携带证据引用;首批界面不提供自动执行登记。</div>
  102. </v-card-text>
  103. <v-card-actions>
  104. <v-spacer />
  105. <v-btn text @click="registerDialog = false">取消</v-btn>
  106. <v-btn color="primary" depressed :loading="saving" @click="saveAgent">保存</v-btn>
  107. </v-card-actions>
  108. </v-card>
  109. </v-dialog>
  110. <v-dialog v-model="grantDialog" max-width="720">
  111. <v-card>
  112. <v-card-title>工具授权 · {{ selectedAgent && selectedAgent.name }}</v-card-title>
  113. <v-card-text>
  114. <v-alert dense outlined type="info">授权严格绑定接口、工具、动作、业务域和环境。</v-alert>
  115. <v-data-table :headers="grantHeaders" :items="selectedGrants" dense hide-default-footer />
  116. <template v-if="canOperate">
  117. <v-divider class="my-4" />
  118. <v-row dense>
  119. <v-col cols="12" md="4"><v-select v-model="grant.interface_type" :items="['api', 'mcp']" label="接口" outlined dense /></v-col>
  120. <v-col cols="12" md="8"><v-text-field v-model.trim="grant.tool_name" label="工具名" outlined dense /></v-col>
  121. <v-col cols="12" md="4"><v-select v-model="grant.action" :items="allowedActions" label="动作" outlined dense /></v-col>
  122. <v-col cols="12" md="4"><v-select v-model="grant.risk_level" :items="['low', 'medium', 'high', 'critical']" label="风险" outlined dense /></v-col>
  123. <v-col cols="12" md="4"><v-switch v-model="grant.requires_approval" label="需要审批" inset /></v-col>
  124. </v-row>
  125. </template>
  126. </v-card-text>
  127. <v-card-actions>
  128. <v-spacer />
  129. <v-btn text @click="grantDialog = false">关闭</v-btn>
  130. <v-btn v-if="canOperate" color="primary" depressed :loading="saving" @click="saveGrant">新增授权</v-btn>
  131. </v-card-actions>
  132. </v-card>
  133. </v-dialog>
  134. <v-dialog v-model="credentialDialog" max-width="720" persistent>
  135. <v-card>
  136. <v-card-title>一次性机器凭证</v-card-title>
  137. <v-card-text>
  138. <v-alert type="warning" outlined>关闭后无法再次查看。请使用企业密钥系统安全保存,禁止写入日志或源码。</v-alert>
  139. <v-textarea :value="issuedCredential.token" readonly outlined rows="5" class="mono" label="X-Agent-Credential" />
  140. <div class="caption">失效时间:{{ formatTime(issuedCredential.expires_at) }}</div>
  141. </v-card-text>
  142. <v-card-actions><v-spacer /><v-btn color="primary" depressed @click="closeCredential">我已安全保存</v-btn></v-card-actions>
  143. </v-card>
  144. </v-dialog>
  145. <v-dialog v-model="replayDialog" max-width="800">
  146. <v-card>
  147. <v-card-title>决策证据回放</v-card-title>
  148. <v-card-text><pre class="replay-box">{{ replayText }}</pre></v-card-text>
  149. <v-card-actions><v-spacer /><v-btn text @click="replayDialog = false">关闭</v-btn></v-card-actions>
  150. </v-card>
  151. </v-dialog>
  152. </v-container>
  153. </template>
  154. <script>
  155. import {
  156. createAgentToolGrant,
  157. getAgentGovernanceDashboard,
  158. getGovernedAgent,
  159. issueAgentCredential,
  160. listAgentActions,
  161. listGovernedAgents,
  162. reconcileAgentAction,
  163. registerGovernedAgent,
  164. replayAgentAction,
  165. transitionGovernedAgent
  166. } from '@/api/agentGovernance'
  167. const unwrap = response => response && response.data && response.data.data !== undefined ? response.data.data : (response && response.data) || response
  168. export default {
  169. name: 'AgentGovernance',
  170. data: () => ({
  171. loading: false,
  172. saving: false,
  173. registerDialog: false,
  174. grantDialog: false,
  175. credentialDialog: false,
  176. replayDialog: false,
  177. dashboard: {},
  178. agents: [],
  179. actions: [],
  180. selectedAgent: null,
  181. selectedGrants: [],
  182. issuedCredential: {},
  183. replayText: '',
  184. form: { code: '', name: '', purpose: '', owner_uid: '', domain_uid: '', autonomy_level: 'read_only', environment: 'test' },
  185. grant: { interface_type: 'mcp', tool_name: 'knowledge.search', action: 'read', risk_level: 'low', requires_approval: false },
  186. autonomyOptions: [{ text: '只读', value: 'read_only' }, { text: '建议', value: 'suggestion' }],
  187. environmentOptions: [{ text: '开发', value: 'development' }, { text: '测试', value: 'test' }, { text: '生产', value: 'production' }],
  188. agentHeaders: [{ text: 'Agent', value: 'identity' }, { text: '自主等级', value: 'autonomy_level' }, { text: '范围', value: 'scope' }, { text: '版本', value: 'current_version' }, { text: '状态', value: 'status' }, { text: '操作', value: 'actions', sortable: false }],
  189. actionHeaders: [{ text: '工具与动作', value: 'tool' }, { text: '环境', value: 'environment' }, { text: '判定', value: 'decision' }, { text: '安全边界', value: 'safety' }, { text: '证据', value: 'actions', sortable: false }],
  190. grantHeaders: [{ text: '接口', value: 'interface_type' }, { text: '工具', value: 'tool_name' }, { text: '动作', value: 'action' }, { text: '环境', value: 'environment' }, { text: '风险', value: 'risk_level' }]
  191. }),
  192. computed: {
  193. permissions () { return (this.$store.state.user && this.$store.state.user.permissions) || [] },
  194. canOperate () { return this.permissions.includes('agents:operate') },
  195. canManage () { return this.permissions.includes('agents:manage') },
  196. metrics () {
  197. return [
  198. { label: '登记总数', value: this.dashboard.agent_count || 0 },
  199. { label: '当前启用', value: this.dashboard.active_count || 0 },
  200. { label: '已拒绝越权', value: this.dashboard.denied_count || 0 },
  201. { label: '等待审批', value: this.dashboard.pending_approval_count || 0 }
  202. ]
  203. },
  204. allowedActions () { return this.selectedAgent && this.selectedAgent.autonomy_level === 'suggestion' ? ['read', 'suggest'] : ['read'] }
  205. },
  206. created () { this.loadAll() },
  207. methods: {
  208. 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' } },
  209. statusColor (value) { return ({ active: 'success', suspended: 'warning', retired: 'grey', draft: 'blue-grey' })[value] || 'grey' },
  210. decisionColor (value) { return value === 'denied' ? 'error' : value === 'pending_approval' ? 'warning' : 'success' },
  211. formatTime (value) { return value ? new Date(value).toLocaleString() : '-' },
  212. 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 } },
  213. 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 } },
  214. 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 },
  215. 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 } },
  216. async transition (item, action) { await transitionGovernedAgent(item.uid, { action, reason: 'Agent 治理台账操作' }, item.current_version); await this.loadAll() },
  217. async issueCredential (item) { const response = await issueAgentCredential(item.uid, 300); this.issuedCredential = unwrap(response) || {}; this.credentialDialog = true },
  218. closeCredential () { this.issuedCredential = {}; this.credentialDialog = false },
  219. async reconcile (item) { await reconcileAgentAction(item.uid, item.current_version); await this.loadAll() },
  220. async showReplay (item) { const response = await replayAgentAction(item.uid); this.replayText = JSON.stringify(unwrap(response), null, 2); this.replayDialog = true }
  221. }
  222. }
  223. </script>
  224. <style scoped>
  225. .agent-page { max-width: 1500px; margin: 0 auto; }
  226. .page-head { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; }
  227. .head-actions { display: flex; gap: 12px; flex-wrap: wrap; }
  228. .metric-block { border-top: 3px solid #1565c0; padding: 16px 4px 8px; }
  229. .metric-value { font-size: 28px; font-weight: 700; line-height: 1.2; margin-top: 5px; }
  230. .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
  231. .empty-state { text-align: center; color: #607d8b; }
  232. .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; }
  233. @media (max-width: 767px) { .page-head { flex-direction: column; } .head-actions { width: 100%; } }
  234. </style>