|
|
@@ -1,20 +1,29 @@
|
|
|
<script setup lang="ts">
|
|
|
-import {computed, reactive, ref} from 'vue'
|
|
|
+import {computed, onMounted, reactive, ref} from 'vue'
|
|
|
import StandaloneDialog from '@/components/common/StandaloneDialog.vue'
|
|
|
import AgentAvatar from '@/components/common/AgentAvatar.vue'
|
|
|
import AppIcon from '@/components/common/AppIcon.vue'
|
|
|
import AgentIdentityPanel from '@/components/app/AgentIdentityPanel.vue'
|
|
|
import ModelServicesPanel from '@/components/app/ModelServicesPanel.vue'
|
|
|
import WorkspaceKnowledgePanel from './WorkspaceKnowledgePanel.vue'
|
|
|
-import type {WorkspaceAgent, WorkspaceApprovalPolicy, WorkspaceConversation, WorkspaceSource} from '@shared/workspace'
|
|
|
+import {apiRequest} from '@/api/client'
|
|
|
+import type {WorkspaceAgent, WorkspaceAgentUsage, WorkspaceApprovalPolicy, WorkspaceConversation, WorkspaceSource} from '@shared/workspace'
|
|
|
import type {Profile} from '@shared/types'
|
|
|
|
|
|
interface Draft {name:string;avatar:string;instructions:string;source:string;canManageTeam:boolean;canCollaborate:boolean;memoryEnabled:boolean;approvalPolicy?:WorkspaceApprovalPolicy}
|
|
|
-type Page='overview'|'identity'|'rules'|'models'|'memory'|'permissions'
|
|
|
+type Page='overview'|'identity'|'rules'|'usage'|'models'|'memory'|'permissions'
|
|
|
const props=defineProps<{agent:WorkspaceAgent;draft:Draft;sources:WorkspaceSource[];profiles:Profile[];agents:WorkspaceAgent[];conversations:WorkspaceConversation[];conversationId?:string;isAdmin?:boolean;knowledgeEnabled?:boolean;busy?:boolean;error?:string}>()
|
|
|
const emit=defineEmits<{close:[];save:[draft:Draft & {approvalPolicy:WorkspaceApprovalPolicy}];changed:[]}>()
|
|
|
const draft=reactive({...props.draft,approvalPolicy:props.draft.approvalPolicy??'ask'})
|
|
|
const page=ref<Page>('overview'),query=ref(''),localError=ref(''),modelDirty=ref(false),memoryDirty=ref(false)
|
|
|
+const usage=ref<WorkspaceAgentUsage|null>(null),usageLoading=ref(false),usageError=ref('')
|
|
|
+const formatTokens=(value:number)=>value.toLocaleString('zh-CN')
|
|
|
+onMounted(async()=>{
|
|
|
+ usageLoading.value=true
|
|
|
+ try{usage.value=await apiRequest<WorkspaceAgentUsage>(`/api/app/agents/${props.agent.id}/usage`)}
|
|
|
+ catch{usageError.value='暂时无法读取 Token 用量'}
|
|
|
+ finally{usageLoading.value=false}
|
|
|
+})
|
|
|
const readOnly=computed(()=>!!props.agent.remoteAgentId)
|
|
|
const snapshot=()=>({name:draft.name,avatar:draft.avatar,instructions:draft.instructions,source:draft.source,canManageTeam:draft.canManageTeam,canCollaborate:draft.canCollaborate,memoryEnabled:draft.memoryEnabled,approvalPolicy:draft.approvalPolicy})
|
|
|
const baseline=JSON.stringify(snapshot())
|
|
|
@@ -34,6 +43,7 @@ const allPages=computed(()=>[
|
|
|
{id:'overview' as const,label:'概览',icon:'panel' as const},
|
|
|
{id:'identity' as const,label:'身份与头像',icon:'users' as const},
|
|
|
{id:'rules' as const,label:'角色与规则',icon:'files' as const},
|
|
|
+ {id:'usage' as const,label:'Token 用量',icon:'history' as const},
|
|
|
...(props.isAdmin?[{id:'models' as const,label:'模型与 Provider',icon:'model' as const}]:[]),
|
|
|
...(props.knowledgeEnabled&&!readOnly.value?[{id:'memory' as const,label:'记忆',icon:'brain' as const}]:[]),
|
|
|
{id:'permissions' as const,label:'权限',icon:'settings' as const},
|
|
|
@@ -72,10 +82,27 @@ function submit(){
|
|
|
<p v-if="readOnly" class="profile-note">名称、头像与角色规则由远端管理;权限审批可以在这里单独设置。</p>
|
|
|
<template v-if="page==='overview'">
|
|
|
<section class="profile-card"><div class="profile-who"><AgentAvatar :name="draft.name" :avatar="draft.avatar" :size="52"/><div><h4>{{draft.name}}</h4><p>{{draft.instructions.trim().slice(0,180)||'尚未填写角色规则。'}}</p></div></div><button type="button" @click="selectPage('identity')">编辑身份与头像</button></section>
|
|
|
- <section class="profile-card"><h4>运行与协作</h4><dl><div><dt>基础机器人</dt><dd>{{sourceLabel}} / {{sourceProfile}}</dd></div><div><dt>执行环境</dt><dd>{{environment}}</dd></div><div><dt>Bot 协作</dt><dd>{{draft.canCollaborate?'已允许':'未开启'}}</dd></div><div><dt>组建团队</dt><dd>{{draft.canManageTeam?'已允许':'未开启'}}</dd></div></dl></section>
|
|
|
+ <section class="profile-card"><h4>运行与协作</h4><dl><div><dt>基础机器人</dt><dd>{{sourceLabel}} / {{sourceProfile}}</dd></div><div><dt>执行环境</dt><dd>{{environment}}</dd></div><div><dt>Token 用量</dt><dd>{{usageLoading?'读取中…':usage?`今日 ${formatTokens(usage.todayUsage.total)} · 本月 ${formatTokens(usage.monthUsage.total)}`:'暂无统计'}}</dd></div><div><dt>Bot 协作</dt><dd>{{draft.canCollaborate?'已允许':'未开启'}}</dd></div><div><dt>组建团队</dt><dd>{{draft.canManageTeam?'已允许':'未开启'}}</dd></div></dl><button type="button" @click="selectPage('usage')">查看 Token 用量</button></section>
|
|
|
</template>
|
|
|
<form v-else-if="page==='identity'" class="profile-form" @submit.prevent="submit"><label>名称<input v-model="draft.name" required maxlength="100" :readonly="readOnly" :disabled="busy"/></label><AgentIdentityPanel v-if="!readOnly" :profile="avatarProfile" embedded :show-name="false" :show-default-model="false" :show-actions="false" :busy="busy" @avatar-change="draft.avatar=$event"/><AgentAvatar v-else :name="draft.name" :avatar="draft.avatar" :size="96"/></form>
|
|
|
<form v-else-if="page==='rules'" class="profile-form" @submit.prevent="submit"><label>角色提示词与规则<textarea v-model="draft.instructions" rows="12" maxlength="24000" :readonly="readOnly" :disabled="busy" placeholder="描述这个 Bot 的职责、回答方式和工作规则。"/></label><p class="profile-note">这些规则属于当前 Bot,会在它的单聊和群聊中使用。</p></form>
|
|
|
+ <template v-else-if="page==='usage'">
|
|
|
+ <p v-if="usageError" class="profile-note">{{usageError}}</p>
|
|
|
+ <section v-if="usage" class="profile-card">
|
|
|
+ <h4>Token 用量</h4>
|
|
|
+ <div class="usage-stats">
|
|
|
+ <div class="usage-stat"><small>今日({{usage.today}})</small><strong>{{formatTokens(usage.todayUsage.total)}}</strong><span>输入 {{formatTokens(usage.todayUsage.input)}} · 输出 {{formatTokens(usage.todayUsage.output)}}</span></div>
|
|
|
+ <div class="usage-stat"><small>本月({{usage.month}})</small><strong>{{formatTokens(usage.monthUsage.total)}}</strong><span>输入 {{formatTokens(usage.monthUsage.input)}} · 输出 {{formatTokens(usage.monthUsage.output)}}</span></div>
|
|
|
+ <div class="usage-stat"><small>累计</small><strong>{{formatTokens(usage.totalUsage.total)}}</strong><span>输入 {{formatTokens(usage.totalUsage.input)}} · 输出 {{formatTokens(usage.totalUsage.output)}}</span></div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ <section v-if="usage" class="profile-card">
|
|
|
+ <h4>每日明细</h4>
|
|
|
+ <p v-if="!usage.daily.length" class="profile-note">还没有记录。与这个 Bot 对话后,这里会按天累计输入和输出的 Token。</p>
|
|
|
+ <table v-else class="usage-table"><thead><tr><th>日期</th><th>输入</th><th>输出</th><th>合计</th></tr></thead><tbody><tr v-for="day in usage.daily" :key="day.date"><td>{{day.date}}</td><td>{{formatTokens(day.input)}}</td><td>{{formatTokens(day.output)}}</td><td>{{formatTokens(day.total)}}</td></tr></tbody></table>
|
|
|
+ </section>
|
|
|
+ <p class="profile-note">用量在每次运行结束时从模型会话统计(输入 + 输出 Token)。从本版本起开始累计;远程 Bot 或个别模型不回报用量时无法统计。</p>
|
|
|
+ </template>
|
|
|
<template v-else-if="page==='models' && isAdmin">
|
|
|
<p v-if="readOnly||agent.nodeId!=='local'" class="profile-note">模型与 Provider 由远端执行节点管理,请在对应节点配置。</p>
|
|
|
<p v-else-if="sourceChanged" class="profile-note">请先保存基础机器人的变更,再配置对应的模型与 Provider。</p>
|
|
|
@@ -106,5 +133,6 @@ function submit(){
|
|
|
</style>
|
|
|
<style scoped>
|
|
|
.bot-profile__sidebar{min-width:0}@media(max-width:600px){.bot-profile{grid-template-columns:minmax(0,1fr)}}
|
|
|
+.usage-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}.usage-stat{display:grid;gap:6px;padding:14px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}.usage-stat small{color:var(--text-muted);font-size:11px}.usage-stat strong{font-size:20px;font-weight:650;font-variant-numeric:tabular-nums}.usage-stat span{color:var(--text-secondary);font-size:11px}.usage-table{width:100%;border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums}.usage-table th{color:var(--text-muted);font-weight:500;text-align:right;padding:6px 4px;border-bottom:1px solid var(--line)}.usage-table th:first-child,.usage-table td:first-child{text-align:left}.usage-table td{padding:7px 4px;border-bottom:1px solid var(--line);text-align:right}.usage-table td:first-child{color:var(--text-secondary)}
|
|
|
.profile-source .profile-source__picker{position:relative;min-height:48px;padding:6px 8px;margin:0;border:1px solid var(--line);border-radius:8px;background:var(--settings-panel);box-sizing:border-box}.profile-source__identity{flex:1}.profile-source__picker>select{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}.profile-source__picker:focus-within{outline:2px solid var(--accent);outline-offset:2px}@media(max-width:600px){.profile-source{display:block}.profile-source .profile-source__picker{max-width:100%}}
|
|
|
</style>
|