Compare commits

...

10 Commits

Author SHA1 Message Date
jxxghp
ec0c8cc521 修复自动签到详情页图标显示 2026-05-26 18:01:29 +08:00
jxxghp
52cd5b96e1 增强自动签到状态圆点对比度 2026-05-26 15:30:05 +08:00
jxxghp
0d7be2b58c 修复自动签到透明主题样式 2026-05-26 15:21:01 +08:00
jxxghp
e29a710a33 优化站点自动签到详情页 2026-05-26 15:04:45 +08:00
jxxghp
de83b88ad1 feat: refine agent tokens management UI 2026-05-26 08:59:55 +08:00
jxxghp
96ac52041a feat: support agent tokens user agent 2026-05-26 08:20:03 +08:00
jxxghp
287ccf50b2 fix: 禁用VWindow触摸滑动,修复表格内滑动触发tab切换 2026-05-25 15:48:36 +08:00
jxxghp
08faed6ff0 fix: 禁用VWindow触摸滑动,修复表格内滑动触发tab切换问题,升版本1.0.7 2026-05-25 15:44:09 +08:00
jxxghp
944867f96e fix: 设置base为相对路径 2026-05-25 15:36:15 +08:00
jxxghp
5f7c342b78 fix: 移除vuetify-filter,修复页面显示问题 2026-05-25 15:35:30 +08:00
29 changed files with 2965 additions and 2374 deletions

4
.gitignore vendored
View File

@@ -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/

View File

@@ -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": "补充分配模型信息及更新用量的运行日志",

View File

@@ -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"))

View 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;
}
}

View 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 _ };

View File

@@ -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 };

View 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 };

View File

@@ -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;
}

View File

@@ -1,4 +0,0 @@
.gap-2[data-v-22b1ab53] {
gap: 8px;
}

View 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 };

View File

@@ -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 };

View File

@@ -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"]))

View File

@@ -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,

View File

@@ -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 _ };

View File

@@ -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');

View 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 };

View File

@@ -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;

View File

@@ -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>

View File

@@ -1,7 +1,7 @@
{
"name": "moviepilot-agenttokens-plugin",
"private": true,
"version": "1.0.6",
"version": "1.0.9",
"type": "module",
"scripts": {
"build": "vite build"

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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