OverviewView.vue 4.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. <script setup lang="ts">
  2. import { onMounted, ref } from 'vue'
  3. import type { ProviderStatusDto } from '@agent-server/shared'
  4. import ClaudeLoginDialog from '../components/ClaudeLoginDialog.vue'
  5. import CodexLoginDialog from '../components/CodexLoginDialog.vue'
  6. import PageHeader from '../components/PageHeader.vue'
  7. import { admin, errorMessage } from '../lib/api'
  8. import { toast } from '../lib/ui'
  9. const providers = ref<ProviderStatusDto[]>([])
  10. const runs = ref({ running: 0, queued: 0, active: 0 })
  11. const loading = ref(false)
  12. const showLogin = ref(false)
  13. const showCodexLogin = ref(false)
  14. const loggingOut = ref(false)
  15. async function logout(provider: 'claude' | 'codex' = 'claude') {
  16. const name = provider === 'claude' ? 'Claude' : 'Codex'
  17. if (!confirm(`退出 ${name} 登录?退出后 AgentServer 将无法运行 ${name},直到重新登录。`)) return
  18. loggingOut.value = true
  19. try {
  20. await (provider === 'claude' ? admin.claudeLogin.logout() : admin.codexLogin.logout())
  21. toast('已退出登录', 'success')
  22. await load()
  23. } catch (err) {
  24. toast(errorMessage(err), 'error')
  25. } finally {
  26. loggingOut.value = false
  27. }
  28. }
  29. function onLoginClosed(loggedIn: boolean, name = 'Claude') {
  30. showLogin.value = false
  31. showCodexLogin.value = false
  32. if (loggedIn) toast(`${name} 登录成功`, 'success')
  33. void load()
  34. }
  35. async function load() {
  36. loading.value = true
  37. try {
  38. const o = await admin.overview()
  39. providers.value = o.providers
  40. runs.value = o.runs
  41. } catch (err) {
  42. toast(errorMessage(err), 'error')
  43. } finally {
  44. loading.value = false
  45. }
  46. }
  47. onMounted(load)
  48. </script>
  49. <template>
  50. <div class="h-full flex flex-col">
  51. <PageHeader title="概览" subtitle="引擎状态与运行情况">
  52. <button type="button" class="btn-secondary" :disabled="loading" @click="load"><span class="codicon codicon-refresh" />刷新</button>
  53. </PageHeader>
  54. <div class="flex-1 overflow-y-auto p-6 flex flex-col gap-6 max-w-240">
  55. <section class="grid grid-cols-3 gap-3">
  56. <div v-for="(v, k) in { running: '运行中', queued: '排队中', active: '活动 Run' }" :key="k" class="card p-4">
  57. <div class="hint">{{ v }}</div>
  58. <div class="text-[24px] font-600 mt-1">{{ runs[k] }}</div>
  59. </div>
  60. </section>
  61. <section class="flex flex-col gap-3">
  62. <h2 class="section-title m-0">引擎</h2>
  63. <div v-for="p in providers" :key="p.id" class="card p-4 flex gap-4 items-start">
  64. <span class="codicon codicon-hubot text-[28px]" :class="p.available ? 'text-accent' : 'text-fg-muted'" />
  65. <div class="flex-1 min-w-0 grid grid-cols-[96px_1fr] gap-y-1 text-[12.5px]">
  66. <div class="col-span-2 flex items-center gap-2 mb-1">
  67. <span class="text-[14px] font-600">{{ p.id === 'claude' ? 'Claude Code' : 'Codex' }}</span>
  68. <span class="badge" :class="p.available ? 'bg-success text-white' : p.enabled ? 'bg-danger text-white' : 'bg-[var(--app-input-border)] text-fg-muted'">
  69. {{ p.available ? '可用' : p.enabled ? '不可用' : '未启用' }}
  70. </span>
  71. <span class="flex-1" />
  72. <template v-if="p.loggedIn !== null">
  73. <button v-if="!p.loggedIn" type="button" class="btn-primary" @click="p.id === 'claude' ? (showLogin = true) : (showCodexLogin = true)"><span class="codicon codicon-account" />登录</button>
  74. <template v-else>
  75. <button type="button" class="btn-ghost" @click="p.id === 'claude' ? (showLogin = true) : (showCodexLogin = true)">重新登录</button>
  76. <button type="button" class="btn-ghost" :disabled="loggingOut" @click="logout(p.id)">退出登录</button>
  77. </template>
  78. </template>
  79. </div>
  80. <span class="text-fg-muted">账户</span><span>{{ p.account ?? '—' }}</span>
  81. <span class="text-fg-muted">CLI</span><span class="font-mono break-all">{{ p.cliPath }} <span class="text-fg-muted">{{ p.version }}</span></span>
  82. <span class="text-fg-muted">默认模型</span><span>{{ p.defaultModel || '—' }}</span>
  83. <span class="text-fg-muted">可选模型</span><span>{{ p.models.join('、') || '—' }}</span>
  84. <template v-if="p.message"><span class="text-fg-muted">说明</span><span class="text-warning">{{ p.message }}</span></template>
  85. </div>
  86. </div>
  87. <div class="hint">服务以专用账号 agentsvc 运行,登录的是你自己的账号:Claude 粘贴授权码完成,Codex 用设备码完成(在 OpenAI 页面输入短码)。也可以在服务器终端执行 <code class="font-mono">agent-claude auth login</code> / <code class="font-mono">agent-codex login --device-auth</code>。无需重启服务。</div>
  88. </section>
  89. </div>
  90. <ClaudeLoginDialog v-if="showLogin" @close="onLoginClosed" />
  91. <CodexLoginDialog v-if="showCodexLogin" @close="ok => onLoginClosed(ok, 'Codex')" />
  92. </div>
  93. </template>