AgentsPage.vue 4.75 KB
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { Badge, Button, Card, Input } from '@drama/ui'

import { api, getToken } from '../api.js'

const { t } = useI18n()
const router = useRouter()
const agents = ref(null)
const me = ref(null)
const note = ref('')
/** 每个 Agent 的页面状态:刚重置出的 key、上限输入框、提示。 */
const local = reactive({})

const BADGE = { pending_claim: 'neutral', active: 'success', suspended: 'warning' }

async function load() {
  me.value = await api('GET', '/api/voko/me')
  agents.value = (await api('GET', '/api/voko/agents')).agents
  for (const agent of agents.value) {
    if (local[agent.id] === undefined) local[agent.id] = { key: '', limit: agent.daily_spend_limit === null ? '' : String(agent.daily_spend_limit), message: '' }
  }
}

async function apply() {
  await api('POST', '/api/voko/access-requests', { note: note.value })
  await load()
}

/**
 * @param {string} id
 * @param {() => Promise<void>} action
 */
async function run(id, action) {
  local[id].message = ''
  try {
    await action()
    await load()
  } catch (err) {
    local[id].message = err instanceof Error ? err.message : String(err)
  }
}

/** @param {string} id */
const resetKey = (id) => run(id, async () => {
  local[id].key = (await api('POST', `/api/voko/agents/${id}/api-key/reset`)).api_key
})

/**
 * @param {string} id
 * @param {'active'|'suspended'} status
 */
const setStatus = (id, status) => run(id, async () => {
  await api('POST', `/api/voko/agents/${id}/status`, { status })
})

/** @param {string} id */
const saveLimit = (id) => run(id, async () => {
  const raw = String(local[id].limit).trim()
  await api('POST', `/api/voko/agents/${id}/daily-spend-limit`, { daily_spend_limit: raw === '' ? null : Number(raw) })
  local[id].message = t('agents.saved')
})

onMounted(async () => {
  if (getToken() === null) {
    await router.push({ path: '/login', query: { next: '/agents' } })
    return
  }
  await load()
})
</script>

<template>
  <h1>{{ t('agents.title') }}</h1>
  <p class="voko-nav"><router-link to="/help">{{ t('help.title') }}</router-link> <router-link to="/approvals">{{ t('agents.approvals') }}</router-link> <router-link to="/settings">{{ t('settings.title') }}</router-link></p>
  <Card v-if="me && (me.access_request === null || me.access_request.status !== 'approved')" class="voko-agent">
    <p>{{ me.user.email }}</p>
    <template v-if="me.access_request === null">
      <p>{{ t('home.notApplied') }}</p>
      <label class="voko-field">{{ t('home.note') }}<Input v-model="note" /></label>
      <Button @click="apply">{{ t('home.apply') }}</Button>
    </template>
    <p v-else>{{ t(`home.status.${me.access_request.status}`) }}</p>
  </Card>
  <p v-if="me && me.user.role === 'admin'"><router-link to="/admin">{{ t('home.admin') }}</router-link></p>
  <template v-if="agents">
    <p v-if="agents.length === 0 && me.access_request !== null && me.access_request.status === 'approved'">{{ t('agents.empty') }}</p>
    <Card v-for="agent in agents" :key="agent.id" class="voko-agent">
      <h2>{{ agent.name }} <Badge :variant="BADGE[agent.status]">{{ t(`agents.status.${agent.status}`) }}</Badge></h2>
      <p>{{ t('agents.level', { level: agent.level }) }} · {{ t('agents.wallet', agent.wallet) }}</p>
      <p class="voko-nav"><router-link :to="`/agents/${agent.id}/world`">{{ t('world.title') }}</router-link> <router-link :to="`/agents/${agent.id}/board`">{{ t('agents.board') }}</router-link> <router-link :to="`/agents/${agent.id}/ledger`">{{ t('ledger.title') }}</router-link></p>
      <label class="voko-field">{{ t('agents.dailyLimit') }}<Input v-model="local[agent.id].limit" type="number" /></label>
      <div class="voko-actions">
        <Button variant="secondary" @click="saveLimit(agent.id)">{{ t('agents.saveLimit') }}</Button>
        <Button variant="secondary" @click="resetKey(agent.id)">{{ t('agents.resetKey') }}</Button>
        <Button v-if="agent.status === 'active'" variant="ghost" @click="setStatus(agent.id, 'suspended')">{{ t('agents.suspend') }}</Button>
        <Button v-else-if="agent.status === 'suspended'" variant="ghost" @click="setStatus(agent.id, 'active')">{{ t('agents.resume') }}</Button>
      </div>
      <p v-if="local[agent.id].key">{{ t('agents.newKey') }} <code class="voko-key">{{ local[agent.id].key }}</code></p>
      <p v-if="local[agent.id].message" role="status">{{ local[agent.id].message }}</p>
    </Card>
  </template>
</template>

<style scoped>
.voko-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--drama-space-md);
}
.voko-agent {
  margin-bottom: var(--drama-space-md);
}
.voko-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--drama-space-sm);
}
.voko-key {
  overflow-wrap: anywhere;
}
</style>