mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-06-15 23:16:47 +00:00
Compare commits
10 Commits
AgentToken
...
AutoSignIn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec0c8cc521 | ||
|
|
52cd5b96e1 | ||
|
|
0d7be2b58c | ||
|
|
e29a710a33 | ||
|
|
de83b88ad1 | ||
|
|
96ac52041a | ||
|
|
287ccf50b2 | ||
|
|
08faed6ff0 | ||
|
|
944867f96e | ||
|
|
5f7c342b78 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,6 +11,8 @@ __pycache__/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
!plugins.v2/agenttokens/dist/
|
||||
!plugins.v2/agenttokens/dist/**
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@@ -160,4 +162,4 @@ cython_debug/
|
||||
#.idea/
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
.vscode/
|
||||
|
||||
@@ -44,12 +44,13 @@
|
||||
"name": "站点自动签到",
|
||||
"description": "自动模拟登录、签到站点。",
|
||||
"labels": "站点",
|
||||
"version": "2.8.2",
|
||||
"version": "2.9.0",
|
||||
"icon": "signin.png",
|
||||
"author": "thsrite",
|
||||
"level": 2,
|
||||
"release": true,
|
||||
"history": {
|
||||
"v2.9.0": "优化插件详情页,改为紧凑状态矩阵展示签到和登录情况",
|
||||
"v2.8.2": "优化站点 Rousi Pro 签到失败提示信息",
|
||||
"v2.8.1": "更新站点 Rousi Pro 签到接口",
|
||||
"v2.8": "适配站点 Rousi Pro",
|
||||
@@ -1048,13 +1049,16 @@
|
||||
"name": "Agent Tokens 管理",
|
||||
"description": "管理多平台免费 Token 配额,按优先级自动切换 Agent LLM 供应商。",
|
||||
"labels": "Agent,AI,系统",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.9",
|
||||
"icon": "agentresourceofficer.png",
|
||||
"author": "jxxghp",
|
||||
"level": 1,
|
||||
"system_version": ">=2.13.0",
|
||||
"release": true,
|
||||
"history": {
|
||||
"v1.0.9": "统一配置页和管理页内容,新增总使用进度图表卡片并优化大小屏布局",
|
||||
"v1.0.8": "支持为 Agent LLM 供应商配置并传递 User-Agent",
|
||||
"v1.0.7": "禁用VWindow触摸滑动,修复表格内滑动触发tab切换问题",
|
||||
"v1.0.6": "优化标题样式并对齐站点管理页面风格,修复弹窗标题截断问题",
|
||||
"v1.0.5": "优化UI布局,修复页面标题和按钮滚动问题",
|
||||
"v1.0.4": "补充分配模型信息及更新用量的运行日志",
|
||||
|
||||
@@ -24,7 +24,7 @@ class AgentTokens(_PluginBase):
|
||||
plugin_name = "Agent Tokens 管理"
|
||||
plugin_desc = "管理多平台免费 Token 配额,按优先级自动切换 Agent LLM 供应商。"
|
||||
plugin_icon = "agentresourceofficer.png"
|
||||
plugin_version = "1.0.6"
|
||||
plugin_version = "1.0.9"
|
||||
plugin_author = "jxxghp"
|
||||
author_url = "https://github.com/jxxghp"
|
||||
plugin_config_prefix = "agenttokens_"
|
||||
@@ -212,6 +212,7 @@ class AgentTokens(_PluginBase):
|
||||
) or "openai",
|
||||
"base_url": cls._clean_text(provider.get("base_url")),
|
||||
"api_key": cls._clean_text(provider.get("api_key")),
|
||||
"user_agent": cls._clean_text(provider.get("user_agent")),
|
||||
"model": cls._clean_text(provider.get("model")),
|
||||
"token_limit": token_limit,
|
||||
"used_tokens": used_tokens,
|
||||
@@ -426,6 +427,7 @@ class AgentTokens(_PluginBase):
|
||||
self._event_set(event.event_data, "provider", provider.get("provider") or "openai")
|
||||
self._event_set(event.event_data, "base_url", provider.get("base_url"))
|
||||
self._event_set(event.event_data, "api_key", provider.get("api_key"))
|
||||
self._event_set(event.event_data, "user_agent", provider.get("user_agent"))
|
||||
self._event_set(event.event_data, "model", provider.get("model"))
|
||||
self._event_set(event.event_data, "base_url_preset", None)
|
||||
self._event_set(event.event_data, "selected_provider_id", provider.get("id"))
|
||||
|
||||
148
plugins.v2/agenttokens/dist/assets/AgentTokensManager-BJe0fhEr.css
vendored
Normal file
148
plugins.v2/agenttokens/dist/assets/AgentTokensManager-BJe0fhEr.css
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
|
||||
.provider-table-shell[data-v-74897f54] {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.provider-table-shell[data-v-74897f54] table {
|
||||
min-width: 880px;
|
||||
}
|
||||
.truncate-cell[data-v-74897f54] {
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.provider-table-shell[data-v-a305c97e] {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.provider-table-shell[data-v-a305c97e] table {
|
||||
min-width: 760px;
|
||||
}
|
||||
.progress-cell[data-v-a305c97e] {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.usage-overview-card[data-v-f9b76345] {
|
||||
block-size: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
.usage-overview-card__content[data-v-f9b76345] {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.usage-overview-card__chart[data-v-f9b76345] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.usage-overview-card__percent[data-v-f9b76345] {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.usage-overview-card__headline[data-v-f9b76345] {
|
||||
margin-block-start: 4px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.usage-overview-card__meta[data-v-f9b76345] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.usage-overview-card[data-v-f9b76345] {
|
||||
padding: 16px;
|
||||
}
|
||||
.usage-overview-card__content[data-v-f9b76345] {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
.usage-overview-card__meta[data-v-f9b76345] {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.agenttokens-page[data-v-a6c1ea54] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
.agenttokens-header[data-v-a6c1ea54] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.agenttokens-control-panel[data-v-a6c1ea54] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.agenttokens-control-panel__switches[data-v-a6c1ea54] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 20px;
|
||||
}
|
||||
.agenttokens-overview-grid[data-v-a6c1ea54] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) repeat(3, minmax(10rem, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.agenttokens-overview-card[data-v-a6c1ea54] {
|
||||
min-block-size: 172px;
|
||||
}
|
||||
.agenttokens-stat-card[data-v-a6c1ea54] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-block-size: 104px;
|
||||
padding: 16px;
|
||||
}
|
||||
.agenttokens-stat-card__value[data-v-a6c1ea54] {
|
||||
margin-block-start: 2px;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.agenttokens-content-panel[data-v-a6c1ea54] {
|
||||
overflow: hidden;
|
||||
}
|
||||
.agenttokens-tabs-row[data-v-a6c1ea54] {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
.agenttokens-window[data-v-a6c1ea54] {
|
||||
padding: 12px;
|
||||
}
|
||||
.agenttokens-table-actions[data-v-a6c1ea54] {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-block-end: 12px;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.agenttokens-overview-grid[data-v-a6c1ea54] {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
.agenttokens-overview-card[data-v-a6c1ea54] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.agenttokens-page[data-v-a6c1ea54] {
|
||||
padding: 12px;
|
||||
}
|
||||
.agenttokens-overview-grid[data-v-a6c1ea54] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.agenttokens-stat-card[data-v-a6c1ea54] {
|
||||
min-block-size: 88px;
|
||||
}
|
||||
}
|
||||
963
plugins.v2/agenttokens/dist/assets/AgentTokensManager-DnY91SQC.js
vendored
Normal file
963
plugins.v2/agenttokens/dist/assets/AgentTokensManager-DnY91SQC.js
vendored
Normal file
@@ -0,0 +1,963 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import { f as formatTokens, P as PROVIDER_TYPE_OPTIONS, d as createProvider, b as buildProviderRows, a as buildProviderSummary, g as getNextProviderPriority, n as normalizeProvider } from './provider-BURm2Fqi.js';
|
||||
|
||||
const _export_sfc = (sfc, props) => {
|
||||
const target = sfc.__vccOpts || sfc;
|
||||
for (const [key, val] of props) {
|
||||
target[key] = val;
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
const {createElementVNode:_createElementVNode$3,openBlock:_openBlock$4,createElementBlock:_createElementBlock$2,createCommentVNode:_createCommentVNode$2,renderList:_renderList$1,Fragment:_Fragment$1,resolveComponent:_resolveComponent$4,createVNode:_createVNode$4,toDisplayString:_toDisplayString$4,unref:_unref$4,withCtx:_withCtx$4,createBlock:_createBlock$4} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1$3 = { key: 0 };
|
||||
const _hoisted_2$3 = { key: 1 };
|
||||
const _hoisted_3$3 = {
|
||||
key: 0,
|
||||
class: "truncate-cell"
|
||||
};
|
||||
const _hoisted_4$2 = { key: 1 };
|
||||
const _hoisted_5$2 = { class: "text-right" };
|
||||
const _hoisted_6$2 = { key: 0 };
|
||||
const _hoisted_7$2 = ["colspan"];
|
||||
|
||||
|
||||
const _sfc_main$4 = {
|
||||
__name: 'ProviderConfigTable',
|
||||
props: {
|
||||
providers: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
providerRows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showCredentials: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['edit', 'remove'],
|
||||
setup(__props, { emit: __emit }) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
const emit = __emit;
|
||||
|
||||
// 获取管理页服务端返回的脱敏 Key。
|
||||
function getMaskedApiKey(index) {
|
||||
return props.providerRows[index]?.masked_api_key || '****'
|
||||
}
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_VSwitch = _resolveComponent$4("VSwitch");
|
||||
const _component_VBtn = _resolveComponent$4("VBtn");
|
||||
const _component_VTable = _resolveComponent$4("VTable");
|
||||
const _component_VSheet = _resolveComponent$4("VSheet");
|
||||
|
||||
return (_openBlock$4(), _createBlock$4(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "provider-table-shell"
|
||||
}, {
|
||||
default: _withCtx$4(() => [
|
||||
_createVNode$4(_component_VTable, { density: "comfortable" }, {
|
||||
default: _withCtx$4(() => [
|
||||
_createElementVNode$3("thead", null, [
|
||||
_createElementVNode$3("tr", null, [
|
||||
_cache[0] || (_cache[0] = _createElementVNode$3("th", null, "启用", -1)),
|
||||
_cache[1] || (_cache[1] = _createElementVNode$3("th", null, "优先级", -1)),
|
||||
_cache[2] || (_cache[2] = _createElementVNode$3("th", null, "名称", -1)),
|
||||
_cache[3] || (_cache[3] = _createElementVNode$3("th", null, "类型", -1)),
|
||||
(__props.showCredentials)
|
||||
? (_openBlock$4(), _createElementBlock$2("th", _hoisted_1$3, "地址"))
|
||||
: _createCommentVNode$2("", true),
|
||||
(__props.showCredentials)
|
||||
? (_openBlock$4(), _createElementBlock$2("th", _hoisted_2$3, "Key"))
|
||||
: _createCommentVNode$2("", true),
|
||||
_cache[4] || (_cache[4] = _createElementVNode$3("th", null, "模型", -1)),
|
||||
_cache[5] || (_cache[5] = _createElementVNode$3("th", null, "额度", -1)),
|
||||
_cache[6] || (_cache[6] = _createElementVNode$3("th", { class: "text-right" }, "操作", -1))
|
||||
])
|
||||
]),
|
||||
_createElementVNode$3("tbody", null, [
|
||||
(_openBlock$4(true), _createElementBlock$2(_Fragment$1, null, _renderList$1(__props.providers, (row, index) => {
|
||||
return (_openBlock$4(), _createElementBlock$2("tr", {
|
||||
key: row.id || index
|
||||
}, [
|
||||
_createElementVNode$3("td", null, [
|
||||
_createVNode$4(_component_VSwitch, {
|
||||
modelValue: row.enabled,
|
||||
"onUpdate:modelValue": $event => ((row.enabled) = $event),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
density: "compact"
|
||||
}, null, 8, ["modelValue", "onUpdate:modelValue"])
|
||||
]),
|
||||
_createElementVNode$3("td", null, _toDisplayString$4(row.priority), 1),
|
||||
_createElementVNode$3("td", null, _toDisplayString$4(row.name), 1),
|
||||
_createElementVNode$3("td", null, _toDisplayString$4(row.provider), 1),
|
||||
(__props.showCredentials)
|
||||
? (_openBlock$4(), _createElementBlock$2("td", _hoisted_3$3, _toDisplayString$4(row.base_url), 1))
|
||||
: _createCommentVNode$2("", true),
|
||||
(__props.showCredentials)
|
||||
? (_openBlock$4(), _createElementBlock$2("td", _hoisted_4$2, _toDisplayString$4(getMaskedApiKey(index)), 1))
|
||||
: _createCommentVNode$2("", true),
|
||||
_createElementVNode$3("td", null, _toDisplayString$4(row.model), 1),
|
||||
_createElementVNode$3("td", null, _toDisplayString$4(row.token_limit > 0 ? _unref$4(formatTokens)(row.token_limit) : '不限'), 1),
|
||||
_createElementVNode$3("td", _hoisted_5$2, [
|
||||
_createVNode$4(_component_VBtn, {
|
||||
icon: "mdi-pencil",
|
||||
size: "small",
|
||||
variant: "text",
|
||||
onClick: $event => (emit('edit', index))
|
||||
}, null, 8, ["onClick"]),
|
||||
_createVNode$4(_component_VBtn, {
|
||||
icon: "mdi-delete",
|
||||
size: "small",
|
||||
variant: "text",
|
||||
color: "error",
|
||||
onClick: $event => (emit('remove', index))
|
||||
}, null, 8, ["onClick"])
|
||||
])
|
||||
]))
|
||||
}), 128)),
|
||||
(!__props.providers.length)
|
||||
? (_openBlock$4(), _createElementBlock$2("tr", _hoisted_6$2, [
|
||||
_createElementVNode$3("td", {
|
||||
colspan: __props.showCredentials ? 9 : 7,
|
||||
class: "text-center text-medium-emphasis py-8"
|
||||
}, "暂无供应商", 8, _hoisted_7$2)
|
||||
]))
|
||||
: _createCommentVNode$2("", true)
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const ProviderConfigTable = /*#__PURE__*/_export_sfc(_sfc_main$4, [['__scopeId',"data-v-74897f54"]]);
|
||||
|
||||
const {toDisplayString:_toDisplayString$3,createTextVNode:_createTextVNode$3,resolveComponent:_resolveComponent$3,withCtx:_withCtx$3,createVNode:_createVNode$3,unref:_unref$3,openBlock:_openBlock$3,createBlock:_createBlock$3} = await importShared('vue');
|
||||
|
||||
|
||||
const {computed: computed$2} = await importShared('vue');
|
||||
|
||||
|
||||
const _sfc_main$3 = {
|
||||
__name: 'ProviderEditorDialog',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
provider: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
editorIndex: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'commit'],
|
||||
setup(__props, { emit: __emit }) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
const emit = __emit;
|
||||
|
||||
const dialogVisible = computed$2({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
// 提交当前弹窗编辑的供应商配置。
|
||||
function commitProvider() {
|
||||
emit('commit');
|
||||
}
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_VCardTitle = _resolveComponent$3("VCardTitle");
|
||||
const _component_VTextField = _resolveComponent$3("VTextField");
|
||||
const _component_VCol = _resolveComponent$3("VCol");
|
||||
const _component_VSelect = _resolveComponent$3("VSelect");
|
||||
const _component_VRow = _resolveComponent$3("VRow");
|
||||
const _component_VCardText = _resolveComponent$3("VCardText");
|
||||
const _component_VSpacer = _resolveComponent$3("VSpacer");
|
||||
const _component_VBtn = _resolveComponent$3("VBtn");
|
||||
const _component_VCardActions = _resolveComponent$3("VCardActions");
|
||||
const _component_VCard = _resolveComponent$3("VCard");
|
||||
const _component_VDialog = _resolveComponent$3("VDialog");
|
||||
|
||||
return (_openBlock$3(), _createBlock$3(_component_VDialog, {
|
||||
modelValue: dialogVisible.value,
|
||||
"onUpdate:modelValue": _cache[10] || (_cache[10] = $event => ((dialogVisible).value = $event)),
|
||||
"max-width": "760",
|
||||
"max-height": "85vh",
|
||||
scrollable: ""
|
||||
}, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VCard, null, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VCardTitle, null, {
|
||||
default: _withCtx$3(() => [
|
||||
_createTextVNode$3(_toDisplayString$3(__props.editorIndex >= 0 ? '编辑供应商' : '新增供应商'), 1)
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCardText, null, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VRow, null, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "8"
|
||||
}, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VTextField, {
|
||||
modelValue: __props.provider.name,
|
||||
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((__props.provider.name) = $event)),
|
||||
label: "名称",
|
||||
variant: "outlined",
|
||||
density: "comfortable"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "4"
|
||||
}, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VTextField, {
|
||||
modelValue: __props.provider.priority,
|
||||
"onUpdate:modelValue": _cache[1] || (_cache[1] = $event => ((__props.provider.priority) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "优先级",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VSelect, {
|
||||
modelValue: __props.provider.provider,
|
||||
"onUpdate:modelValue": _cache[2] || (_cache[2] = $event => ((__props.provider.provider) = $event)),
|
||||
items: _unref$3(PROVIDER_TYPE_OPTIONS),
|
||||
label: "类型",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue", "items"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VTextField, {
|
||||
modelValue: __props.provider.model,
|
||||
"onUpdate:modelValue": _cache[3] || (_cache[3] = $event => ((__props.provider.model) = $event)),
|
||||
label: "模型",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCol, { cols: "12" }, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VTextField, {
|
||||
modelValue: __props.provider.base_url,
|
||||
"onUpdate:modelValue": _cache[4] || (_cache[4] = $event => ((__props.provider.base_url) = $event)),
|
||||
label: "API 地址",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCol, { cols: "12" }, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VTextField, {
|
||||
modelValue: __props.provider.api_key,
|
||||
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => ((__props.provider.api_key) = $event)),
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCol, { cols: "12" }, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VTextField, {
|
||||
modelValue: __props.provider.user_agent,
|
||||
"onUpdate:modelValue": _cache[6] || (_cache[6] = $event => ((__props.provider.user_agent) = $event)),
|
||||
label: "User-Agent",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VTextField, {
|
||||
modelValue: __props.provider.token_limit,
|
||||
"onUpdate:modelValue": _cache[7] || (_cache[7] = $event => ((__props.provider.token_limit) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "Token 额度",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VTextField, {
|
||||
modelValue: __props.provider.used_tokens,
|
||||
"onUpdate:modelValue": _cache[8] || (_cache[8] = $event => ((__props.provider.used_tokens) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "初始已用",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VCardActions, null, {
|
||||
default: _withCtx$3(() => [
|
||||
_createVNode$3(_component_VSpacer),
|
||||
_createVNode$3(_component_VBtn, {
|
||||
variant: "text",
|
||||
onClick: _cache[9] || (_cache[9] = $event => (dialogVisible.value = false))
|
||||
}, {
|
||||
default: _withCtx$3(() => [...(_cache[11] || (_cache[11] = [
|
||||
_createTextVNode$3("取消", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$3(_component_VBtn, {
|
||||
color: "primary",
|
||||
onClick: commitProvider
|
||||
}, {
|
||||
default: _withCtx$3(() => [...(_cache[12] || (_cache[12] = [
|
||||
_createTextVNode$3("确定", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"]))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const {createElementVNode:_createElementVNode$2,renderList:_renderList,Fragment:_Fragment,openBlock:_openBlock$2,createElementBlock:_createElementBlock$1,toDisplayString:_toDisplayString$2,unref:_unref$2,resolveComponent:_resolveComponent$2,createVNode:_createVNode$2,createTextVNode:_createTextVNode$2,withCtx:_withCtx$2,createCommentVNode:_createCommentVNode$1,createBlock:_createBlock$2} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1$2 = { class: "progress-cell" };
|
||||
const _hoisted_2$2 = { class: "text-right" };
|
||||
const _hoisted_3$2 = { key: 0 };
|
||||
|
||||
|
||||
const _sfc_main$2 = {
|
||||
__name: 'ProviderUsageTable',
|
||||
props: {
|
||||
providerRows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['reset'],
|
||||
setup(__props, { emit: __emit }) {
|
||||
|
||||
|
||||
|
||||
const emit = __emit;
|
||||
|
||||
// 根据供应商状态返回 Vuetify 颜色。
|
||||
function rowStatusColor(row) {
|
||||
if (!row.enabled) return 'default'
|
||||
if (row.usage?.exhausted) return 'error'
|
||||
if (!row.api_key || !row.base_url || !row.model) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 根据供应商状态返回短标签。
|
||||
function rowStatusText(row) {
|
||||
if (!row.enabled) return '停用'
|
||||
if (row.usage?.exhausted) return '耗尽'
|
||||
if (!row.api_key || !row.base_url || !row.model) return '缺配置'
|
||||
return '可用'
|
||||
}
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_VProgressLinear = _resolveComponent$2("VProgressLinear");
|
||||
const _component_VChip = _resolveComponent$2("VChip");
|
||||
const _component_VBtn = _resolveComponent$2("VBtn");
|
||||
const _component_VTable = _resolveComponent$2("VTable");
|
||||
const _component_VSheet = _resolveComponent$2("VSheet");
|
||||
|
||||
return (_openBlock$2(), _createBlock$2(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "provider-table-shell"
|
||||
}, {
|
||||
default: _withCtx$2(() => [
|
||||
_createVNode$2(_component_VTable, { density: "comfortable" }, {
|
||||
default: _withCtx$2(() => [
|
||||
_cache[1] || (_cache[1] = _createElementVNode$2("thead", null, [
|
||||
_createElementVNode$2("tr", null, [
|
||||
_createElementVNode$2("th", null, "优先级"),
|
||||
_createElementVNode$2("th", null, "名称"),
|
||||
_createElementVNode$2("th", null, "模型"),
|
||||
_createElementVNode$2("th", null, "已用"),
|
||||
_createElementVNode$2("th", null, "余量"),
|
||||
_createElementVNode$2("th", null, "进度"),
|
||||
_createElementVNode$2("th", null, "状态"),
|
||||
_createElementVNode$2("th", { class: "text-right" }, "操作")
|
||||
])
|
||||
], -1)),
|
||||
_createElementVNode$2("tbody", null, [
|
||||
(_openBlock$2(true), _createElementBlock$1(_Fragment, null, _renderList(__props.providerRows, (row, index) => {
|
||||
return (_openBlock$2(), _createElementBlock$1("tr", {
|
||||
key: row.id || index
|
||||
}, [
|
||||
_createElementVNode$2("td", null, _toDisplayString$2(row.priority), 1),
|
||||
_createElementVNode$2("td", null, _toDisplayString$2(row.name), 1),
|
||||
_createElementVNode$2("td", null, _toDisplayString$2(row.model), 1),
|
||||
_createElementVNode$2("td", null, _toDisplayString$2(_unref$2(formatTokens)(row.usage?.total_tokens)), 1),
|
||||
_createElementVNode$2("td", null, _toDisplayString$2(row.usage?.remaining_tokens === null ? '不限' : _unref$2(formatTokens)(row.usage?.remaining_tokens)), 1),
|
||||
_createElementVNode$2("td", _hoisted_1$2, [
|
||||
_createVNode$2(_component_VProgressLinear, {
|
||||
"model-value": row.usage?.usage_percent || 0,
|
||||
color: rowStatusColor(row),
|
||||
height: "8",
|
||||
rounded: ""
|
||||
}, null, 8, ["model-value", "color"])
|
||||
]),
|
||||
_createElementVNode$2("td", null, [
|
||||
_createVNode$2(_component_VChip, {
|
||||
size: "small",
|
||||
color: rowStatusColor(row),
|
||||
variant: "tonal"
|
||||
}, {
|
||||
default: _withCtx$2(() => [
|
||||
_createTextVNode$2(_toDisplayString$2(rowStatusText(row)), 1)
|
||||
]),
|
||||
_: 2
|
||||
}, 1032, ["color"])
|
||||
]),
|
||||
_createElementVNode$2("td", _hoisted_2$2, [
|
||||
_createVNode$2(_component_VBtn, {
|
||||
icon: "mdi-backup-restore",
|
||||
size: "small",
|
||||
variant: "text",
|
||||
onClick: $event => (emit('reset', row.id, index))
|
||||
}, null, 8, ["onClick"])
|
||||
])
|
||||
]))
|
||||
}), 128)),
|
||||
(!__props.providerRows.length)
|
||||
? (_openBlock$2(), _createElementBlock$1("tr", _hoisted_3$2, [...(_cache[0] || (_cache[0] = [
|
||||
_createElementVNode$2("td", {
|
||||
colspan: "8",
|
||||
class: "text-center text-medium-emphasis py-8"
|
||||
}, "暂无供应商", -1)
|
||||
]))]))
|
||||
: _createCommentVNode$1("", true)
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const ProviderUsageTable = /*#__PURE__*/_export_sfc(_sfc_main$2, [['__scopeId',"data-v-a305c97e"]]);
|
||||
|
||||
const {toDisplayString:_toDisplayString$1,createElementVNode:_createElementVNode$1,resolveComponent:_resolveComponent$1,withCtx:_withCtx$1,createVNode:_createVNode$1,unref:_unref$1,createTextVNode:_createTextVNode$1,openBlock:_openBlock$1,createBlock:_createBlock$1} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1$1 = { class: "usage-overview-card__content" };
|
||||
const _hoisted_2$1 = { class: "usage-overview-card__chart" };
|
||||
const _hoisted_3$1 = { class: "usage-overview-card__percent" };
|
||||
const _hoisted_4$1 = { class: "usage-overview-card__body" };
|
||||
const _hoisted_5$1 = { class: "usage-overview-card__headline" };
|
||||
const _hoisted_6$1 = { class: "text-medium-emphasis" };
|
||||
const _hoisted_7$1 = { class: "usage-overview-card__meta" };
|
||||
|
||||
const {computed: computed$1} = await importShared('vue');
|
||||
|
||||
|
||||
const _sfc_main$1 = {
|
||||
__name: 'UsageOverviewCard',
|
||||
props: {
|
||||
summary: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(__props) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
const totalUsed = computed$1(() => Number(props.summary.total_used || 0));
|
||||
const totalLimit = computed$1(() => Number(props.summary.total_limit || 0));
|
||||
const usagePercent = computed$1(() => {
|
||||
if (totalLimit.value <= 0) return 0
|
||||
return Math.min((totalUsed.value * 100) / totalLimit.value, 100)
|
||||
});
|
||||
const usagePercentText = computed$1(() => `${Math.round(usagePercent.value)}%`);
|
||||
const remainingTokens = computed$1(() => {
|
||||
if (totalLimit.value <= 0) return null
|
||||
return Math.max(totalLimit.value - totalUsed.value, 0)
|
||||
});
|
||||
const progressColor = computed$1(() => {
|
||||
if (totalLimit.value <= 0) return 'primary'
|
||||
if (usagePercent.value >= 90) return 'error'
|
||||
if (usagePercent.value >= 70) return 'warning'
|
||||
return 'success'
|
||||
});
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_VProgressCircular = _resolveComponent$1("VProgressCircular");
|
||||
const _component_VProgressLinear = _resolveComponent$1("VProgressLinear");
|
||||
const _component_VSheet = _resolveComponent$1("VSheet");
|
||||
|
||||
return (_openBlock$1(), _createBlock$1(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "usage-overview-card"
|
||||
}, {
|
||||
default: _withCtx$1(() => [
|
||||
_createElementVNode$1("div", _hoisted_1$1, [
|
||||
_createElementVNode$1("div", _hoisted_2$1, [
|
||||
_createVNode$1(_component_VProgressCircular, {
|
||||
"model-value": usagePercent.value,
|
||||
color: progressColor.value,
|
||||
"bg-color": "surface-variant",
|
||||
size: 132,
|
||||
width: 12
|
||||
}, {
|
||||
default: _withCtx$1(() => [
|
||||
_createElementVNode$1("div", _hoisted_3$1, _toDisplayString$1(totalLimit.value > 0 ? usagePercentText.value : '不限'), 1)
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["model-value", "color"])
|
||||
]),
|
||||
_createElementVNode$1("div", _hoisted_4$1, [
|
||||
_cache[0] || (_cache[0] = _createElementVNode$1("div", { class: "text-caption text-medium-emphasis" }, "总使用进度", -1)),
|
||||
_createElementVNode$1("div", _hoisted_5$1, [
|
||||
_createTextVNode$1(_toDisplayString$1(_unref$1(formatTokens)(totalUsed.value)) + " ", 1),
|
||||
_createElementVNode$1("span", _hoisted_6$1, "/ " + _toDisplayString$1(totalLimit.value > 0 ? _unref$1(formatTokens)(totalLimit.value) : '不限'), 1)
|
||||
]),
|
||||
_createVNode$1(_component_VProgressLinear, {
|
||||
"model-value": usagePercent.value,
|
||||
color: progressColor.value,
|
||||
height: "8",
|
||||
rounded: "",
|
||||
class: "my-4"
|
||||
}, null, 8, ["model-value", "color"]),
|
||||
_createElementVNode$1("div", _hoisted_7$1, [
|
||||
_createElementVNode$1("span", null, "剩余 " + _toDisplayString$1(remainingTokens.value === null ? '不限' : _unref$1(formatTokens)(remainingTokens.value)), 1),
|
||||
_createElementVNode$1("span", null, "可用 " + _toDisplayString$1(__props.summary.available_count || 0) + " / " + _toDisplayString$1(__props.summary.enabled_count || 0), 1)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const UsageOverviewCard = /*#__PURE__*/_export_sfc(_sfc_main$1, [['__scopeId',"data-v-f9b76345"]]);
|
||||
|
||||
const {createElementVNode:_createElementVNode,resolveComponent:_resolveComponent,createVNode:_createVNode,openBlock:_openBlock,createElementBlock:_createElementBlock,createCommentVNode:_createCommentVNode,toDisplayString:_toDisplayString,createTextVNode:_createTextVNode,withCtx:_withCtx,createBlock:_createBlock,unref:_unref} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1 = { class: "agenttokens-page" };
|
||||
const _hoisted_2 = {
|
||||
key: 0,
|
||||
class: "agenttokens-header"
|
||||
};
|
||||
const _hoisted_3 = { class: "agenttokens-control-panel__switches" };
|
||||
const _hoisted_4 = { class: "agenttokens-overview-grid" };
|
||||
const _hoisted_5 = { class: "agenttokens-stat-card__value" };
|
||||
const _hoisted_6 = { class: "agenttokens-stat-card__value" };
|
||||
const _hoisted_7 = { class: "agenttokens-stat-card__value" };
|
||||
const _hoisted_8 = { class: "agenttokens-tabs-row" };
|
||||
const _hoisted_9 = { class: "agenttokens-table-actions" };
|
||||
|
||||
const {computed,ref} = await importShared('vue');
|
||||
|
||||
|
||||
const _sfc_main = {
|
||||
__name: 'AgentTokensManager',
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({ enabled: false, show_sidebar_nav: true, providers: [] }),
|
||||
},
|
||||
providerRows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
summary: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
saving: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['refresh', 'save', 'reset-usage', 'reset-all-usage'],
|
||||
setup(__props, { emit: __emit }) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
const emit = __emit;
|
||||
|
||||
const activeTab = ref('usage');
|
||||
const showEditor = ref(false);
|
||||
const editorIndex = ref(-1);
|
||||
const editedProvider = ref(createProvider());
|
||||
|
||||
const configValue = computed(() => props.config || { enabled: false, show_sidebar_nav: true, providers: [] });
|
||||
const providers = computed(() => (Array.isArray(configValue.value.providers) ? configValue.value.providers : []));
|
||||
const displayProviderRows = computed(() => (
|
||||
props.providerRows.length ? props.providerRows : buildProviderRows(providers.value)
|
||||
));
|
||||
const displaySummary = computed(() => (
|
||||
Object.keys(props.summary || {}).length ? props.summary : buildProviderSummary(displayProviderRows.value)
|
||||
));
|
||||
|
||||
// 打开新增供应商弹窗。
|
||||
function addProvider() {
|
||||
editedProvider.value = { ...createProvider(), priority: getNextProviderPriority(providers.value) };
|
||||
editorIndex.value = -1;
|
||||
showEditor.value = true;
|
||||
}
|
||||
|
||||
// 打开编辑供应商弹窗。
|
||||
function editProvider(index) {
|
||||
editedProvider.value = { ...providers.value[index] };
|
||||
editorIndex.value = index;
|
||||
showEditor.value = true;
|
||||
}
|
||||
|
||||
// 将弹窗中的供应商写回配置列表。
|
||||
function commitProvider() {
|
||||
const nextProviders = [...providers.value];
|
||||
const normalized = normalizeProvider(editedProvider.value, nextProviders.length + 1);
|
||||
if (editorIndex.value >= 0) {
|
||||
nextProviders.splice(editorIndex.value, 1, normalized);
|
||||
} else {
|
||||
nextProviders.push(normalized);
|
||||
}
|
||||
configValue.value.providers = nextProviders;
|
||||
showEditor.value = false;
|
||||
}
|
||||
|
||||
// 从配置列表中移除一个供应商。
|
||||
function removeProvider(index) {
|
||||
const nextProviders = [...providers.value];
|
||||
nextProviders.splice(index, 1);
|
||||
configValue.value.providers = nextProviders;
|
||||
}
|
||||
|
||||
// 请求重置单个供应商用量。
|
||||
function resetUsage(providerId, index) {
|
||||
emit('reset-usage', providerId, index);
|
||||
}
|
||||
|
||||
// 请求重置全部供应商用量。
|
||||
function resetAllUsage() {
|
||||
emit('reset-all-usage');
|
||||
}
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_VSpacer = _resolveComponent("VSpacer");
|
||||
const _component_VBtn = _resolveComponent("VBtn");
|
||||
const _component_VAlert = _resolveComponent("VAlert");
|
||||
const _component_VSwitch = _resolveComponent("VSwitch");
|
||||
const _component_VSheet = _resolveComponent("VSheet");
|
||||
const _component_VIcon = _resolveComponent("VIcon");
|
||||
const _component_VTab = _resolveComponent("VTab");
|
||||
const _component_VTabs = _resolveComponent("VTabs");
|
||||
const _component_VDivider = _resolveComponent("VDivider");
|
||||
const _component_VWindowItem = _resolveComponent("VWindowItem");
|
||||
const _component_VWindow = _resolveComponent("VWindow");
|
||||
|
||||
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
|
||||
(!__props.hideTitle)
|
||||
? (_openBlock(), _createElementBlock("div", _hoisted_2, [
|
||||
_cache[7] || (_cache[7] = _createElementVNode("h2", { class: "text-2xl font-bold leading-7 text-gray-100 truncate sm:text-3xl sm:leading-9" }, [
|
||||
_createElementVNode("span", { class: "text-moviepilot" }, "Agent Tokens 管理")
|
||||
], -1)),
|
||||
_createVNode(_component_VSpacer),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-refresh",
|
||||
variant: "text",
|
||||
loading: __props.loading,
|
||||
onClick: _cache[0] || (_cache[0] = $event => (emit('refresh')))
|
||||
}, null, 8, ["loading"]),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-content-save",
|
||||
variant: "text",
|
||||
color: "primary",
|
||||
loading: __props.saving,
|
||||
onClick: _cache[1] || (_cache[1] = $event => (emit('save')))
|
||||
}, null, 8, ["loading"])
|
||||
]))
|
||||
: _createCommentVNode("", true),
|
||||
(__props.error)
|
||||
? (_openBlock(), _createBlock(_component_VAlert, {
|
||||
key: 1,
|
||||
type: "error",
|
||||
variant: "tonal",
|
||||
class: "mb-4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(__props.error), 1)
|
||||
]),
|
||||
_: 1
|
||||
}))
|
||||
: _createCommentVNode("", true),
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "agenttokens-control-panel"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createElementVNode("div", _hoisted_3, [
|
||||
_createVNode(_component_VSwitch, {
|
||||
modelValue: configValue.value.enabled,
|
||||
"onUpdate:modelValue": _cache[2] || (_cache[2] = $event => ((configValue.value.enabled) = $event)),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
inset: "",
|
||||
label: "启用插件"
|
||||
}, null, 8, ["modelValue"]),
|
||||
_createVNode(_component_VSwitch, {
|
||||
modelValue: configValue.value.show_sidebar_nav,
|
||||
"onUpdate:modelValue": _cache[3] || (_cache[3] = $event => ((configValue.value.show_sidebar_nav) = $event)),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
inset: "",
|
||||
label: "侧边栏入口"
|
||||
}, null, 8, ["modelValue"])
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createElementVNode("div", _hoisted_4, [
|
||||
_createVNode(UsageOverviewCard, {
|
||||
class: "agenttokens-overview-card",
|
||||
summary: displaySummary.value
|
||||
}, null, 8, ["summary"]),
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "agenttokens-stat-card"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VIcon, {
|
||||
icon: "mdi-check-decagram-outline",
|
||||
color: "success"
|
||||
}),
|
||||
_createElementVNode("div", null, [
|
||||
_cache[8] || (_cache[8] = _createElementVNode("div", { class: "text-caption text-medium-emphasis" }, "可用供应商", -1)),
|
||||
_createElementVNode("div", _hoisted_5, _toDisplayString(displaySummary.value.available_count || 0) + " / " + _toDisplayString(displaySummary.value.enabled_count || 0), 1)
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "agenttokens-stat-card"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VIcon, {
|
||||
icon: "mdi-chart-timeline-variant",
|
||||
color: "primary"
|
||||
}),
|
||||
_createElementVNode("div", null, [
|
||||
_cache[9] || (_cache[9] = _createElementVNode("div", { class: "text-caption text-medium-emphasis" }, "累计使用", -1)),
|
||||
_createElementVNode("div", _hoisted_6, _toDisplayString(_unref(formatTokens)(displaySummary.value.total_used)), 1)
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "agenttokens-stat-card"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VIcon, {
|
||||
icon: "mdi-database-outline",
|
||||
color: "info"
|
||||
}),
|
||||
_createElementVNode("div", null, [
|
||||
_cache[10] || (_cache[10] = _createElementVNode("div", { class: "text-caption text-medium-emphasis" }, "总额度", -1)),
|
||||
_createElementVNode("div", _hoisted_7, _toDisplayString(displaySummary.value.total_limit ? _unref(formatTokens)(displaySummary.value.total_limit) : '不限'), 1)
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "agenttokens-content-panel"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createElementVNode("div", _hoisted_8, [
|
||||
_createVNode(_component_VTabs, {
|
||||
modelValue: activeTab.value,
|
||||
"onUpdate:modelValue": _cache[4] || (_cache[4] = $event => ((activeTab).value = $event)),
|
||||
density: "comfortable"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTab, { value: "usage" }, {
|
||||
default: _withCtx(() => [...(_cache[11] || (_cache[11] = [
|
||||
_createTextVNode("用量", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VTab, { value: "config" }, {
|
||||
default: _withCtx(() => [...(_cache[12] || (_cache[12] = [
|
||||
_createTextVNode("配置", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]),
|
||||
_createVNode(_component_VDivider),
|
||||
_createVNode(_component_VWindow, {
|
||||
modelValue: activeTab.value,
|
||||
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => ((activeTab).value = $event)),
|
||||
touch: false,
|
||||
class: "agenttokens-window"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VWindowItem, { value: "usage" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(ProviderUsageTable, {
|
||||
"provider-rows": displayProviderRows.value,
|
||||
onReset: resetUsage
|
||||
}, null, 8, ["provider-rows"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VWindowItem, { value: "config" }, {
|
||||
default: _withCtx(() => [
|
||||
_createElementVNode("div", _hoisted_9, [
|
||||
_createVNode(_component_VBtn, {
|
||||
"prepend-icon": "mdi-plus",
|
||||
color: "primary",
|
||||
variant: "tonal",
|
||||
onClick: addProvider
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[13] || (_cache[13] = [
|
||||
_createTextVNode("新增", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VBtn, {
|
||||
"prepend-icon": "mdi-backup-restore",
|
||||
color: "warning",
|
||||
variant: "tonal",
|
||||
onClick: resetAllUsage
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[14] || (_cache[14] = [
|
||||
_createTextVNode(" 重置用量 ", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_createVNode(ProviderConfigTable, {
|
||||
providers: providers.value,
|
||||
"provider-rows": displayProviderRows.value,
|
||||
"show-credentials": "",
|
||||
onEdit: editProvider,
|
||||
onRemove: removeProvider
|
||||
}, null, 8, ["providers", "provider-rows"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_sfc_main$3, {
|
||||
modelValue: showEditor.value,
|
||||
"onUpdate:modelValue": _cache[6] || (_cache[6] = $event => ((showEditor).value = $event)),
|
||||
provider: editedProvider.value,
|
||||
"editor-index": editorIndex.value,
|
||||
onCommit: commitProvider
|
||||
}, null, 8, ["modelValue", "provider", "editor-index"])
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const AgentTokensManager = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-a6c1ea54"]]);
|
||||
|
||||
export { AgentTokensManager as A, _export_sfc as _ };
|
||||
@@ -1,775 +0,0 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import { _ as _export_sfc } from './_plugin-vue_export-helper-pcqpp-6-.js';
|
||||
|
||||
const {createElementVNode:_createElementVNode,resolveComponent:_resolveComponent,createVNode:_createVNode,openBlock:_openBlock,createElementBlock:_createElementBlock,createCommentVNode:_createCommentVNode,toDisplayString:_toDisplayString,createTextVNode:_createTextVNode,withCtx:_withCtx,createBlock:_createBlock,renderList:_renderList,Fragment:_Fragment} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1 = { class: "agenttokens-page pa-4" };
|
||||
const _hoisted_2 = {
|
||||
key: 0,
|
||||
class: "d-flex align-center gap-2 mb-4 flex-nowrap"
|
||||
};
|
||||
const _hoisted_3 = { class: "text-h5" };
|
||||
const _hoisted_4 = { class: "text-h5" };
|
||||
const _hoisted_5 = { class: "text-h5" };
|
||||
const _hoisted_6 = { class: "progress-cell" };
|
||||
const _hoisted_7 = { class: "text-right" };
|
||||
const _hoisted_8 = { key: 0 };
|
||||
const _hoisted_9 = { class: "d-flex justify-end mb-3 gap-2" };
|
||||
const _hoisted_10 = { class: "truncate-cell" };
|
||||
const _hoisted_11 = { class: "text-right" };
|
||||
const _hoisted_12 = { key: 0 };
|
||||
|
||||
const {computed,onMounted,ref} = await importShared('vue');
|
||||
|
||||
|
||||
|
||||
const _sfc_main = {
|
||||
__name: 'AppPage',
|
||||
props: {
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
pluginId: {
|
||||
type: String,
|
||||
default: 'AgentTokens',
|
||||
},
|
||||
hideTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const error = ref('');
|
||||
const activeTab = ref('usage');
|
||||
const showEditor = ref(false);
|
||||
const editorIndex = ref(-1);
|
||||
const editedProvider = ref(createProvider());
|
||||
const status = ref({
|
||||
config: { enabled: false, providers: [] },
|
||||
providers: [],
|
||||
summary: {},
|
||||
});
|
||||
|
||||
// 构造 API 基础路径。
|
||||
const pluginBase = computed(() => `plugin/${props.pluginId || 'AgentTokens'}`);
|
||||
const config = computed(() => status.value.config || { enabled: false, providers: [] });
|
||||
const providerRows = computed(() => status.value.providers || []);
|
||||
const summary = computed(() => status.value.summary || {});
|
||||
|
||||
const providerTypeOptions = [
|
||||
{ title: 'OpenAI Compatible', value: 'openai' },
|
||||
{ title: 'DeepSeek', value: 'deepseek' },
|
||||
{ title: 'Google Gemini', value: 'google' },
|
||||
{ title: 'Anthropic Compatible', value: 'anthropic' },
|
||||
{ title: 'ChatGPT', value: 'chatgpt' },
|
||||
];
|
||||
|
||||
// 构建一个新的供应商默认配置。
|
||||
function createProvider() {
|
||||
return {
|
||||
id: '',
|
||||
enabled: true,
|
||||
name: '',
|
||||
provider: 'openai',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
model: '',
|
||||
token_limit: 0,
|
||||
used_tokens: 0,
|
||||
priority: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容 MoviePilot API 包装器和原始响应两种返回形态。
|
||||
function unwrapResponse(response) {
|
||||
if (response && Object.prototype.hasOwnProperty.call(response, 'data') && response.success !== undefined) {
|
||||
return response.data
|
||||
}
|
||||
return response?.data ?? response
|
||||
}
|
||||
|
||||
// 格式化 token 数字,保持表格紧凑可读。
|
||||
function formatTokens(value) {
|
||||
const numberValue = Number(value || 0);
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : '0'
|
||||
}
|
||||
|
||||
// 根据供应商状态返回 Vuetify 颜色。
|
||||
function rowStatusColor(row) {
|
||||
if (!row.enabled) return 'default'
|
||||
if (row.usage?.exhausted) return 'error'
|
||||
if (!row.api_key || !row.base_url || !row.model) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 根据供应商状态返回短标签。
|
||||
function rowStatusText(row) {
|
||||
if (!row.enabled) return '停用'
|
||||
if (row.usage?.exhausted) return '耗尽'
|
||||
if (!row.api_key || !row.base_url || !row.model) return '缺配置'
|
||||
return '可用'
|
||||
}
|
||||
|
||||
// 从插件 API 拉取当前配置和用量状态。
|
||||
async function loadStatus() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const response = await props.api.get(`${pluginBase.value}/status`);
|
||||
status.value = unwrapResponse(response) || status.value;
|
||||
} catch (err) {
|
||||
error.value = err?.message || '加载失败';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存完整插件配置并刷新服务端标准化后的状态。
|
||||
async function saveConfig() {
|
||||
saving.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const payload = {
|
||||
enabled: Boolean(config.value.enabled),
|
||||
show_sidebar_nav: Boolean(config.value.show_sidebar_nav),
|
||||
providers: [...(config.value.providers || [])],
|
||||
};
|
||||
const response = await props.api.post(`${pluginBase.value}/config`, payload);
|
||||
status.value = unwrapResponse(response) || status.value;
|
||||
} catch (err) {
|
||||
error.value = err?.message || '保存失败';
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 打开新增供应商弹窗。
|
||||
function addProvider() {
|
||||
const nextPriority = Math.max(0, ...(config.value.providers || []).map(item => Number(item.priority || 0))) + 1;
|
||||
editedProvider.value = { ...createProvider(), priority: nextPriority };
|
||||
editorIndex.value = -1;
|
||||
showEditor.value = true;
|
||||
}
|
||||
|
||||
// 打开编辑供应商弹窗。
|
||||
function editProvider(index) {
|
||||
editedProvider.value = { ...config.value.providers[index] };
|
||||
editorIndex.value = index;
|
||||
showEditor.value = true;
|
||||
}
|
||||
|
||||
// 将弹窗中的供应商写回配置列表。
|
||||
function commitProvider() {
|
||||
const providers = [...(config.value.providers || [])];
|
||||
const normalized = {
|
||||
...editedProvider.value,
|
||||
token_limit: Number(editedProvider.value.token_limit || 0),
|
||||
used_tokens: Number(editedProvider.value.used_tokens || 0),
|
||||
priority: Number(editedProvider.value.priority || providers.length + 1),
|
||||
};
|
||||
if (editorIndex.value >= 0) {
|
||||
providers.splice(editorIndex.value, 1, normalized);
|
||||
} else {
|
||||
providers.push(normalized);
|
||||
}
|
||||
status.value.config = { ...config.value, providers };
|
||||
showEditor.value = false;
|
||||
}
|
||||
|
||||
// 从配置列表中移除一个供应商。
|
||||
function removeProvider(index) {
|
||||
const providers = [...(config.value.providers || [])];
|
||||
providers.splice(index, 1);
|
||||
status.value.config = { ...config.value, providers };
|
||||
}
|
||||
|
||||
// 重置指定供应商的运行记录。
|
||||
async function resetUsage(providerId) {
|
||||
if (!providerId) return
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await props.api.post(`${pluginBase.value}/usage/reset`, { provider_id: providerId });
|
||||
status.value = unwrapResponse(response) || status.value;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置全部供应商的运行记录。
|
||||
async function resetAllUsage() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await props.api.post(`${pluginBase.value}/usage/reset_all`, {});
|
||||
status.value = unwrapResponse(response) || status.value;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
__expose({
|
||||
loadStatus,
|
||||
saveConfig,
|
||||
loading,
|
||||
saving,
|
||||
});
|
||||
|
||||
onMounted(loadStatus);
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_VSpacer = _resolveComponent("VSpacer");
|
||||
const _component_VBtn = _resolveComponent("VBtn");
|
||||
const _component_VAlert = _resolveComponent("VAlert");
|
||||
const _component_VSwitch = _resolveComponent("VSwitch");
|
||||
const _component_VCol = _resolveComponent("VCol");
|
||||
const _component_VRow = _resolveComponent("VRow");
|
||||
const _component_VSheet = _resolveComponent("VSheet");
|
||||
const _component_VTab = _resolveComponent("VTab");
|
||||
const _component_VTabs = _resolveComponent("VTabs");
|
||||
const _component_VProgressLinear = _resolveComponent("VProgressLinear");
|
||||
const _component_VChip = _resolveComponent("VChip");
|
||||
const _component_VTable = _resolveComponent("VTable");
|
||||
const _component_VWindowItem = _resolveComponent("VWindowItem");
|
||||
const _component_VWindow = _resolveComponent("VWindow");
|
||||
const _component_VCardTitle = _resolveComponent("VCardTitle");
|
||||
const _component_VTextField = _resolveComponent("VTextField");
|
||||
const _component_VSelect = _resolveComponent("VSelect");
|
||||
const _component_VCardText = _resolveComponent("VCardText");
|
||||
const _component_VCardActions = _resolveComponent("VCardActions");
|
||||
const _component_VCard = _resolveComponent("VCard");
|
||||
const _component_VDialog = _resolveComponent("VDialog");
|
||||
|
||||
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
|
||||
(!__props.hideTitle)
|
||||
? (_openBlock(), _createElementBlock("div", _hoisted_2, [
|
||||
_cache[14] || (_cache[14] = _createElementVNode("h2", { class: "text-2xl font-bold leading-7 text-gray-100 truncate sm:text-3xl sm:leading-9" }, [
|
||||
_createElementVNode("span", { class: "text-moviepilot" }, "Agent Tokens 管理")
|
||||
], -1)),
|
||||
_createVNode(_component_VSpacer),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-refresh",
|
||||
variant: "text",
|
||||
loading: loading.value,
|
||||
onClick: loadStatus
|
||||
}, null, 8, ["loading"]),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-content-save",
|
||||
variant: "text",
|
||||
color: "primary",
|
||||
loading: saving.value,
|
||||
onClick: saveConfig
|
||||
}, null, 8, ["loading"])
|
||||
]))
|
||||
: _createCommentVNode("", true),
|
||||
(error.value)
|
||||
? (_openBlock(), _createBlock(_component_VAlert, {
|
||||
key: 1,
|
||||
type: "error",
|
||||
variant: "tonal",
|
||||
class: "mb-4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(error.value), 1)
|
||||
]),
|
||||
_: 1
|
||||
}))
|
||||
: _createCommentVNode("", true),
|
||||
_createVNode(_component_VRow, { class: "mb-4" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
sm: "auto"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
(status.value.config)
|
||||
? (_openBlock(), _createBlock(_component_VSwitch, {
|
||||
key: 0,
|
||||
modelValue: status.value.config.enabled,
|
||||
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((status.value.config.enabled) = $event)),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
inset: "",
|
||||
label: "启用插件"
|
||||
}, null, 8, ["modelValue"]))
|
||||
: _createCommentVNode("", true)
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
sm: "auto"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
(status.value.config)
|
||||
? (_openBlock(), _createBlock(_component_VSwitch, {
|
||||
key: 0,
|
||||
modelValue: status.value.config.show_sidebar_nav,
|
||||
"onUpdate:modelValue": _cache[1] || (_cache[1] = $event => ((status.value.config.show_sidebar_nav) = $event)),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
inset: "",
|
||||
label: "侧边栏入口"
|
||||
}, null, 8, ["modelValue"]))
|
||||
: _createCommentVNode("", true)
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VRow, { class: "mb-2" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
sm: "4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "pa-4 h-100"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_cache[15] || (_cache[15] = _createElementVNode("div", { class: "text-caption text-medium-emphasis" }, "可用供应商", -1)),
|
||||
_createElementVNode("div", _hoisted_3, _toDisplayString(summary.value.available_count || 0) + " / " + _toDisplayString(summary.value.enabled_count || 0), 1)
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
sm: "4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "pa-4 h-100"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_cache[16] || (_cache[16] = _createElementVNode("div", { class: "text-caption text-medium-emphasis" }, "累计使用", -1)),
|
||||
_createElementVNode("div", _hoisted_4, _toDisplayString(formatTokens(summary.value.total_used)), 1)
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
sm: "4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: "",
|
||||
class: "pa-4 h-100"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_cache[17] || (_cache[17] = _createElementVNode("div", { class: "text-caption text-medium-emphasis" }, "总额度", -1)),
|
||||
_createElementVNode("div", _hoisted_5, _toDisplayString(formatTokens(summary.value.total_limit)), 1)
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VTabs, {
|
||||
modelValue: activeTab.value,
|
||||
"onUpdate:modelValue": _cache[2] || (_cache[2] = $event => ((activeTab).value = $event)),
|
||||
density: "comfortable",
|
||||
class: "mb-3"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTab, { value: "usage" }, {
|
||||
default: _withCtx(() => [...(_cache[18] || (_cache[18] = [
|
||||
_createTextVNode("用量", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VTab, { value: "config" }, {
|
||||
default: _withCtx(() => [...(_cache[19] || (_cache[19] = [
|
||||
_createTextVNode("配置", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"]),
|
||||
_createVNode(_component_VWindow, {
|
||||
modelValue: activeTab.value,
|
||||
"onUpdate:modelValue": _cache[3] || (_cache[3] = $event => ((activeTab).value = $event))
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VWindowItem, { value: "usage" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: ""
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTable, { density: "comfortable" }, {
|
||||
default: _withCtx(() => [
|
||||
_cache[21] || (_cache[21] = _createElementVNode("thead", null, [
|
||||
_createElementVNode("tr", null, [
|
||||
_createElementVNode("th", null, "优先级"),
|
||||
_createElementVNode("th", null, "名称"),
|
||||
_createElementVNode("th", null, "模型"),
|
||||
_createElementVNode("th", null, "已用"),
|
||||
_createElementVNode("th", null, "余量"),
|
||||
_createElementVNode("th", null, "进度"),
|
||||
_createElementVNode("th", null, "状态"),
|
||||
_createElementVNode("th", { class: "text-right" }, "操作")
|
||||
])
|
||||
], -1)),
|
||||
_createElementVNode("tbody", null, [
|
||||
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(providerRows.value, (row) => {
|
||||
return (_openBlock(), _createElementBlock("tr", {
|
||||
key: row.id
|
||||
}, [
|
||||
_createElementVNode("td", null, _toDisplayString(row.priority), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.name), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.model), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(formatTokens(row.usage?.total_tokens)), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.usage?.remaining_tokens === null ? '不限' : formatTokens(row.usage?.remaining_tokens)), 1),
|
||||
_createElementVNode("td", _hoisted_6, [
|
||||
_createVNode(_component_VProgressLinear, {
|
||||
"model-value": row.usage?.usage_percent || 0,
|
||||
color: rowStatusColor(row),
|
||||
height: "8",
|
||||
rounded: ""
|
||||
}, null, 8, ["model-value", "color"])
|
||||
]),
|
||||
_createElementVNode("td", null, [
|
||||
_createVNode(_component_VChip, {
|
||||
size: "small",
|
||||
color: rowStatusColor(row),
|
||||
variant: "tonal"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(rowStatusText(row)), 1)
|
||||
]),
|
||||
_: 2
|
||||
}, 1032, ["color"])
|
||||
]),
|
||||
_createElementVNode("td", _hoisted_7, [
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-backup-restore",
|
||||
size: "small",
|
||||
variant: "text",
|
||||
onClick: $event => (resetUsage(row.id))
|
||||
}, null, 8, ["onClick"])
|
||||
])
|
||||
]))
|
||||
}), 128)),
|
||||
(!providerRows.value.length)
|
||||
? (_openBlock(), _createElementBlock("tr", _hoisted_8, [...(_cache[20] || (_cache[20] = [
|
||||
_createElementVNode("td", {
|
||||
colspan: "8",
|
||||
class: "text-center text-medium-emphasis py-8"
|
||||
}, "暂无供应商", -1)
|
||||
]))]))
|
||||
: _createCommentVNode("", true)
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VWindowItem, { value: "config" }, {
|
||||
default: _withCtx(() => [
|
||||
_createElementVNode("div", _hoisted_9, [
|
||||
_createVNode(_component_VBtn, {
|
||||
"prepend-icon": "mdi-plus",
|
||||
color: "primary",
|
||||
variant: "tonal",
|
||||
onClick: addProvider
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[22] || (_cache[22] = [
|
||||
_createTextVNode("新增", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VBtn, {
|
||||
"prepend-icon": "mdi-backup-restore",
|
||||
color: "warning",
|
||||
variant: "tonal",
|
||||
onClick: resetAllUsage
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[23] || (_cache[23] = [
|
||||
_createTextVNode("重置用量", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: ""
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTable, { density: "comfortable" }, {
|
||||
default: _withCtx(() => [
|
||||
_cache[25] || (_cache[25] = _createElementVNode("thead", null, [
|
||||
_createElementVNode("tr", null, [
|
||||
_createElementVNode("th", null, "启用"),
|
||||
_createElementVNode("th", null, "优先级"),
|
||||
_createElementVNode("th", null, "名称"),
|
||||
_createElementVNode("th", null, "类型"),
|
||||
_createElementVNode("th", null, "地址"),
|
||||
_createElementVNode("th", null, "Key"),
|
||||
_createElementVNode("th", null, "模型"),
|
||||
_createElementVNode("th", null, "额度"),
|
||||
_createElementVNode("th", { class: "text-right" }, "操作")
|
||||
])
|
||||
], -1)),
|
||||
_createElementVNode("tbody", null, [
|
||||
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(config.value.providers, (row, index) => {
|
||||
return (_openBlock(), _createElementBlock("tr", {
|
||||
key: row.id || index
|
||||
}, [
|
||||
_createElementVNode("td", null, [
|
||||
_createVNode(_component_VSwitch, {
|
||||
modelValue: row.enabled,
|
||||
"onUpdate:modelValue": $event => ((row.enabled) = $event),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
density: "compact"
|
||||
}, null, 8, ["modelValue", "onUpdate:modelValue"])
|
||||
]),
|
||||
_createElementVNode("td", null, _toDisplayString(row.priority), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.name), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.provider), 1),
|
||||
_createElementVNode("td", _hoisted_10, _toDisplayString(row.base_url), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(providerRows.value[index]?.masked_api_key || '****'), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.model), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.token_limit > 0 ? formatTokens(row.token_limit) : '不限'), 1),
|
||||
_createElementVNode("td", _hoisted_11, [
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-pencil",
|
||||
size: "small",
|
||||
variant: "text",
|
||||
onClick: $event => (editProvider(index))
|
||||
}, null, 8, ["onClick"]),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-delete",
|
||||
size: "small",
|
||||
variant: "text",
|
||||
color: "error",
|
||||
onClick: $event => (removeProvider(index))
|
||||
}, null, 8, ["onClick"])
|
||||
])
|
||||
]))
|
||||
}), 128)),
|
||||
(!config.value.providers?.length)
|
||||
? (_openBlock(), _createElementBlock("tr", _hoisted_12, [...(_cache[24] || (_cache[24] = [
|
||||
_createElementVNode("td", {
|
||||
colspan: "9",
|
||||
class: "text-center text-medium-emphasis py-8"
|
||||
}, "暂无供应商", -1)
|
||||
]))]))
|
||||
: _createCommentVNode("", true)
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"]),
|
||||
_createVNode(_component_VDialog, {
|
||||
modelValue: showEditor.value,
|
||||
"onUpdate:modelValue": _cache[13] || (_cache[13] = $event => ((showEditor).value = $event)),
|
||||
"max-width": "760"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VCard, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VCardTitle, null, {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(editorIndex.value >= 0 ? '编辑供应商' : '新增供应商'), 1)
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCardText, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VRow, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "8"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.name,
|
||||
"onUpdate:modelValue": _cache[4] || (_cache[4] = $event => ((editedProvider.value.name) = $event)),
|
||||
label: "名称",
|
||||
variant: "outlined",
|
||||
density: "comfortable"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.priority,
|
||||
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => ((editedProvider.value.priority) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "优先级",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VSelect, {
|
||||
modelValue: editedProvider.value.provider,
|
||||
"onUpdate:modelValue": _cache[6] || (_cache[6] = $event => ((editedProvider.value.provider) = $event)),
|
||||
items: providerTypeOptions,
|
||||
label: "类型",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.model,
|
||||
"onUpdate:modelValue": _cache[7] || (_cache[7] = $event => ((editedProvider.value.model) = $event)),
|
||||
label: "模型",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.base_url,
|
||||
"onUpdate:modelValue": _cache[8] || (_cache[8] = $event => ((editedProvider.value.base_url) = $event)),
|
||||
label: "API 地址",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.api_key,
|
||||
"onUpdate:modelValue": _cache[9] || (_cache[9] = $event => ((editedProvider.value.api_key) = $event)),
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.token_limit,
|
||||
"onUpdate:modelValue": _cache[10] || (_cache[10] = $event => ((editedProvider.value.token_limit) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "Token 额度",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.used_tokens,
|
||||
"onUpdate:modelValue": _cache[11] || (_cache[11] = $event => ((editedProvider.value.used_tokens) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "初始已用",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCardActions, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VSpacer),
|
||||
_createVNode(_component_VBtn, {
|
||||
variant: "text",
|
||||
onClick: _cache[12] || (_cache[12] = $event => (showEditor.value = false))
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[26] || (_cache[26] = [
|
||||
_createTextVNode("取消", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VBtn, {
|
||||
color: "primary",
|
||||
onClick: commitProvider
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[27] || (_cache[27] = [
|
||||
_createTextVNode("确定", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const AppPage = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-f70e1bcb"]]);
|
||||
|
||||
export { AppPage as default };
|
||||
130
plugins.v2/agenttokens/dist/assets/__federation_expose_AppPage-DVPoxkMN.js
vendored
Normal file
130
plugins.v2/agenttokens/dist/assets/__federation_expose_AppPage-DVPoxkMN.js
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import { A as AgentTokensManager } from './AgentTokensManager-DnY91SQC.js';
|
||||
import { u as unwrapResponse } from './provider-BURm2Fqi.js';
|
||||
|
||||
const {openBlock:_openBlock,createBlock:_createBlock} = await importShared('vue');
|
||||
|
||||
|
||||
const {computed,onMounted,ref} = await importShared('vue');
|
||||
|
||||
|
||||
const _sfc_main = {
|
||||
__name: 'AppPage',
|
||||
props: {
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
pluginId: {
|
||||
type: String,
|
||||
default: 'AgentTokens',
|
||||
},
|
||||
hideTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(__props, { expose: __expose }) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const error = ref('');
|
||||
const status = ref({
|
||||
config: { enabled: false, show_sidebar_nav: true, providers: [] },
|
||||
providers: [],
|
||||
summary: {},
|
||||
});
|
||||
|
||||
// 构造 API 基础路径。
|
||||
const pluginBase = computed(() => `plugin/${props.pluginId || 'AgentTokens'}`);
|
||||
const config = computed(() => status.value.config || { enabled: false, show_sidebar_nav: true, providers: [] });
|
||||
const providerRows = computed(() => status.value.providers || []);
|
||||
const summary = computed(() => status.value.summary || {});
|
||||
|
||||
// 从插件 API 拉取当前配置和用量状态。
|
||||
async function loadStatus() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const response = await props.api.get(`${pluginBase.value}/status`);
|
||||
status.value = unwrapResponse(response) || status.value;
|
||||
} catch (err) {
|
||||
error.value = err?.message || '加载失败';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存完整插件配置并刷新服务端标准化后的状态。
|
||||
async function saveConfig() {
|
||||
saving.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const payload = {
|
||||
enabled: Boolean(config.value.enabled),
|
||||
show_sidebar_nav: Boolean(config.value.show_sidebar_nav),
|
||||
providers: [...(config.value.providers || [])],
|
||||
};
|
||||
const response = await props.api.post(`${pluginBase.value}/config`, payload);
|
||||
status.value = unwrapResponse(response) || status.value;
|
||||
} catch (err) {
|
||||
error.value = err?.message || '保存失败';
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置指定供应商的运行记录。
|
||||
async function resetUsage(providerId) {
|
||||
if (!providerId) return
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await props.api.post(`${pluginBase.value}/usage/reset`, { provider_id: providerId });
|
||||
status.value = unwrapResponse(response) || status.value;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置全部供应商的运行记录。
|
||||
async function resetAllUsage() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await props.api.post(`${pluginBase.value}/usage/reset_all`, {});
|
||||
status.value = unwrapResponse(response) || status.value;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
__expose({
|
||||
loadStatus,
|
||||
saveConfig,
|
||||
loading,
|
||||
saving,
|
||||
});
|
||||
|
||||
onMounted(loadStatus);
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
return (_openBlock(), _createBlock(AgentTokensManager, {
|
||||
config: config.value,
|
||||
"provider-rows": providerRows.value,
|
||||
summary: summary.value,
|
||||
error: error.value,
|
||||
loading: loading.value,
|
||||
saving: saving.value,
|
||||
"hide-title": __props.hideTitle,
|
||||
onRefresh: loadStatus,
|
||||
onSave: saveConfig,
|
||||
onResetUsage: resetUsage,
|
||||
onResetAllUsage: resetAllUsage
|
||||
}, null, 8, ["config", "provider-rows", "summary", "error", "loading", "saving", "hide-title"]))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { _sfc_main as default };
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
.gap-2[data-v-f70e1bcb] {
|
||||
gap: 8px;
|
||||
}
|
||||
.progress-cell[data-v-f70e1bcb] {
|
||||
min-width: 140px;
|
||||
}
|
||||
.truncate-cell[data-v-f70e1bcb] {
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
.gap-2[data-v-22b1ab53] {
|
||||
gap: 8px;
|
||||
}
|
||||
103
plugins.v2/agenttokens/dist/assets/__federation_expose_Config-C6p4hYpa.js
vendored
Normal file
103
plugins.v2/agenttokens/dist/assets/__federation_expose_Config-C6p4hYpa.js
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import { A as AgentTokensManager } from './AgentTokensManager-DnY91SQC.js';
|
||||
import { c as cloneConfig } from './provider-BURm2Fqi.js';
|
||||
|
||||
const {createElementVNode:_createElementVNode,resolveComponent:_resolveComponent,createVNode:_createVNode,withCtx:_withCtx,openBlock:_openBlock,createElementBlock:_createElementBlock} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1 = { class: "agenttokens-config" };
|
||||
|
||||
const {onMounted,ref} = await importShared('vue');
|
||||
|
||||
|
||||
const _sfc_main = {
|
||||
__name: 'Config',
|
||||
props: {
|
||||
initialConfig: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['save', 'close'],
|
||||
setup(__props, { emit: __emit }) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
const emit = __emit;
|
||||
|
||||
const localConfig = ref({ enabled: false, show_sidebar_nav: true, providers: [] });
|
||||
|
||||
// 重置本地配置中的单个供应商用量。
|
||||
function resetUsage(providerId, index) {
|
||||
const providers = localConfig.value.providers || [];
|
||||
const providerIndex = providers.findIndex(provider => provider.id && provider.id === providerId);
|
||||
const targetIndex = providerIndex >= 0 ? providerIndex : index;
|
||||
if (!providers[targetIndex]) return
|
||||
providers[targetIndex].used_tokens = 0;
|
||||
}
|
||||
|
||||
// 重置本地配置中的全部供应商用量。
|
||||
function resetAllUsage() {
|
||||
(localConfig.value.providers || []).forEach(provider => {
|
||||
provider.used_tokens = 0;
|
||||
});
|
||||
}
|
||||
|
||||
// 通知宿主保存 Vue 配置。
|
||||
function saveConfig() {
|
||||
emit('save', cloneConfig(localConfig.value));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
localConfig.value = cloneConfig(props.initialConfig);
|
||||
if (localConfig.value.show_sidebar_nav === undefined) {
|
||||
localConfig.value.show_sidebar_nav = true;
|
||||
}
|
||||
if (!Array.isArray(localConfig.value.providers)) {
|
||||
localConfig.value.providers = [];
|
||||
}
|
||||
});
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_VSpacer = _resolveComponent("VSpacer");
|
||||
const _component_VBtn = _resolveComponent("VBtn");
|
||||
const _component_VToolbar = _resolveComponent("VToolbar");
|
||||
const _component_VDivider = _resolveComponent("VDivider");
|
||||
|
||||
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
|
||||
_createVNode(_component_VToolbar, {
|
||||
density: "comfortable",
|
||||
color: "transparent"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_cache[1] || (_cache[1] = _createElementVNode("div", { class: "text-h6 ms-3" }, "Agent Tokens 配置", -1)),
|
||||
_createVNode(_component_VSpacer),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-content-save",
|
||||
variant: "text",
|
||||
color: "primary",
|
||||
onClick: saveConfig
|
||||
}),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-close",
|
||||
variant: "text",
|
||||
onClick: _cache[0] || (_cache[0] = $event => (emit('close')))
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VDivider),
|
||||
_createVNode(AgentTokensManager, {
|
||||
config: localConfig.value,
|
||||
"hide-title": "",
|
||||
onSave: saveConfig,
|
||||
onResetUsage: resetUsage,
|
||||
onResetAllUsage: resetAllUsage
|
||||
}, null, 8, ["config"])
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { _sfc_main as default };
|
||||
@@ -1,463 +0,0 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import { _ as _export_sfc } from './_plugin-vue_export-helper-pcqpp-6-.js';
|
||||
|
||||
const {createElementVNode:_createElementVNode,resolveComponent:_resolveComponent,createVNode:_createVNode,withCtx:_withCtx,createTextVNode:_createTextVNode,renderList:_renderList,Fragment:_Fragment,openBlock:_openBlock,createElementBlock:_createElementBlock,toDisplayString:_toDisplayString,createCommentVNode:_createCommentVNode} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1 = { class: "agenttokens-config" };
|
||||
const _hoisted_2 = { class: "pa-4" };
|
||||
const _hoisted_3 = { class: "d-flex align-center mb-4 gap-2 flex-wrap" };
|
||||
const _hoisted_4 = { class: "text-right" };
|
||||
const _hoisted_5 = { key: 0 };
|
||||
const _hoisted_6 = { class: "pa-4 d-flex justify-end" };
|
||||
|
||||
const {onMounted,ref} = await importShared('vue');
|
||||
|
||||
|
||||
|
||||
const _sfc_main = {
|
||||
__name: 'Config',
|
||||
props: {
|
||||
initialConfig: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['save', 'close', 'switch'],
|
||||
setup(__props, { emit: __emit }) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
const emit = __emit;
|
||||
|
||||
const localConfig = ref({ enabled: false, show_sidebar_nav: true, providers: [] });
|
||||
const showEditor = ref(false);
|
||||
const editorIndex = ref(-1);
|
||||
const editedProvider = ref(createProvider());
|
||||
|
||||
const providerTypeOptions = [
|
||||
{ title: 'OpenAI Compatible', value: 'openai' },
|
||||
{ title: 'DeepSeek', value: 'deepseek' },
|
||||
{ title: 'Google Gemini', value: 'google' },
|
||||
{ title: 'Anthropic Compatible', value: 'anthropic' },
|
||||
{ title: 'ChatGPT', value: 'chatgpt' },
|
||||
];
|
||||
|
||||
// 构建一个新的供应商默认配置。
|
||||
function createProvider() {
|
||||
return {
|
||||
id: '',
|
||||
enabled: true,
|
||||
name: '',
|
||||
provider: 'openai',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
model: '',
|
||||
token_limit: 0,
|
||||
used_tokens: 0,
|
||||
priority: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成深拷贝配置,避免直接修改父组件传入对象。
|
||||
function cloneConfig(config) {
|
||||
return JSON.parse(JSON.stringify(config || { enabled: false, show_sidebar_nav: true, providers: [] }))
|
||||
}
|
||||
|
||||
// 格式化 token 数字。
|
||||
function formatTokens(value) {
|
||||
const numberValue = Number(value || 0);
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : '0'
|
||||
}
|
||||
|
||||
// 打开新增供应商弹窗。
|
||||
function addProvider() {
|
||||
const nextPriority = Math.max(0, ...(localConfig.value.providers || []).map(item => Number(item.priority || 0))) + 1;
|
||||
editedProvider.value = { ...createProvider(), priority: nextPriority };
|
||||
editorIndex.value = -1;
|
||||
showEditor.value = true;
|
||||
}
|
||||
|
||||
// 打开编辑供应商弹窗。
|
||||
function editProvider(index) {
|
||||
editedProvider.value = { ...localConfig.value.providers[index] };
|
||||
editorIndex.value = index;
|
||||
showEditor.value = true;
|
||||
}
|
||||
|
||||
// 将弹窗中的供应商写回本地配置。
|
||||
function commitProvider() {
|
||||
const providers = [...(localConfig.value.providers || [])];
|
||||
const provider = {
|
||||
...editedProvider.value,
|
||||
token_limit: Number(editedProvider.value.token_limit || 0),
|
||||
used_tokens: Number(editedProvider.value.used_tokens || 0),
|
||||
priority: Number(editedProvider.value.priority || providers.length + 1),
|
||||
};
|
||||
if (editorIndex.value >= 0) {
|
||||
providers.splice(editorIndex.value, 1, provider);
|
||||
} else {
|
||||
providers.push(provider);
|
||||
}
|
||||
localConfig.value.providers = providers;
|
||||
showEditor.value = false;
|
||||
}
|
||||
|
||||
// 移除一个供应商配置。
|
||||
function removeProvider(index) {
|
||||
const providers = [...(localConfig.value.providers || [])];
|
||||
providers.splice(index, 1);
|
||||
localConfig.value.providers = providers;
|
||||
}
|
||||
|
||||
// 通知宿主保存 Vue 配置。
|
||||
function saveConfig() {
|
||||
emit('save', cloneConfig(localConfig.value));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
localConfig.value = cloneConfig(props.initialConfig);
|
||||
if (localConfig.value.show_sidebar_nav === undefined) {
|
||||
localConfig.value.show_sidebar_nav = true;
|
||||
}
|
||||
if (!Array.isArray(localConfig.value.providers)) {
|
||||
localConfig.value.providers = [];
|
||||
}
|
||||
});
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_VSpacer = _resolveComponent("VSpacer");
|
||||
const _component_VBtn = _resolveComponent("VBtn");
|
||||
const _component_VToolbar = _resolveComponent("VToolbar");
|
||||
const _component_VDivider = _resolveComponent("VDivider");
|
||||
const _component_VSwitch = _resolveComponent("VSwitch");
|
||||
const _component_VTable = _resolveComponent("VTable");
|
||||
const _component_VSheet = _resolveComponent("VSheet");
|
||||
const _component_VCardTitle = _resolveComponent("VCardTitle");
|
||||
const _component_VTextField = _resolveComponent("VTextField");
|
||||
const _component_VCol = _resolveComponent("VCol");
|
||||
const _component_VSelect = _resolveComponent("VSelect");
|
||||
const _component_VRow = _resolveComponent("VRow");
|
||||
const _component_VCardText = _resolveComponent("VCardText");
|
||||
const _component_VCardActions = _resolveComponent("VCardActions");
|
||||
const _component_VCard = _resolveComponent("VCard");
|
||||
const _component_VDialog = _resolveComponent("VDialog");
|
||||
|
||||
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
|
||||
_createVNode(_component_VToolbar, {
|
||||
density: "comfortable",
|
||||
color: "transparent"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_cache[14] || (_cache[14] = _createElementVNode("div", { class: "text-h6 ms-3" }, "Agent Tokens 配置", -1)),
|
||||
_createVNode(_component_VSpacer),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-close",
|
||||
variant: "text",
|
||||
onClick: _cache[0] || (_cache[0] = $event => (emit('close')))
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VDivider),
|
||||
_createElementVNode("div", _hoisted_2, [
|
||||
_createElementVNode("div", _hoisted_3, [
|
||||
_createVNode(_component_VSwitch, {
|
||||
modelValue: localConfig.value.enabled,
|
||||
"onUpdate:modelValue": _cache[1] || (_cache[1] = $event => ((localConfig.value.enabled) = $event)),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
inset: "",
|
||||
label: "启用插件"
|
||||
}, null, 8, ["modelValue"]),
|
||||
_createVNode(_component_VSwitch, {
|
||||
modelValue: localConfig.value.show_sidebar_nav,
|
||||
"onUpdate:modelValue": _cache[2] || (_cache[2] = $event => ((localConfig.value.show_sidebar_nav) = $event)),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
inset: "",
|
||||
label: "显示侧边栏入口"
|
||||
}, null, 8, ["modelValue"]),
|
||||
_createVNode(_component_VSpacer),
|
||||
_createVNode(_component_VBtn, {
|
||||
"prepend-icon": "mdi-database-eye",
|
||||
variant: "tonal",
|
||||
onClick: _cache[3] || (_cache[3] = $event => (emit('switch')))
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[15] || (_cache[15] = [
|
||||
_createTextVNode("用量", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VBtn, {
|
||||
"prepend-icon": "mdi-plus",
|
||||
color: "primary",
|
||||
variant: "tonal",
|
||||
onClick: addProvider
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[16] || (_cache[16] = [
|
||||
_createTextVNode("新增", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_createVNode(_component_VSheet, {
|
||||
border: "",
|
||||
rounded: ""
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTable, { density: "comfortable" }, {
|
||||
default: _withCtx(() => [
|
||||
_cache[18] || (_cache[18] = _createElementVNode("thead", null, [
|
||||
_createElementVNode("tr", null, [
|
||||
_createElementVNode("th", null, "启用"),
|
||||
_createElementVNode("th", null, "优先级"),
|
||||
_createElementVNode("th", null, "名称"),
|
||||
_createElementVNode("th", null, "类型"),
|
||||
_createElementVNode("th", null, "模型"),
|
||||
_createElementVNode("th", null, "额度"),
|
||||
_createElementVNode("th", { class: "text-right" }, "操作")
|
||||
])
|
||||
], -1)),
|
||||
_createElementVNode("tbody", null, [
|
||||
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(localConfig.value.providers, (row, index) => {
|
||||
return (_openBlock(), _createElementBlock("tr", {
|
||||
key: row.id || index
|
||||
}, [
|
||||
_createElementVNode("td", null, [
|
||||
_createVNode(_component_VSwitch, {
|
||||
modelValue: row.enabled,
|
||||
"onUpdate:modelValue": $event => ((row.enabled) = $event),
|
||||
color: "primary",
|
||||
"hide-details": "",
|
||||
density: "compact"
|
||||
}, null, 8, ["modelValue", "onUpdate:modelValue"])
|
||||
]),
|
||||
_createElementVNode("td", null, _toDisplayString(row.priority), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.name), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.provider), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.model), 1),
|
||||
_createElementVNode("td", null, _toDisplayString(row.token_limit > 0 ? formatTokens(row.token_limit) : '不限'), 1),
|
||||
_createElementVNode("td", _hoisted_4, [
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-pencil",
|
||||
size: "small",
|
||||
variant: "text",
|
||||
onClick: $event => (editProvider(index))
|
||||
}, null, 8, ["onClick"]),
|
||||
_createVNode(_component_VBtn, {
|
||||
icon: "mdi-delete",
|
||||
size: "small",
|
||||
variant: "text",
|
||||
color: "error",
|
||||
onClick: $event => (removeProvider(index))
|
||||
}, null, 8, ["onClick"])
|
||||
])
|
||||
]))
|
||||
}), 128)),
|
||||
(!localConfig.value.providers.length)
|
||||
? (_openBlock(), _createElementBlock("tr", _hoisted_5, [...(_cache[17] || (_cache[17] = [
|
||||
_createElementVNode("td", {
|
||||
colspan: "7",
|
||||
class: "text-center text-medium-emphasis py-8"
|
||||
}, "暂无供应商", -1)
|
||||
]))]))
|
||||
: _createCommentVNode("", true)
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_createVNode(_component_VDivider),
|
||||
_createElementVNode("div", _hoisted_6, [
|
||||
_createVNode(_component_VBtn, {
|
||||
"prepend-icon": "mdi-content-save",
|
||||
color: "primary",
|
||||
onClick: saveConfig
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[19] || (_cache[19] = [
|
||||
_createTextVNode("保存", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_createVNode(_component_VDialog, {
|
||||
modelValue: showEditor.value,
|
||||
"onUpdate:modelValue": _cache[13] || (_cache[13] = $event => ((showEditor).value = $event)),
|
||||
"max-width": "760"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VCard, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VCardTitle, null, {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(editorIndex.value >= 0 ? '编辑供应商' : '新增供应商'), 1)
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCardText, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VRow, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "8"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.name,
|
||||
"onUpdate:modelValue": _cache[4] || (_cache[4] = $event => ((editedProvider.value.name) = $event)),
|
||||
label: "名称",
|
||||
variant: "outlined",
|
||||
density: "comfortable"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.priority,
|
||||
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => ((editedProvider.value.priority) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "优先级",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VSelect, {
|
||||
modelValue: editedProvider.value.provider,
|
||||
"onUpdate:modelValue": _cache[6] || (_cache[6] = $event => ((editedProvider.value.provider) = $event)),
|
||||
items: providerTypeOptions,
|
||||
label: "类型",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.model,
|
||||
"onUpdate:modelValue": _cache[7] || (_cache[7] = $event => ((editedProvider.value.model) = $event)),
|
||||
label: "模型",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.base_url,
|
||||
"onUpdate:modelValue": _cache[8] || (_cache[8] = $event => ((editedProvider.value.base_url) = $event)),
|
||||
label: "API 地址",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.api_key,
|
||||
"onUpdate:modelValue": _cache[9] || (_cache[9] = $event => ((editedProvider.value.api_key) = $event)),
|
||||
label: "API Key",
|
||||
type: "password",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.token_limit,
|
||||
"onUpdate:modelValue": _cache[10] || (_cache[10] = $event => ((editedProvider.value.token_limit) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "Token 额度",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCol, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VTextField, {
|
||||
modelValue: editedProvider.value.used_tokens,
|
||||
"onUpdate:modelValue": _cache[11] || (_cache[11] = $event => ((editedProvider.value.used_tokens) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "初始已用",
|
||||
type: "number",
|
||||
variant: "outlined"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VCardActions, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_VSpacer),
|
||||
_createVNode(_component_VBtn, {
|
||||
variant: "text",
|
||||
onClick: _cache[12] || (_cache[12] = $event => (showEditor.value = false))
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[20] || (_cache[20] = [
|
||||
_createTextVNode("取消", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VBtn, {
|
||||
color: "primary",
|
||||
onClick: commitProvider
|
||||
}, {
|
||||
default: _withCtx(() => [...(_cache[21] || (_cache[21] = [
|
||||
_createTextVNode("确定", -1)
|
||||
]))]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const Config = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-22b1ab53"]]);
|
||||
|
||||
export { Config as default };
|
||||
@@ -1,6 +1,7 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import { f as formatTokens, u as unwrapResponse } from './provider-BURm2Fqi.js';
|
||||
|
||||
const {createElementVNode:_createElementVNode,toDisplayString:_toDisplayString,resolveComponent:_resolveComponent,createVNode:_createVNode,renderList:_renderList,Fragment:_Fragment,openBlock:_openBlock,createElementBlock:_createElementBlock,createTextVNode:_createTextVNode,withCtx:_withCtx,createBlock:_createBlock} = await importShared('vue');
|
||||
const {createElementVNode:_createElementVNode,toDisplayString:_toDisplayString,resolveComponent:_resolveComponent,createVNode:_createVNode,unref:_unref,renderList:_renderList,Fragment:_Fragment,openBlock:_openBlock,createElementBlock:_createElementBlock,createTextVNode:_createTextVNode,withCtx:_withCtx,createBlock:_createBlock} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1 = { class: "agenttokens-dashboard" };
|
||||
@@ -12,7 +13,6 @@ const _hoisted_5 = { class: "text-caption" };
|
||||
const {computed,onMounted,onUnmounted,ref} = await importShared('vue');
|
||||
|
||||
|
||||
|
||||
const _sfc_main = {
|
||||
__name: 'Dashboard',
|
||||
props: {
|
||||
@@ -36,20 +36,6 @@ let timer = null;
|
||||
const summary = computed(() => status.value.summary || {});
|
||||
const providers = computed(() => status.value.providers || []);
|
||||
|
||||
// 兼容 MoviePilot API 包装器和原始响应两种返回形态。
|
||||
function unwrapResponse(response) {
|
||||
if (response && Object.prototype.hasOwnProperty.call(response, 'data') && response.success !== undefined) {
|
||||
return response.data
|
||||
}
|
||||
return response?.data ?? response
|
||||
}
|
||||
|
||||
// 格式化 token 数字。
|
||||
function formatTokens(value) {
|
||||
const numberValue = Number(value || 0);
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : '0'
|
||||
}
|
||||
|
||||
// 读取仪表板所需的精简状态。
|
||||
async function loadStatus() {
|
||||
if (!props.allowRefresh) return
|
||||
@@ -103,7 +89,7 @@ return (_ctx, _cache) => {
|
||||
rounded: "",
|
||||
class: "mb-3"
|
||||
}, null, 8, ["model-value"]),
|
||||
_createElementVNode("div", _hoisted_4, _toDisplayString(formatTokens(summary.value.total_used)) + " / " + _toDisplayString(summary.value.total_limit ? formatTokens(summary.value.total_limit) : '不限'), 1),
|
||||
_createElementVNode("div", _hoisted_4, _toDisplayString(_unref(formatTokens)(summary.value.total_used)) + " / " + _toDisplayString(summary.value.total_limit ? _unref(formatTokens)(summary.value.total_limit) : '不限'), 1),
|
||||
_createVNode(_component_VList, {
|
||||
density: "compact",
|
||||
class: "py-0"
|
||||
@@ -127,7 +113,7 @@ return (_ctx, _cache) => {
|
||||
}, 1032, ["color"])
|
||||
]),
|
||||
append: _withCtx(() => [
|
||||
_createElementVNode("span", _hoisted_5, _toDisplayString(formatTokens(row.usage?.total_tokens)), 1)
|
||||
_createElementVNode("span", _hoisted_5, _toDisplayString(_unref(formatTokens)(row.usage?.total_tokens)), 1)
|
||||
]),
|
||||
_: 2
|
||||
}, 1032, ["title", "subtitle"]))
|
||||
@@ -1,6 +1,6 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import AppPage from './__federation_expose_AppPage-B7K0b9vH.js';
|
||||
import { _ as _export_sfc } from './_plugin-vue_export-helper-pcqpp-6-.js';
|
||||
import _sfc_main$1 from './__federation_expose_AppPage-DVPoxkMN.js';
|
||||
import { _ as _export_sfc } from './AgentTokensManager-DnY91SQC.js';
|
||||
|
||||
const {createElementVNode:_createElementVNode,resolveComponent:_resolveComponent,createVNode:_createVNode,withCtx:_withCtx,openBlock:_openBlock,createElementBlock:_createElementBlock} = await importShared('vue');
|
||||
|
||||
@@ -62,7 +62,7 @@ return (_ctx, _cache) => {
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_VDivider),
|
||||
_createVNode(AppPage, {
|
||||
_createVNode(_sfc_main$1, {
|
||||
ref_key: "pageRef",
|
||||
ref: pageRef,
|
||||
api: __props.api,
|
||||
@@ -1,9 +0,0 @@
|
||||
const _export_sfc = (sfc, props) => {
|
||||
const target = sfc.__vccOpts || sfc;
|
||||
for (const [key, val] of props) {
|
||||
target[key] = val;
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
export { _export_sfc as _ };
|
||||
@@ -1,5 +1,5 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import AppPage from './__federation_expose_AppPage-B7K0b9vH.js';
|
||||
import _sfc_main from './__federation_expose_AppPage-DVPoxkMN.js';
|
||||
|
||||
true&&(function polyfill() {
|
||||
const relList = document.createElement("link").relList;
|
||||
@@ -41,4 +41,4 @@ true&&(function polyfill() {
|
||||
|
||||
const {createApp} = await importShared('vue');
|
||||
|
||||
createApp(AppPage).mount('#app');
|
||||
createApp(_sfc_main).mount('#app');
|
||||
102
plugins.v2/agenttokens/dist/assets/provider-BURm2Fqi.js
vendored
Normal file
102
plugins.v2/agenttokens/dist/assets/provider-BURm2Fqi.js
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
const PROVIDER_TYPE_OPTIONS = [
|
||||
{ title: 'OpenAI Compatible', value: 'openai' },
|
||||
{ title: 'DeepSeek', value: 'deepseek' },
|
||||
{ title: 'Google Gemini', value: 'google' },
|
||||
{ title: 'Anthropic Compatible', value: 'anthropic' },
|
||||
{ title: 'ChatGPT', value: 'chatgpt' },
|
||||
];
|
||||
|
||||
// 构建一个新的供应商默认配置。
|
||||
function createProvider() {
|
||||
return {
|
||||
id: '',
|
||||
enabled: true,
|
||||
name: '',
|
||||
provider: 'openai',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
user_agent: '',
|
||||
model: '',
|
||||
token_limit: 0,
|
||||
used_tokens: 0,
|
||||
priority: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成深拷贝配置,避免直接修改父组件传入对象。
|
||||
function cloneConfig(config) {
|
||||
return JSON.parse(JSON.stringify(config || { enabled: false, show_sidebar_nav: true, providers: [] }))
|
||||
}
|
||||
|
||||
// 格式化 token 数字,保持表格和统计展示可读。
|
||||
function formatTokens(value) {
|
||||
const numberValue = Number(value || 0);
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : '0'
|
||||
}
|
||||
|
||||
// 兼容 MoviePilot API 包装器和原始响应两种返回形态。
|
||||
function unwrapResponse(response) {
|
||||
if (response && Object.prototype.hasOwnProperty.call(response, 'data') && response.success !== undefined) {
|
||||
return response.data
|
||||
}
|
||||
return response?.data ?? response
|
||||
}
|
||||
|
||||
// 计算新增供应商的下一个优先级。
|
||||
function getNextProviderPriority(providers) {
|
||||
return Math.max(0, ...(providers || []).map(item => Number(item.priority || 0))) + 1
|
||||
}
|
||||
|
||||
// 标准化弹窗中写回的供应商数值字段。
|
||||
function normalizeProvider(provider, fallbackPriority) {
|
||||
return {
|
||||
...provider,
|
||||
token_limit: Number(provider.token_limit || 0),
|
||||
used_tokens: Number(provider.used_tokens || 0),
|
||||
priority: Number(provider.priority || fallbackPriority),
|
||||
}
|
||||
}
|
||||
|
||||
// 按配置生成本地用量行,供配置弹窗复用管理页展示结构。
|
||||
function buildProviderRow(provider) {
|
||||
const tokenLimit = Number(provider.token_limit || 0);
|
||||
const totalTokens = Number(provider.used_tokens || 0);
|
||||
const remainingTokens = tokenLimit <= 0 ? null : Math.max(tokenLimit - totalTokens, 0);
|
||||
const usagePercent = tokenLimit <= 0 ? 0 : Math.min((totalTokens * 100) / tokenLimit, 100);
|
||||
|
||||
return {
|
||||
...provider,
|
||||
masked_api_key: provider.api_key ? '****' : '',
|
||||
usage: {
|
||||
total_tokens: totalTokens,
|
||||
remaining_tokens: remainingTokens,
|
||||
usage_percent: usagePercent,
|
||||
exhausted: tokenLimit > 0 && remainingTokens === 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 批量生成本地供应商用量行。
|
||||
function buildProviderRows(providers) {
|
||||
return (providers || []).map(provider => buildProviderRow(provider))
|
||||
}
|
||||
|
||||
// 根据供应商行汇总用量统计。
|
||||
function buildProviderSummary(rows) {
|
||||
const providers = rows || [];
|
||||
const enabledRows = providers.filter(row => row.enabled);
|
||||
const totalUsed = providers.reduce((sum, row) => sum + Number(row.usage?.total_tokens || row.used_tokens || 0), 0);
|
||||
const totalLimit = providers.reduce((sum, row) => {
|
||||
const tokenLimit = Number(row.token_limit || 0);
|
||||
return tokenLimit > 0 ? sum + tokenLimit : sum
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
available_count: enabledRows.filter(row => !row.usage?.exhausted && row.api_key && row.base_url && row.model).length,
|
||||
enabled_count: enabledRows.length,
|
||||
total_used: totalUsed,
|
||||
total_limit: totalLimit,
|
||||
}
|
||||
}
|
||||
|
||||
export { PROVIDER_TYPE_OPTIONS as P, buildProviderSummary as a, buildProviderRows as b, cloneConfig as c, createProvider as d, formatTokens as f, getNextProviderPriority as g, normalizeProvider as n, unwrapResponse as u };
|
||||
@@ -2,17 +2,17 @@ const currentImports = {};
|
||||
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
|
||||
let moduleMap = {
|
||||
"./Page":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Page-vwwFlnk-.css","__federation_expose_AppPage-oQOrnVay.css"], false, './Page');
|
||||
return __federation_import('./__federation_expose_Page-P8w_qF-9.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
dynamicLoadingCss(["__federation_expose_Page-vwwFlnk-.css","AgentTokensManager-BJe0fhEr.css"], false, './Page');
|
||||
return __federation_import('./__federation_expose_Page-MLoSpL20.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./Config":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Config-C5yY6NwA.css"], false, './Config');
|
||||
return __federation_import('./__federation_expose_Config-C7PQoNCG.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
dynamicLoadingCss(["AgentTokensManager-BJe0fhEr.css"], false, './Config');
|
||||
return __federation_import('./__federation_expose_Config-C6p4hYpa.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./Dashboard":()=>{
|
||||
dynamicLoadingCss([], false, './Dashboard');
|
||||
return __federation_import('./__federation_expose_Dashboard-BPSul9jL.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
return __federation_import('./__federation_expose_Dashboard-Ch2BuVKu.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./AppPage":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_AppPage-oQOrnVay.css"], false, './AppPage');
|
||||
return __federation_import('./__federation_expose_AppPage-B7K0b9vH.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};
|
||||
dynamicLoadingCss(["AgentTokensManager-BJe0fhEr.css"], false, './AppPage');
|
||||
return __federation_import('./__federation_expose_AppPage-DVPoxkMN.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};
|
||||
const seen = {};
|
||||
const dynamicLoadingCss = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
|
||||
const metaUrl = import.meta.url;
|
||||
|
||||
9
plugins.v2/agenttokens/dist/index.html
vendored
9
plugins.v2/agenttokens/dist/index.html
vendored
@@ -1,6 +1,7 @@
|
||||
<script type="module" crossorigin src="/assets/index-C9EvEaTb.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D6dGOibj.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/__federation_fn_import-JrT3xvdd.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-pcqpp-6-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/__federation_expose_AppPage-B7K0b9vH.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/__federation_expose_AppPage-oQOrnVay.css">
|
||||
<link rel="modulepreload" crossorigin href="/assets/provider-BURm2Fqi.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/AgentTokensManager-DnY91SQC.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/__federation_expose_AppPage-DVPoxkMN.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/AgentTokensManager-BJe0fhEr.css">
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "moviepilot-agenttokens-plugin",
|
||||
"private": true,
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
|
||||
299
plugins.v2/agenttokens/src/components/AgentTokensManager.vue
Normal file
299
plugins.v2/agenttokens/src/components/AgentTokensManager.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import ProviderConfigTable from './ProviderConfigTable.vue'
|
||||
import ProviderEditorDialog from './ProviderEditorDialog.vue'
|
||||
import ProviderUsageTable from './ProviderUsageTable.vue'
|
||||
import UsageOverviewCard from './UsageOverviewCard.vue'
|
||||
import {
|
||||
buildProviderRows,
|
||||
buildProviderSummary,
|
||||
createProvider,
|
||||
formatTokens,
|
||||
getNextProviderPriority,
|
||||
normalizeProvider,
|
||||
} from '../provider'
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({ enabled: false, show_sidebar_nav: true, providers: [] }),
|
||||
},
|
||||
providerRows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
summary: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
saving: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['refresh', 'save', 'reset-usage', 'reset-all-usage'])
|
||||
|
||||
const activeTab = ref('usage')
|
||||
const showEditor = ref(false)
|
||||
const editorIndex = ref(-1)
|
||||
const editedProvider = ref(createProvider())
|
||||
|
||||
const configValue = computed(() => props.config || { enabled: false, show_sidebar_nav: true, providers: [] })
|
||||
const providers = computed(() => (Array.isArray(configValue.value.providers) ? configValue.value.providers : []))
|
||||
const displayProviderRows = computed(() => (
|
||||
props.providerRows.length ? props.providerRows : buildProviderRows(providers.value)
|
||||
))
|
||||
const displaySummary = computed(() => (
|
||||
Object.keys(props.summary || {}).length ? props.summary : buildProviderSummary(displayProviderRows.value)
|
||||
))
|
||||
|
||||
// 打开新增供应商弹窗。
|
||||
function addProvider() {
|
||||
editedProvider.value = { ...createProvider(), priority: getNextProviderPriority(providers.value) }
|
||||
editorIndex.value = -1
|
||||
showEditor.value = true
|
||||
}
|
||||
|
||||
// 打开编辑供应商弹窗。
|
||||
function editProvider(index) {
|
||||
editedProvider.value = { ...providers.value[index] }
|
||||
editorIndex.value = index
|
||||
showEditor.value = true
|
||||
}
|
||||
|
||||
// 将弹窗中的供应商写回配置列表。
|
||||
function commitProvider() {
|
||||
const nextProviders = [...providers.value]
|
||||
const normalized = normalizeProvider(editedProvider.value, nextProviders.length + 1)
|
||||
if (editorIndex.value >= 0) {
|
||||
nextProviders.splice(editorIndex.value, 1, normalized)
|
||||
} else {
|
||||
nextProviders.push(normalized)
|
||||
}
|
||||
configValue.value.providers = nextProviders
|
||||
showEditor.value = false
|
||||
}
|
||||
|
||||
// 从配置列表中移除一个供应商。
|
||||
function removeProvider(index) {
|
||||
const nextProviders = [...providers.value]
|
||||
nextProviders.splice(index, 1)
|
||||
configValue.value.providers = nextProviders
|
||||
}
|
||||
|
||||
// 请求重置单个供应商用量。
|
||||
function resetUsage(providerId, index) {
|
||||
emit('reset-usage', providerId, index)
|
||||
}
|
||||
|
||||
// 请求重置全部供应商用量。
|
||||
function resetAllUsage() {
|
||||
emit('reset-all-usage')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agenttokens-page">
|
||||
<div v-if="!hideTitle" class="agenttokens-header">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-100 truncate sm:text-3xl sm:leading-9">
|
||||
<span class="text-moviepilot">Agent Tokens 管理</span>
|
||||
</h2>
|
||||
<VSpacer />
|
||||
<VBtn icon="mdi-refresh" variant="text" :loading="loading" @click="emit('refresh')" />
|
||||
<VBtn icon="mdi-content-save" variant="text" color="primary" :loading="saving" @click="emit('save')" />
|
||||
</div>
|
||||
|
||||
<VAlert v-if="error" type="error" variant="tonal" class="mb-4">{{ error }}</VAlert>
|
||||
|
||||
<VSheet border rounded class="agenttokens-control-panel">
|
||||
<div class="agenttokens-control-panel__switches">
|
||||
<VSwitch v-model="configValue.enabled" color="primary" hide-details inset label="启用插件" />
|
||||
<VSwitch v-model="configValue.show_sidebar_nav" color="primary" hide-details inset label="侧边栏入口" />
|
||||
</div>
|
||||
</VSheet>
|
||||
|
||||
<div class="agenttokens-overview-grid">
|
||||
<UsageOverviewCard class="agenttokens-overview-card" :summary="displaySummary" />
|
||||
<VSheet border rounded class="agenttokens-stat-card">
|
||||
<VIcon icon="mdi-check-decagram-outline" color="success" />
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">可用供应商</div>
|
||||
<div class="agenttokens-stat-card__value">
|
||||
{{ displaySummary.available_count || 0 }} / {{ displaySummary.enabled_count || 0 }}
|
||||
</div>
|
||||
</div>
|
||||
</VSheet>
|
||||
<VSheet border rounded class="agenttokens-stat-card">
|
||||
<VIcon icon="mdi-chart-timeline-variant" color="primary" />
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">累计使用</div>
|
||||
<div class="agenttokens-stat-card__value">{{ formatTokens(displaySummary.total_used) }}</div>
|
||||
</div>
|
||||
</VSheet>
|
||||
<VSheet border rounded class="agenttokens-stat-card">
|
||||
<VIcon icon="mdi-database-outline" color="info" />
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">总额度</div>
|
||||
<div class="agenttokens-stat-card__value">
|
||||
{{ displaySummary.total_limit ? formatTokens(displaySummary.total_limit) : '不限' }}
|
||||
</div>
|
||||
</div>
|
||||
</VSheet>
|
||||
</div>
|
||||
|
||||
<VSheet border rounded class="agenttokens-content-panel">
|
||||
<div class="agenttokens-tabs-row">
|
||||
<VTabs v-model="activeTab" density="comfortable">
|
||||
<VTab value="usage">用量</VTab>
|
||||
<VTab value="config">配置</VTab>
|
||||
</VTabs>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VWindow v-model="activeTab" :touch="false" class="agenttokens-window">
|
||||
<VWindowItem value="usage">
|
||||
<ProviderUsageTable :provider-rows="displayProviderRows" @reset="resetUsage" />
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="config">
|
||||
<div class="agenttokens-table-actions">
|
||||
<VBtn prepend-icon="mdi-plus" color="primary" variant="tonal" @click="addProvider">新增</VBtn>
|
||||
<VBtn prepend-icon="mdi-backup-restore" color="warning" variant="tonal" @click="resetAllUsage">
|
||||
重置用量
|
||||
</VBtn>
|
||||
</div>
|
||||
<ProviderConfigTable
|
||||
:providers="providers"
|
||||
:provider-rows="displayProviderRows"
|
||||
show-credentials
|
||||
@edit="editProvider"
|
||||
@remove="removeProvider"
|
||||
/>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VSheet>
|
||||
|
||||
<ProviderEditorDialog
|
||||
v-model="showEditor"
|
||||
:provider="editedProvider"
|
||||
:editor-index="editorIndex"
|
||||
@commit="commitProvider"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agenttokens-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.agenttokens-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agenttokens-control-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.agenttokens-control-panel__switches {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 20px;
|
||||
}
|
||||
|
||||
.agenttokens-overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) repeat(3, minmax(10rem, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agenttokens-overview-card {
|
||||
min-block-size: 172px;
|
||||
}
|
||||
|
||||
.agenttokens-stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-block-size: 104px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.agenttokens-stat-card__value {
|
||||
margin-block-start: 2px;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.agenttokens-content-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agenttokens-tabs-row {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.agenttokens-window {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.agenttokens-table-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-block-end: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.agenttokens-overview-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.agenttokens-overview-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.agenttokens-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.agenttokens-table-actions > :deep(.v-btn) {
|
||||
flex: 1 1 10rem;
|
||||
}
|
||||
|
||||
.agenttokens-overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.agenttokens-stat-card {
|
||||
min-block-size: 88px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import AgentTokensManager from './AgentTokensManager.vue'
|
||||
import { unwrapResponse } from '../provider'
|
||||
|
||||
const props = defineProps({
|
||||
api: {
|
||||
@@ -19,76 +21,18 @@ const props = defineProps({
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
const activeTab = ref('usage')
|
||||
const showEditor = ref(false)
|
||||
const editorIndex = ref(-1)
|
||||
const editedProvider = ref(createProvider())
|
||||
const status = ref({
|
||||
config: { enabled: false, providers: [] },
|
||||
config: { enabled: false, show_sidebar_nav: true, providers: [] },
|
||||
providers: [],
|
||||
summary: {},
|
||||
})
|
||||
|
||||
// 构造 API 基础路径。
|
||||
const pluginBase = computed(() => `plugin/${props.pluginId || 'AgentTokens'}`)
|
||||
const config = computed(() => status.value.config || { enabled: false, providers: [] })
|
||||
const config = computed(() => status.value.config || { enabled: false, show_sidebar_nav: true, providers: [] })
|
||||
const providerRows = computed(() => status.value.providers || [])
|
||||
const summary = computed(() => status.value.summary || {})
|
||||
|
||||
const providerTypeOptions = [
|
||||
{ title: 'OpenAI Compatible', value: 'openai' },
|
||||
{ title: 'DeepSeek', value: 'deepseek' },
|
||||
{ title: 'Google Gemini', value: 'google' },
|
||||
{ title: 'Anthropic Compatible', value: 'anthropic' },
|
||||
{ title: 'ChatGPT', value: 'chatgpt' },
|
||||
]
|
||||
|
||||
// 构建一个新的供应商默认配置。
|
||||
function createProvider() {
|
||||
return {
|
||||
id: '',
|
||||
enabled: true,
|
||||
name: '',
|
||||
provider: 'openai',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
model: '',
|
||||
token_limit: 0,
|
||||
used_tokens: 0,
|
||||
priority: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容 MoviePilot API 包装器和原始响应两种返回形态。
|
||||
function unwrapResponse(response) {
|
||||
if (response && Object.prototype.hasOwnProperty.call(response, 'data') && response.success !== undefined) {
|
||||
return response.data
|
||||
}
|
||||
return response?.data ?? response
|
||||
}
|
||||
|
||||
// 格式化 token 数字,保持表格紧凑可读。
|
||||
function formatTokens(value) {
|
||||
const numberValue = Number(value || 0)
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : '0'
|
||||
}
|
||||
|
||||
// 根据供应商状态返回 Vuetify 颜色。
|
||||
function rowStatusColor(row) {
|
||||
if (!row.enabled) return 'default'
|
||||
if (row.usage?.exhausted) return 'error'
|
||||
if (!row.api_key || !row.base_url || !row.model) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 根据供应商状态返回短标签。
|
||||
function rowStatusText(row) {
|
||||
if (!row.enabled) return '停用'
|
||||
if (row.usage?.exhausted) return '耗尽'
|
||||
if (!row.api_key || !row.base_url || !row.model) return '缺配置'
|
||||
return '可用'
|
||||
}
|
||||
|
||||
// 从插件 API 拉取当前配置和用量状态。
|
||||
async function loadStatus() {
|
||||
loading.value = true
|
||||
@@ -122,46 +66,6 @@ async function saveConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// 打开新增供应商弹窗。
|
||||
function addProvider() {
|
||||
const nextPriority = Math.max(0, ...(config.value.providers || []).map(item => Number(item.priority || 0))) + 1
|
||||
editedProvider.value = { ...createProvider(), priority: nextPriority }
|
||||
editorIndex.value = -1
|
||||
showEditor.value = true
|
||||
}
|
||||
|
||||
// 打开编辑供应商弹窗。
|
||||
function editProvider(index) {
|
||||
editedProvider.value = { ...config.value.providers[index] }
|
||||
editorIndex.value = index
|
||||
showEditor.value = true
|
||||
}
|
||||
|
||||
// 将弹窗中的供应商写回配置列表。
|
||||
function commitProvider() {
|
||||
const providers = [...(config.value.providers || [])]
|
||||
const normalized = {
|
||||
...editedProvider.value,
|
||||
token_limit: Number(editedProvider.value.token_limit || 0),
|
||||
used_tokens: Number(editedProvider.value.used_tokens || 0),
|
||||
priority: Number(editedProvider.value.priority || providers.length + 1),
|
||||
}
|
||||
if (editorIndex.value >= 0) {
|
||||
providers.splice(editorIndex.value, 1, normalized)
|
||||
} else {
|
||||
providers.push(normalized)
|
||||
}
|
||||
status.value.config = { ...config.value, providers }
|
||||
showEditor.value = false
|
||||
}
|
||||
|
||||
// 从配置列表中移除一个供应商。
|
||||
function removeProvider(index) {
|
||||
const providers = [...(config.value.providers || [])]
|
||||
providers.splice(index, 1)
|
||||
status.value.config = { ...config.value, providers }
|
||||
}
|
||||
|
||||
// 重置指定供应商的运行记录。
|
||||
async function resetUsage(providerId) {
|
||||
if (!providerId) return
|
||||
@@ -196,206 +100,17 @@ onMounted(loadStatus)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agenttokens-page pa-4">
|
||||
<div v-if="!hideTitle" class="d-flex align-center gap-2 mb-4 flex-nowrap">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-100 truncate sm:text-3xl sm:leading-9">
|
||||
<span class="text-moviepilot">Agent Tokens 管理</span>
|
||||
</h2>
|
||||
<VSpacer />
|
||||
<VBtn icon="mdi-refresh" variant="text" :loading="loading" @click="loadStatus" />
|
||||
<VBtn icon="mdi-content-save" variant="text" color="primary" :loading="saving" @click="saveConfig" />
|
||||
</div>
|
||||
|
||||
<VAlert v-if="error" type="error" variant="tonal" class="mb-4">{{ error }}</VAlert>
|
||||
|
||||
<VRow class="mb-4">
|
||||
<VCol cols="12" sm="auto">
|
||||
<VSwitch v-if="status.config" v-model="status.config.enabled" color="primary" hide-details inset label="启用插件" />
|
||||
</VCol>
|
||||
<VCol cols="12" sm="auto">
|
||||
<VSwitch v-if="status.config" v-model="status.config.show_sidebar_nav" color="primary" hide-details inset label="侧边栏入口" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VRow class="mb-2">
|
||||
<VCol cols="12" sm="4">
|
||||
<VSheet border rounded class="pa-4 h-100">
|
||||
<div class="text-caption text-medium-emphasis">可用供应商</div>
|
||||
<div class="text-h5">{{ summary.available_count || 0 }} / {{ summary.enabled_count || 0 }}</div>
|
||||
</VSheet>
|
||||
</VCol>
|
||||
<VCol cols="12" sm="4">
|
||||
<VSheet border rounded class="pa-4 h-100">
|
||||
<div class="text-caption text-medium-emphasis">累计使用</div>
|
||||
<div class="text-h5">{{ formatTokens(summary.total_used) }}</div>
|
||||
</VSheet>
|
||||
</VCol>
|
||||
<VCol cols="12" sm="4">
|
||||
<VSheet border rounded class="pa-4 h-100">
|
||||
<div class="text-caption text-medium-emphasis">总额度</div>
|
||||
<div class="text-h5">{{ formatTokens(summary.total_limit) }}</div>
|
||||
</VSheet>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VTabs v-model="activeTab" density="comfortable" class="mb-3">
|
||||
<VTab value="usage">用量</VTab>
|
||||
<VTab value="config">配置</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow v-model="activeTab">
|
||||
<VWindowItem value="usage">
|
||||
<VSheet border rounded>
|
||||
<VTable density="comfortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>优先级</th>
|
||||
<th>名称</th>
|
||||
<th>模型</th>
|
||||
<th>已用</th>
|
||||
<th>余量</th>
|
||||
<th>进度</th>
|
||||
<th>状态</th>
|
||||
<th class="text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in providerRows" :key="row.id">
|
||||
<td>{{ row.priority }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.model }}</td>
|
||||
<td>{{ formatTokens(row.usage?.total_tokens) }}</td>
|
||||
<td>
|
||||
{{ row.usage?.remaining_tokens === null ? '不限' : formatTokens(row.usage?.remaining_tokens) }}
|
||||
</td>
|
||||
<td class="progress-cell">
|
||||
<VProgressLinear
|
||||
:model-value="row.usage?.usage_percent || 0"
|
||||
:color="rowStatusColor(row)"
|
||||
height="8"
|
||||
rounded
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<VChip size="small" :color="rowStatusColor(row)" variant="tonal">{{ rowStatusText(row) }}</VChip>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<VBtn icon="mdi-backup-restore" size="small" variant="text" @click="resetUsage(row.id)" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!providerRows.length">
|
||||
<td colspan="8" class="text-center text-medium-emphasis py-8">暂无供应商</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VSheet>
|
||||
</VWindowItem>
|
||||
|
||||
<VWindowItem value="config">
|
||||
<div class="d-flex justify-end mb-3 gap-2">
|
||||
<VBtn prepend-icon="mdi-plus" color="primary" variant="tonal" @click="addProvider">新增</VBtn>
|
||||
<VBtn prepend-icon="mdi-backup-restore" color="warning" variant="tonal" @click="resetAllUsage">重置用量</VBtn>
|
||||
</div>
|
||||
<VSheet border rounded>
|
||||
<VTable density="comfortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>启用</th>
|
||||
<th>优先级</th>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th>地址</th>
|
||||
<th>Key</th>
|
||||
<th>模型</th>
|
||||
<th>额度</th>
|
||||
<th class="text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in config.providers" :key="row.id || index">
|
||||
<td>
|
||||
<VSwitch v-model="row.enabled" color="primary" hide-details density="compact" />
|
||||
</td>
|
||||
<td>{{ row.priority }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.provider }}</td>
|
||||
<td class="truncate-cell">{{ row.base_url }}</td>
|
||||
<td>{{ providerRows[index]?.masked_api_key || '****' }}</td>
|
||||
<td>{{ row.model }}</td>
|
||||
<td>{{ row.token_limit > 0 ? formatTokens(row.token_limit) : '不限' }}</td>
|
||||
<td class="text-right">
|
||||
<VBtn icon="mdi-pencil" size="small" variant="text" @click="editProvider(index)" />
|
||||
<VBtn icon="mdi-delete" size="small" variant="text" color="error" @click="removeProvider(index)" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!config.providers?.length">
|
||||
<td colspan="9" class="text-center text-medium-emphasis py-8">暂无供应商</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VSheet>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
|
||||
<VDialog v-model="showEditor" max-width="760">
|
||||
<VCard>
|
||||
<VCardTitle>{{ editorIndex >= 0 ? '编辑供应商' : '新增供应商' }}</VCardTitle>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField v-model="editedProvider.name" label="名称" variant="outlined" density="comfortable" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model.number="editedProvider.priority" label="优先级" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="editedProvider.provider"
|
||||
:items="providerTypeOptions"
|
||||
label="类型"
|
||||
variant="outlined"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="editedProvider.model" label="模型" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="editedProvider.base_url" label="API 地址" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="editedProvider.api_key" label="API Key" type="password" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model.number="editedProvider.token_limit" label="Token 额度" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model.number="editedProvider.used_tokens" label="初始已用" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="text" @click="showEditor = false">取消</VBtn>
|
||||
<VBtn color="primary" @click="commitProvider">确定</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
<AgentTokensManager
|
||||
:config="config"
|
||||
:provider-rows="providerRows"
|
||||
:summary="summary"
|
||||
:error="error"
|
||||
:loading="loading"
|
||||
:saving="saving"
|
||||
:hide-title="hideTitle"
|
||||
@refresh="loadStatus"
|
||||
@save="saveConfig"
|
||||
@reset-usage="resetUsage"
|
||||
@reset-all-usage="resetAllUsage"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-cell {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.truncate-cell {
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import AgentTokensManager from './AgentTokensManager.vue'
|
||||
import { cloneConfig } from '../provider'
|
||||
|
||||
const props = defineProps({
|
||||
initialConfig: {
|
||||
@@ -8,86 +10,24 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['save', 'close', 'switch'])
|
||||
const emit = defineEmits(['save', 'close'])
|
||||
|
||||
const localConfig = ref({ enabled: false, show_sidebar_nav: true, providers: [] })
|
||||
const showEditor = ref(false)
|
||||
const editorIndex = ref(-1)
|
||||
const editedProvider = ref(createProvider())
|
||||
|
||||
const providerTypeOptions = [
|
||||
{ title: 'OpenAI Compatible', value: 'openai' },
|
||||
{ title: 'DeepSeek', value: 'deepseek' },
|
||||
{ title: 'Google Gemini', value: 'google' },
|
||||
{ title: 'Anthropic Compatible', value: 'anthropic' },
|
||||
{ title: 'ChatGPT', value: 'chatgpt' },
|
||||
]
|
||||
|
||||
// 构建一个新的供应商默认配置。
|
||||
function createProvider() {
|
||||
return {
|
||||
id: '',
|
||||
enabled: true,
|
||||
name: '',
|
||||
provider: 'openai',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
model: '',
|
||||
token_limit: 0,
|
||||
used_tokens: 0,
|
||||
priority: 1,
|
||||
}
|
||||
// 重置本地配置中的单个供应商用量。
|
||||
function resetUsage(providerId, index) {
|
||||
const providers = localConfig.value.providers || []
|
||||
const providerIndex = providers.findIndex(provider => provider.id && provider.id === providerId)
|
||||
const targetIndex = providerIndex >= 0 ? providerIndex : index
|
||||
if (!providers[targetIndex]) return
|
||||
providers[targetIndex].used_tokens = 0
|
||||
}
|
||||
|
||||
// 生成深拷贝配置,避免直接修改父组件传入对象。
|
||||
function cloneConfig(config) {
|
||||
return JSON.parse(JSON.stringify(config || { enabled: false, show_sidebar_nav: true, providers: [] }))
|
||||
}
|
||||
|
||||
// 格式化 token 数字。
|
||||
function formatTokens(value) {
|
||||
const numberValue = Number(value || 0)
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : '0'
|
||||
}
|
||||
|
||||
// 打开新增供应商弹窗。
|
||||
function addProvider() {
|
||||
const nextPriority = Math.max(0, ...(localConfig.value.providers || []).map(item => Number(item.priority || 0))) + 1
|
||||
editedProvider.value = { ...createProvider(), priority: nextPriority }
|
||||
editorIndex.value = -1
|
||||
showEditor.value = true
|
||||
}
|
||||
|
||||
// 打开编辑供应商弹窗。
|
||||
function editProvider(index) {
|
||||
editedProvider.value = { ...localConfig.value.providers[index] }
|
||||
editorIndex.value = index
|
||||
showEditor.value = true
|
||||
}
|
||||
|
||||
// 将弹窗中的供应商写回本地配置。
|
||||
function commitProvider() {
|
||||
const providers = [...(localConfig.value.providers || [])]
|
||||
const provider = {
|
||||
...editedProvider.value,
|
||||
token_limit: Number(editedProvider.value.token_limit || 0),
|
||||
used_tokens: Number(editedProvider.value.used_tokens || 0),
|
||||
priority: Number(editedProvider.value.priority || providers.length + 1),
|
||||
}
|
||||
if (editorIndex.value >= 0) {
|
||||
providers.splice(editorIndex.value, 1, provider)
|
||||
} else {
|
||||
providers.push(provider)
|
||||
}
|
||||
localConfig.value.providers = providers
|
||||
showEditor.value = false
|
||||
}
|
||||
|
||||
// 移除一个供应商配置。
|
||||
function removeProvider(index) {
|
||||
const providers = [...(localConfig.value.providers || [])]
|
||||
providers.splice(index, 1)
|
||||
localConfig.value.providers = providers
|
||||
// 重置本地配置中的全部供应商用量。
|
||||
function resetAllUsage() {
|
||||
;(localConfig.value.providers || []).forEach(provider => {
|
||||
provider.used_tokens = 0
|
||||
})
|
||||
}
|
||||
|
||||
// 通知宿主保存 Vue 配置。
|
||||
@@ -111,103 +51,17 @@ onMounted(() => {
|
||||
<VToolbar density="comfortable" color="transparent">
|
||||
<div class="text-h6 ms-3">Agent Tokens 配置</div>
|
||||
<VSpacer />
|
||||
<VBtn icon="mdi-content-save" variant="text" color="primary" @click="saveConfig" />
|
||||
<VBtn icon="mdi-close" variant="text" @click="emit('close')" />
|
||||
</VToolbar>
|
||||
<VDivider />
|
||||
|
||||
<div class="pa-4">
|
||||
<div class="d-flex align-center mb-4 gap-2 flex-wrap">
|
||||
<VSwitch v-model="localConfig.enabled" color="primary" hide-details inset label="启用插件" />
|
||||
<VSwitch v-model="localConfig.show_sidebar_nav" color="primary" hide-details inset label="显示侧边栏入口" />
|
||||
<VSpacer />
|
||||
<VBtn prepend-icon="mdi-database-eye" variant="tonal" @click="emit('switch')">用量</VBtn>
|
||||
<VBtn prepend-icon="mdi-plus" color="primary" variant="tonal" @click="addProvider">新增</VBtn>
|
||||
</div>
|
||||
|
||||
<VSheet border rounded>
|
||||
<VTable density="comfortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>启用</th>
|
||||
<th>优先级</th>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th>模型</th>
|
||||
<th>额度</th>
|
||||
<th class="text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in localConfig.providers" :key="row.id || index">
|
||||
<td>
|
||||
<VSwitch v-model="row.enabled" color="primary" hide-details density="compact" />
|
||||
</td>
|
||||
<td>{{ row.priority }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.provider }}</td>
|
||||
<td>{{ row.model }}</td>
|
||||
<td>{{ row.token_limit > 0 ? formatTokens(row.token_limit) : '不限' }}</td>
|
||||
<td class="text-right">
|
||||
<VBtn icon="mdi-pencil" size="small" variant="text" @click="editProvider(index)" />
|
||||
<VBtn icon="mdi-delete" size="small" variant="text" color="error" @click="removeProvider(index)" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!localConfig.providers.length">
|
||||
<td colspan="7" class="text-center text-medium-emphasis py-8">暂无供应商</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VSheet>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
<div class="pa-4 d-flex justify-end">
|
||||
<VBtn prepend-icon="mdi-content-save" color="primary" @click="saveConfig">保存</VBtn>
|
||||
</div>
|
||||
|
||||
<VDialog v-model="showEditor" max-width="760">
|
||||
<VCard>
|
||||
<VCardTitle>{{ editorIndex >= 0 ? '编辑供应商' : '新增供应商' }}</VCardTitle>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField v-model="editedProvider.name" label="名称" variant="outlined" density="comfortable" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model.number="editedProvider.priority" label="优先级" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect v-model="editedProvider.provider" :items="providerTypeOptions" label="类型" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="editedProvider.model" label="模型" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="editedProvider.base_url" label="API 地址" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="editedProvider.api_key" label="API Key" type="password" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model.number="editedProvider.token_limit" label="Token 额度" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model.number="editedProvider.used_tokens" label="初始已用" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="text" @click="showEditor = false">取消</VBtn>
|
||||
<VBtn color="primary" @click="commitProvider">确定</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<AgentTokensManager
|
||||
:config="localConfig"
|
||||
hide-title
|
||||
@save="saveConfig"
|
||||
@reset-usage="resetUsage"
|
||||
@reset-all-usage="resetAllUsage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { formatTokens, unwrapResponse } from '../provider'
|
||||
|
||||
const props = defineProps({
|
||||
api: {
|
||||
@@ -19,20 +20,6 @@ let timer = null
|
||||
const summary = computed(() => status.value.summary || {})
|
||||
const providers = computed(() => status.value.providers || [])
|
||||
|
||||
// 兼容 MoviePilot API 包装器和原始响应两种返回形态。
|
||||
function unwrapResponse(response) {
|
||||
if (response && Object.prototype.hasOwnProperty.call(response, 'data') && response.success !== undefined) {
|
||||
return response.data
|
||||
}
|
||||
return response?.data ?? response
|
||||
}
|
||||
|
||||
// 格式化 token 数字。
|
||||
function formatTokens(value) {
|
||||
const numberValue = Number(value || 0)
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : '0'
|
||||
}
|
||||
|
||||
// 读取仪表板所需的精简状态。
|
||||
async function loadStatus() {
|
||||
if (!props.allowRefresh) return
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
import { formatTokens } from '../provider'
|
||||
|
||||
const props = defineProps({
|
||||
providers: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
providerRows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showCredentials: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['edit', 'remove'])
|
||||
|
||||
// 获取管理页服务端返回的脱敏 Key。
|
||||
function getMaskedApiKey(index) {
|
||||
return props.providerRows[index]?.masked_api_key || '****'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VSheet border rounded class="provider-table-shell">
|
||||
<VTable density="comfortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>启用</th>
|
||||
<th>优先级</th>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th v-if="showCredentials">地址</th>
|
||||
<th v-if="showCredentials">Key</th>
|
||||
<th>模型</th>
|
||||
<th>额度</th>
|
||||
<th class="text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in providers" :key="row.id || index">
|
||||
<td>
|
||||
<VSwitch v-model="row.enabled" color="primary" hide-details density="compact" />
|
||||
</td>
|
||||
<td>{{ row.priority }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.provider }}</td>
|
||||
<td v-if="showCredentials" class="truncate-cell">{{ row.base_url }}</td>
|
||||
<td v-if="showCredentials">{{ getMaskedApiKey(index) }}</td>
|
||||
<td>{{ row.model }}</td>
|
||||
<td>{{ row.token_limit > 0 ? formatTokens(row.token_limit) : '不限' }}</td>
|
||||
<td class="text-right">
|
||||
<VBtn icon="mdi-pencil" size="small" variant="text" @click="emit('edit', index)" />
|
||||
<VBtn icon="mdi-delete" size="small" variant="text" color="error" @click="emit('remove', index)" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!providers.length">
|
||||
<td :colspan="showCredentials ? 9 : 7" class="text-center text-medium-emphasis py-8">暂无供应商</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VSheet>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.provider-table-shell {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.provider-table-shell :deep(table) {
|
||||
min-width: 880px;
|
||||
}
|
||||
|
||||
.truncate-cell {
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { PROVIDER_TYPE_OPTIONS } from '../provider'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
provider: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
editorIndex: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'commit'])
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
// 提交当前弹窗编辑的供应商配置。
|
||||
function commitProvider() {
|
||||
emit('commit')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog v-model="dialogVisible" max-width="760" max-height="85vh" scrollable>
|
||||
<VCard>
|
||||
<VCardTitle>{{ editorIndex >= 0 ? '编辑供应商' : '新增供应商' }}</VCardTitle>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField v-model="provider.name" label="名称" variant="outlined" density="comfortable" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField v-model.number="provider.priority" label="优先级" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect v-model="provider.provider" :items="PROVIDER_TYPE_OPTIONS" label="类型" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="provider.model" label="模型" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="provider.base_url" label="API 地址" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="provider.api_key" label="API Key" type="password" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-model="provider.user_agent" label="User-Agent" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model.number="provider.token_limit" label="Token 额度" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model.number="provider.used_tokens" label="初始已用" type="number" variant="outlined" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="text" @click="dialogVisible = false">取消</VBtn>
|
||||
<VBtn color="primary" @click="commitProvider">确定</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
89
plugins.v2/agenttokens/src/components/ProviderUsageTable.vue
Normal file
89
plugins.v2/agenttokens/src/components/ProviderUsageTable.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { formatTokens } from '../provider'
|
||||
|
||||
defineProps({
|
||||
providerRows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['reset'])
|
||||
|
||||
// 根据供应商状态返回 Vuetify 颜色。
|
||||
function rowStatusColor(row) {
|
||||
if (!row.enabled) return 'default'
|
||||
if (row.usage?.exhausted) return 'error'
|
||||
if (!row.api_key || !row.base_url || !row.model) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 根据供应商状态返回短标签。
|
||||
function rowStatusText(row) {
|
||||
if (!row.enabled) return '停用'
|
||||
if (row.usage?.exhausted) return '耗尽'
|
||||
if (!row.api_key || !row.base_url || !row.model) return '缺配置'
|
||||
return '可用'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VSheet border rounded class="provider-table-shell">
|
||||
<VTable density="comfortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>优先级</th>
|
||||
<th>名称</th>
|
||||
<th>模型</th>
|
||||
<th>已用</th>
|
||||
<th>余量</th>
|
||||
<th>进度</th>
|
||||
<th>状态</th>
|
||||
<th class="text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in providerRows" :key="row.id || index">
|
||||
<td>{{ row.priority }}</td>
|
||||
<td>{{ row.name }}</td>
|
||||
<td>{{ row.model }}</td>
|
||||
<td>{{ formatTokens(row.usage?.total_tokens) }}</td>
|
||||
<td>
|
||||
{{ row.usage?.remaining_tokens === null ? '不限' : formatTokens(row.usage?.remaining_tokens) }}
|
||||
</td>
|
||||
<td class="progress-cell">
|
||||
<VProgressLinear
|
||||
:model-value="row.usage?.usage_percent || 0"
|
||||
:color="rowStatusColor(row)"
|
||||
height="8"
|
||||
rounded
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<VChip size="small" :color="rowStatusColor(row)" variant="tonal">{{ rowStatusText(row) }}</VChip>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<VBtn icon="mdi-backup-restore" size="small" variant="text" @click="emit('reset', row.id, index)" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!providerRows.length">
|
||||
<td colspan="8" class="text-center text-medium-emphasis py-8">暂无供应商</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VSheet>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.provider-table-shell {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.provider-table-shell :deep(table) {
|
||||
min-width: 760px;
|
||||
}
|
||||
|
||||
.progress-cell {
|
||||
min-width: 140px;
|
||||
}
|
||||
</style>
|
||||
121
plugins.v2/agenttokens/src/components/UsageOverviewCard.vue
Normal file
121
plugins.v2/agenttokens/src/components/UsageOverviewCard.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { formatTokens } from '../provider'
|
||||
|
||||
const props = defineProps({
|
||||
summary: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
const totalUsed = computed(() => Number(props.summary.total_used || 0))
|
||||
const totalLimit = computed(() => Number(props.summary.total_limit || 0))
|
||||
const usagePercent = computed(() => {
|
||||
if (totalLimit.value <= 0) return 0
|
||||
return Math.min((totalUsed.value * 100) / totalLimit.value, 100)
|
||||
})
|
||||
const usagePercentText = computed(() => `${Math.round(usagePercent.value)}%`)
|
||||
const remainingTokens = computed(() => {
|
||||
if (totalLimit.value <= 0) return null
|
||||
return Math.max(totalLimit.value - totalUsed.value, 0)
|
||||
})
|
||||
const progressColor = computed(() => {
|
||||
if (totalLimit.value <= 0) return 'primary'
|
||||
if (usagePercent.value >= 90) return 'error'
|
||||
if (usagePercent.value >= 70) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VSheet border rounded class="usage-overview-card">
|
||||
<div class="usage-overview-card__content">
|
||||
<div class="usage-overview-card__chart">
|
||||
<VProgressCircular
|
||||
:model-value="usagePercent"
|
||||
:color="progressColor"
|
||||
bg-color="surface-variant"
|
||||
:size="132"
|
||||
:width="12"
|
||||
>
|
||||
<div class="usage-overview-card__percent">{{ totalLimit > 0 ? usagePercentText : '不限' }}</div>
|
||||
</VProgressCircular>
|
||||
</div>
|
||||
|
||||
<div class="usage-overview-card__body">
|
||||
<div class="text-caption text-medium-emphasis">总使用进度</div>
|
||||
<div class="usage-overview-card__headline">
|
||||
{{ formatTokens(totalUsed) }}
|
||||
<span class="text-medium-emphasis">/ {{ totalLimit > 0 ? formatTokens(totalLimit) : '不限' }}</span>
|
||||
</div>
|
||||
<VProgressLinear
|
||||
:model-value="usagePercent"
|
||||
:color="progressColor"
|
||||
height="8"
|
||||
rounded
|
||||
class="my-4"
|
||||
/>
|
||||
<div class="usage-overview-card__meta">
|
||||
<span>剩余 {{ remainingTokens === null ? '不限' : formatTokens(remainingTokens) }}</span>
|
||||
<span>可用 {{ summary.available_count || 0 }} / {{ summary.enabled_count || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VSheet>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.usage-overview-card {
|
||||
block-size: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.usage-overview-card__content {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.usage-overview-card__chart {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.usage-overview-card__percent {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.usage-overview-card__headline {
|
||||
margin-block-start: 4px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.usage-overview-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.usage-overview-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.usage-overview-card__content {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.usage-overview-card__meta {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
plugins.v2/agenttokens/src/provider.js
Normal file
100
plugins.v2/agenttokens/src/provider.js
Normal file
@@ -0,0 +1,100 @@
|
||||
export const PROVIDER_TYPE_OPTIONS = [
|
||||
{ title: 'OpenAI Compatible', value: 'openai' },
|
||||
{ title: 'DeepSeek', value: 'deepseek' },
|
||||
{ title: 'Google Gemini', value: 'google' },
|
||||
{ title: 'Anthropic Compatible', value: 'anthropic' },
|
||||
{ title: 'ChatGPT', value: 'chatgpt' },
|
||||
]
|
||||
|
||||
// 构建一个新的供应商默认配置。
|
||||
export function createProvider() {
|
||||
return {
|
||||
id: '',
|
||||
enabled: true,
|
||||
name: '',
|
||||
provider: 'openai',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
user_agent: '',
|
||||
model: '',
|
||||
token_limit: 0,
|
||||
used_tokens: 0,
|
||||
priority: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成深拷贝配置,避免直接修改父组件传入对象。
|
||||
export function cloneConfig(config) {
|
||||
return JSON.parse(JSON.stringify(config || { enabled: false, show_sidebar_nav: true, providers: [] }))
|
||||
}
|
||||
|
||||
// 格式化 token 数字,保持表格和统计展示可读。
|
||||
export function formatTokens(value) {
|
||||
const numberValue = Number(value || 0)
|
||||
return Number.isFinite(numberValue) ? numberValue.toLocaleString() : '0'
|
||||
}
|
||||
|
||||
// 兼容 MoviePilot API 包装器和原始响应两种返回形态。
|
||||
export function unwrapResponse(response) {
|
||||
if (response && Object.prototype.hasOwnProperty.call(response, 'data') && response.success !== undefined) {
|
||||
return response.data
|
||||
}
|
||||
return response?.data ?? response
|
||||
}
|
||||
|
||||
// 计算新增供应商的下一个优先级。
|
||||
export function getNextProviderPriority(providers) {
|
||||
return Math.max(0, ...(providers || []).map(item => Number(item.priority || 0))) + 1
|
||||
}
|
||||
|
||||
// 标准化弹窗中写回的供应商数值字段。
|
||||
export function normalizeProvider(provider, fallbackPriority) {
|
||||
return {
|
||||
...provider,
|
||||
token_limit: Number(provider.token_limit || 0),
|
||||
used_tokens: Number(provider.used_tokens || 0),
|
||||
priority: Number(provider.priority || fallbackPriority),
|
||||
}
|
||||
}
|
||||
|
||||
// 按配置生成本地用量行,供配置弹窗复用管理页展示结构。
|
||||
export function buildProviderRow(provider) {
|
||||
const tokenLimit = Number(provider.token_limit || 0)
|
||||
const totalTokens = Number(provider.used_tokens || 0)
|
||||
const remainingTokens = tokenLimit <= 0 ? null : Math.max(tokenLimit - totalTokens, 0)
|
||||
const usagePercent = tokenLimit <= 0 ? 0 : Math.min((totalTokens * 100) / tokenLimit, 100)
|
||||
|
||||
return {
|
||||
...provider,
|
||||
masked_api_key: provider.api_key ? '****' : '',
|
||||
usage: {
|
||||
total_tokens: totalTokens,
|
||||
remaining_tokens: remainingTokens,
|
||||
usage_percent: usagePercent,
|
||||
exhausted: tokenLimit > 0 && remainingTokens === 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 批量生成本地供应商用量行。
|
||||
export function buildProviderRows(providers) {
|
||||
return (providers || []).map(provider => buildProviderRow(provider))
|
||||
}
|
||||
|
||||
// 根据供应商行汇总用量统计。
|
||||
export function buildProviderSummary(rows) {
|
||||
const providers = rows || []
|
||||
const enabledRows = providers.filter(row => row.enabled)
|
||||
const totalUsed = providers.reduce((sum, row) => sum + Number(row.usage?.total_tokens || row.used_tokens || 0), 0)
|
||||
const totalLimit = providers.reduce((sum, row) => {
|
||||
const tokenLimit = Number(row.token_limit || 0)
|
||||
return tokenLimit > 0 ? sum + tokenLimit : sum
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
available_count: enabledRows.filter(row => !row.usage?.exhausted && row.api_key && row.base_url && row.model).length,
|
||||
enabled_count: enabledRows.length,
|
||||
total_used: totalUsed,
|
||||
total_limit: totalLimit,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user