Release v1.4.3

This commit is contained in:
lutc5
2026-04-30 18:20:04 +08:00
parent a2f777a1a8
commit a02fd51c19
24 changed files with 1909 additions and 1176 deletions

View File

@@ -6,7 +6,7 @@ import Models from './views/Models.vue'
import Requests from './views/Requests.vue'
import Settings from './views/Settings.vue'
import { EventsOff, EventsOn } from '../wailsjs/runtime'
import { GetStatus, HideWindow, MinimizeWindow } from '../wailsjs/go/main/App.js'
import { ClearLogs, GetLogs, GetStatus, HideWindow, MinimizeWindow } from '../wailsjs/go/main/App.js'
import lingmaIcon from './assets/images/lingma-icon.png'
const currentTab = ref('dashboard')
@@ -42,8 +42,13 @@ function showToast(message) {
}, 2200)
}
function clearLocalLogs() {
logs.value = []
async function clearLocalLogs() {
try {
await ClearLogs()
logs.value = []
} catch (e) {
logs.value = []
}
}
function setStatus(nextStatus) {
@@ -158,14 +163,25 @@ onMounted(() => {
systemThemeQuery?.addEventListener?.('change', applyTheme)
applyTheme()
refreshStatus()
GetLogs().then((items) => {
logs.value = Array.isArray(items) ? items : []
}).catch(() => {})
safeEventsOn('models:updated', (data) => {
status.value.models = Array.isArray(data) ? data.length : status.value.models
addLog('info', `模型列表已更新:${status.value.models} 个模型`)
})
safeEventsOn('log', (data) => {
addLog(data.level || 'info', data.message || '')
if (data.time && data.message !== undefined) {
logs.value.unshift(data)
if (logs.value.length > 500) logs.value = logs.value.slice(0, 500)
} else {
addLog(data.level || 'info', data.message || '')
}
refreshStatus()
})
safeEventsOn('logs:updated', (data) => {
logs.value = Array.isArray(data) ? data : []
})
safeEventsOn('quit:confirm', (message) => {
showToast(message || '再按一次退出快捷键将停止代理并退出应用')
})
@@ -183,6 +199,7 @@ onUnmounted(() => {
systemThemeQuery?.removeEventListener?.('change', applyTheme)
safeEventsOff('models:updated')
safeEventsOff('log')
safeEventsOff('logs:updated')
safeEventsOff('quit:confirm')
safeEventsOff('status:updated')
safeEventsOff('requests:updated')
@@ -222,7 +239,7 @@ onUnmounted(() => {
<span class="status-dot" :class="{ running: status.running }"></span>
<div>
<strong>{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }}</strong>
<small>v1.4.2</small>
<small>v1.4.3</small>
</div>
</div>
</aside>

View File

@@ -1,71 +0,0 @@
<script lang="ts" setup>
import {reactive} from 'vue'
import {Greet} from '../../wailsjs/go/main/App'
const data = reactive({
name: "",
resultText: "Please enter your name below 👇",
})
function greet() {
Greet(data.name).then(result => {
data.resultText = result
})
}
</script>
<template>
<main>
<div id="result" class="result">{{ data.resultText }}</div>
<div id="input" class="input-box">
<input id="name" v-model="data.name" autocomplete="off" class="input" type="text"/>
<button class="btn" @click="greet">Greet</button>
</div>
</main>
</template>
<style scoped>
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
}
.input-box .btn {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}
</style>

View File

@@ -1,5 +1,12 @@
:root {
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
font-family:
Inter,
ui-sans-serif,
-apple-system,
BlinkMacSystemFont,
'SF Pro Text',
'Segoe UI',
sans-serif;
color: #172033;
background: #eef2f6;
font-synthesis: none;
@@ -26,7 +33,7 @@
--radius: 8px;
}
:root[data-theme="dark"] {
:root[data-theme='dark'] {
color: #edf3ff;
background: #111827;
--bg: #111827;
@@ -68,7 +75,7 @@ body {
background: var(--bg);
}
:root[data-theme="dark"] body {
:root[data-theme='dark'] body {
background: var(--bg);
}
@@ -106,7 +113,7 @@ button {
box-shadow: none;
}
:root[data-theme="dark"] .app-shell {
:root[data-theme='dark'] .app-shell {
border-color: rgba(148, 163, 184, 0.22);
background: rgba(16, 24, 36, 0.78);
}
@@ -123,7 +130,7 @@ button {
box-shadow: inset -1px 0 0 rgba(125, 139, 158, 0.16);
}
:root[data-theme="dark"] .sidebar {
:root[data-theme='dark'] .sidebar {
border-right-color: rgba(148, 163, 184, 0.14);
background: linear-gradient(180deg, rgba(28, 39, 56, 0.7), rgba(18, 27, 40, 0.66));
box-shadow: inset -1px 0 0 rgba(148, 163, 184, 0.12);
@@ -145,13 +152,13 @@ button {
background: rgba(255, 255, 255, 0.58);
}
:root[data-theme="dark"] .brand:hover,
:root[data-theme="dark"] .nav-item:hover,
:root[data-theme="dark"] .sidebar-status {
:root[data-theme='dark'] .brand:hover,
:root[data-theme='dark'] .nav-item:hover,
:root[data-theme='dark'] .sidebar-status {
background: rgba(255, 255, 255, 0.08);
}
:root[data-theme="dark"] .nav-item {
:root[data-theme='dark'] .nav-item {
color: #aebbd0;
}
@@ -224,7 +231,7 @@ button {
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.12);
}
:root[data-theme="dark"] .nav-item.active {
:root[data-theme='dark'] .nav-item.active {
color: #d8e6ff;
background: rgba(67, 111, 190, 0.24);
box-shadow: inset 0 0 0 1px rgba(105, 161, 255, 0.18);
@@ -285,13 +292,13 @@ button {
min-height: 46px;
padding: 0 16px;
border-bottom: 1px solid rgba(112, 128, 148, 0.18);
background: rgba(255, 255, 255, 0.58);
backdrop-filter: blur(20px) saturate(1.08);
background: #f6f9fd;
backdrop-filter: none;
}
:root[data-theme="dark"] .topbar {
:root[data-theme='dark'] .topbar {
border-bottom-color: rgba(148, 163, 184, 0.14);
background: rgba(20, 30, 45, 0.66);
background: #162131;
}
.topbar-spacer {
@@ -372,10 +379,10 @@ button {
backdrop-filter: blur(18px) saturate(1.12);
}
:root[data-theme="dark"] .glass-panel,
:root[data-theme="dark"] .metric,
:root[data-theme="dark"] .table-panel,
:root[data-theme="dark"] .config-panel {
:root[data-theme='dark'] .glass-panel,
:root[data-theme='dark'] .metric,
:root[data-theme='dark'] .table-panel,
:root[data-theme='dark'] .config-panel {
border-color: rgba(148, 163, 184, 0.14);
background: var(--surface);
}
@@ -583,6 +590,10 @@ button {
gap: 18px;
}
.settings-grid {
align-items: start;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -591,10 +602,11 @@ button {
.dashboard-grid {
display: grid;
align-items: stretch;
grid-template-columns: minmax(0, 1fr) minmax(0, 0.95fr) minmax(300px, 0.95fr);
grid-template-areas:
"health models config"
"requests requests config";
'health models config'
'requests requests usage';
gap: 12px;
}
@@ -604,16 +616,109 @@ button {
.area-models {
grid-area: models;
min-height: 0;
}
.area-config {
grid-area: config;
}
.compact-header {
margin-bottom: 10px;
}
.compact-header p {
margin-top: 4px;
}
.area-usage {
grid-area: usage;
}
.area-requests {
grid-area: requests;
}
.usage-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.usage-grid div {
min-width: 0;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface-soft);
}
.usage-grid label {
display: block;
margin-bottom: 6px;
color: var(--muted);
font-size: 11px;
font-weight: 680;
}
.usage-grid strong {
display: block;
overflow: hidden;
color: var(--text);
font-size: 20px;
line-height: 1.15;
text-overflow: ellipsis;
}
.usage-foot {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
margin-top: 12px;
color: var(--muted);
font-size: 12px;
}
.config-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.config-summary-item {
min-width: 0;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface-soft);
}
.config-summary-item label {
display: block;
margin-bottom: 6px;
color: var(--muted);
font-size: 11px;
font-weight: 680;
}
.config-summary-item strong {
display: block;
overflow: hidden;
color: var(--text);
font-size: 13px;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-summary-item.span-2 {
grid-column: 1 / -1;
}
.compact-link {
margin-top: 10px;
}
.activity-chart {
display: grid;
grid-template-columns: repeat(36, minmax(3px, 1fr));
@@ -637,13 +742,13 @@ button {
white-space: nowrap;
}
:root[data-theme="dark"] .activity-chart,
:root[data-theme="dark"] .data-table th,
:root[data-theme="dark"] .field input,
:root[data-theme="dark"] .field textarea,
:root[data-theme="dark"] .search-input,
:root[data-theme="dark"] .detail-panel pre,
:root[data-theme="dark"] .code-block {
:root[data-theme='dark'] .activity-chart,
:root[data-theme='dark'] .data-table th,
:root[data-theme='dark'] .field input,
:root[data-theme='dark'] .field textarea,
:root[data-theme='dark'] .search-input,
:root[data-theme='dark'] .detail-panel pre,
:root[data-theme='dark'] .code-block {
color: var(--text);
border-color: var(--line);
background: rgba(15, 23, 42, 0.74);
@@ -719,8 +824,8 @@ button {
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.12);
}
:root[data-theme="dark"] .model-choice:hover,
:root[data-theme="dark"] .model-choice:focus-visible {
:root[data-theme='dark'] .model-choice:hover,
:root[data-theme='dark'] .model-choice:focus-visible {
color: #f3f7ff;
border-color: rgba(105, 161, 255, 0.38);
background: rgba(72, 118, 214, 0.34);
@@ -728,7 +833,48 @@ button {
.models-list .model-row,
.model-list-row {
grid-template-columns: 22px minmax(220px, 1fr) auto;
grid-template-columns: 22px minmax(220px, 1fr) minmax(260px, auto);
}
.model-specs {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.spec-chip {
display: inline-flex;
min-height: 22px;
align-items: center;
padding: 0 8px;
border: 1px solid var(--line);
border-radius: 7px;
color: var(--muted);
background: var(--surface-soft);
font-size: 11px;
font-weight: 680;
white-space: nowrap;
}
.spec-chip.strong {
color: #0d6a41;
border-color: rgba(24, 160, 88, 0.18);
background: var(--green-soft);
}
.spec-chip.muted-chip {
color: #8a5a08;
border-color: rgba(217, 119, 6, 0.16);
background: var(--warn-soft);
}
:root[data-theme='dark'] .spec-chip.strong {
color: #7ee0aa;
}
:root[data-theme='dark'] .spec-chip.muted-chip {
color: #ffd27a;
}
.model-brand-icon {
@@ -744,6 +890,8 @@ button {
display: flex;
min-height: 0;
flex-direction: column;
overflow: hidden;
height: 295px;
}
.model-card-list,
@@ -754,7 +902,14 @@ button {
}
.model-card-list {
max-height: 248px;
flex: 1 1 auto;
max-height: none;
scrollbar-width: none;
}
.model-card-list::-webkit-scrollbar {
width: 0;
height: 0;
}
.model-page-list {
@@ -870,6 +1025,66 @@ button {
border-bottom: 1px solid var(--line);
}
.toolbar-header {
margin: 0;
display: flex;
align-items: baseline;
gap: 8px;
}
.toolbar-count {
font-size: 12px;
font-weight: normal;
white-space: nowrap;
}
.toolbar-search-wrap {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.toolbar-search-input {
max-width: 300px;
width: 100%;
}
.btn-sm-outline {
padding: 4px 10px;
font-size: 12px;
background: transparent;
border: 1px solid var(--line);
border-radius: 6px;
cursor: pointer;
color: var(--text);
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.16s ease;
}
.btn-sm-outline:hover {
background: rgba(0, 0, 0, 0.05);
}
.btn-sm-outline:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-sm-outline i {
margin-left: 2px;
}
:root[data-theme='dark'] .btn-sm-outline {
color: #dce8fb;
}
:root[data-theme='dark'] .btn-sm-outline:hover {
background: rgba(255, 255, 255, 0.1);
}
.table-scroll {
flex: 0 0 auto;
max-height: none;
@@ -893,7 +1108,7 @@ button {
.area-requests .table-scroll {
min-height: 0;
max-height: 260px;
max-height: 211px;
overflow: auto;
}
@@ -932,10 +1147,12 @@ button {
}
.data-table tbody tr {
height: var(--request-row-height, 64px);
height: var(--request-row-height, 42px);
cursor: pointer;
background: rgba(255, 255, 255, 0.34);
transition: background-color 140ms ease, box-shadow 140ms ease;
transition:
background-color 140ms ease,
box-shadow 140ms ease;
}
.data-table tbody tr:hover {
@@ -947,23 +1164,23 @@ button {
box-shadow: inset 3px 0 0 var(--blue);
}
:root[data-theme="dark"] .data-table {
:root[data-theme='dark'] .data-table {
background: rgba(15, 23, 42, 0.8);
}
:root[data-theme="dark"] .data-table th {
:root[data-theme='dark'] .data-table th {
background: rgba(15, 23, 42, 0.96);
}
:root[data-theme="dark"] .data-table tbody tr {
:root[data-theme='dark'] .data-table tbody tr {
background: rgba(20, 31, 48, 0.7);
}
:root[data-theme="dark"] .data-table tbody tr:hover {
:root[data-theme='dark'] .data-table tbody tr:hover {
background: rgba(45, 65, 96, 0.9);
}
:root[data-theme="dark"] .data-table tbody tr.selected {
:root[data-theme='dark'] .data-table tbody tr.selected {
background: rgba(38, 65, 112, 0.96);
box-shadow: inset 3px 0 0 #67a1ff;
}
@@ -1001,8 +1218,7 @@ button {
background: var(--red-soft);
}
.link-row,
.table-footer button {
.link-row {
display: flex;
align-items: center;
justify-content: space-between;
@@ -1014,24 +1230,10 @@ button {
cursor: pointer;
}
:root[data-theme="dark"] .link-row,
:root[data-theme="dark"] .table-footer button {
:root[data-theme='dark'] .link-row {
color: #dce8fb;
}
.table-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
color: var(--muted);
font-size: 12px;
}
.table-footer button {
width: auto;
gap: 8px;
}
.method-chip {
color: #334155;
@@ -1047,7 +1249,10 @@ button {
min-height: 32px;
border-radius: 8px;
cursor: pointer;
transition: transform 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
transition:
transform 0.16s ease,
background 0.16s ease,
box-shadow 0.16s ease;
}
.primary-button {
@@ -1132,12 +1337,92 @@ button:disabled {
gap: 6px;
}
.settings-fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
.settings-fieldset:disabled {
opacity: 0.56;
}
.compact-hint {
margin-bottom: 14px;
}
.compact-form-grid {
row-gap: 14px;
}
.field label {
color: var(--muted);
font-size: 12px;
font-weight: 680;
}
.switch-field {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.switch-field p {
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
.switch {
position: relative;
flex: 0 0 auto;
display: inline-flex;
width: 44px;
height: 26px;
}
.switch input {
position: absolute;
inset: 0;
opacity: 0;
}
.switch span {
position: absolute;
inset: 0;
border: 1px solid var(--line-strong);
border-radius: 999px;
background: rgba(148, 163, 184, 0.28);
transition:
background 0.16s ease,
border-color 0.16s ease;
}
.switch span::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
border-radius: 999px;
background: white;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.2);
transition: transform 0.16s ease;
}
.switch input:checked + span {
border-color: rgba(37, 99, 235, 0.72);
background: #2563eb;
}
.switch input:checked + span::after {
transform: translateX(18px);
}
.field input,
.field textarea,
.search-input {
@@ -1151,6 +1436,14 @@ button:disabled {
outline: none;
}
.field .switch input {
width: auto;
min-height: 0;
padding: 0;
border: 0;
background: transparent;
}
.field textarea {
min-height: 78px;
padding-top: 9px;
@@ -1230,14 +1523,14 @@ button:disabled {
background: var(--blue-soft);
}
:root[data-theme="dark"] .custom-select > button {
:root[data-theme='dark'] .custom-select > button {
color: var(--text);
border-color: var(--line);
background: rgba(15, 23, 42, 0.74);
}
:root[data-theme="dark"] .select-menu button:hover,
:root[data-theme="dark"] .select-menu button.selected {
:root[data-theme='dark'] .select-menu button:hover,
:root[data-theme='dark'] .select-menu button.selected {
color: #dce9ff;
background: rgba(72, 118, 214, 0.32);
}
@@ -1262,7 +1555,7 @@ button:disabled {
.hint-box code {
color: var(--text);
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
font-size: 12px;
}
@@ -1276,7 +1569,7 @@ button:disabled {
background: rgba(255, 255, 255, 0.54);
}
:root[data-theme="dark"] .detect-card {
:root[data-theme='dark'] .detect-card {
background: rgba(15, 23, 42, 0.52);
}
@@ -1326,7 +1619,7 @@ button:disabled {
margin: 0;
color: var(--text);
overflow-wrap: anywhere;
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.45;
}
@@ -1377,7 +1670,7 @@ button:disabled {
user-select: text;
}
:root[data-theme="dark"] .detail-panel {
:root[data-theme='dark'] .detail-panel {
background: rgba(12, 18, 30, 0.96);
}
@@ -1423,7 +1716,7 @@ button:disabled {
-webkit-user-select: text;
user-select: text;
background: rgba(255, 255, 255, 0.82);
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.55;
overflow-wrap: anywhere;
@@ -1525,28 +1818,28 @@ button:disabled {
border-color: rgba(44, 111, 231, 0.38);
}
:root[data-theme="dark"] .json-key {
:root[data-theme='dark'] .json-key {
color: #c4b5fd;
}
:root[data-theme="dark"] .json-string {
:root[data-theme='dark'] .json-string {
color: #86efac;
}
:root[data-theme="dark"] .json-number {
:root[data-theme='dark'] .json-number {
color: #93c5fd;
}
:root[data-theme="dark"] .json-boolean {
:root[data-theme='dark'] .json-boolean {
color: #fca5a5;
}
:root[data-theme="dark"] .json-null,
:root[data-theme="dark"] .json-punctuation {
:root[data-theme='dark'] .json-null,
:root[data-theme='dark'] .json-punctuation {
color: #9aa8bd;
}
:root[data-theme="dark"] .json-summary {
:root[data-theme='dark'] .json-summary {
color: #b7c3d6;
border-color: rgba(148, 163, 184, 0.24);
background: rgba(30, 41, 59, 0.78);
@@ -1563,19 +1856,17 @@ button:disabled {
height: 0;
}
:root[data-theme="dark"] .detail-panel pre,
:root[data-theme="dark"] .code-block,
:root[data-theme="dark"] .json-viewer {
:root[data-theme='dark'] .detail-panel pre,
:root[data-theme='dark'] .code-block,
:root[data-theme='dark'] .json-viewer {
color: var(--text);
border-color: var(--line);
background: rgba(17, 24, 39, 0.94);
}
.log-row {
grid-template-columns: 82px 58px minmax(0, 1fr);
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
font-size: 12px;
-webkit-user-select: text;
user-select: text;
@@ -1636,9 +1927,10 @@ button:disabled {
.dashboard-grid {
grid-template-areas:
"health models"
"config config"
"requests requests";
'health models'
'config config'
'usage usage'
'requests requests';
}
.status-strip {
@@ -1651,37 +1943,41 @@ button:disabled {
border-left: 0;
}
.strip-actions {
.strip-actions {
grid-column: span 2;
}
.config-summary {
grid-template-columns: 1fr 1fr;
}
}
:root[data-theme="dark"] .strip-actions,
:root[data-theme="dark"] .secondary-button,
:root[data-theme="dark"] .ghost-button,
:root[data-theme="dark"] .icon-button,
:root[data-theme="dark"] .segmented,
:root[data-theme="dark"] .segmented button {
:root[data-theme='dark'] .strip-actions,
:root[data-theme='dark'] .secondary-button,
:root[data-theme='dark'] .ghost-button,
:root[data-theme='dark'] .icon-button,
:root[data-theme='dark'] .segmented,
:root[data-theme='dark'] .segmented button {
color: var(--text);
border-color: var(--line);
background: rgba(30, 41, 59, 0.66);
}
:root[data-theme="dark"] .strip-actions {
:root[data-theme='dark'] .strip-actions {
background: rgba(15, 23, 42, 0.78);
}
:root[data-theme="dark"] .strip-actions button {
:root[data-theme='dark'] .strip-actions button {
color: #e6eefc;
}
:root[data-theme="dark"] .strip-actions button:disabled {
:root[data-theme='dark'] .strip-actions button:disabled {
color: #a9b7cc;
background: rgba(15, 23, 42, 0.52);
opacity: 0.86;
}
:root[data-theme="dark"] .segmented button.active {
:root[data-theme='dark'] .segmented button.active {
color: #f8fbff;
background: rgba(72, 118, 214, 0.42);
}
@@ -1759,10 +2055,11 @@ button:disabled {
.dashboard-grid {
grid-template-areas:
"health"
"models"
"config"
"requests";
'health'
'models'
'config'
'usage'
'requests';
}
.status-strip {
@@ -1775,6 +2072,14 @@ button:disabled {
width: 100%;
}
.config-summary {
grid-template-columns: 1fr;
}
.config-summary-item.span-2 {
grid-column: auto;
}
.span-2 {
grid-column: auto;
}

View File

@@ -1,23 +1,14 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue'
import {
GetModels,
GetConfig,
GetRequests,
GetStatus,
QuitApp,
RefreshModels,
StartProxy,
StopProxy,
} from '../../wailsjs/go/main/App.js'
import { GetModels, GetConfig, GetRequests, GetStatus, GetTokenStats, QuitApp, RefreshModels, StartProxy, StopProxy } from '../../wailsjs/go/main/App.js'
import { ClipboardSetText } from '../../wailsjs/runtime'
import { modelIcon } from '../modelIcons'
const props = defineProps({
shellStatus: {
type: Object,
default: () => ({ running: false, addr: '', models: 0 }),
},
default: () => ({ running: false, addr: '', models: 0 })
}
})
const emit = defineEmits(['log', 'status', 'notice', 'open-settings', 'open-requests', 'open-models'])
@@ -25,9 +16,11 @@ const emit = defineEmits(['log', 'status', 'notice', 'open-settings', 'open-requ
const status = ref(props.shellStatus)
const models = ref([])
const requests = ref([])
const tokenStats = ref({ totalRequests: 0, successRequests: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0 })
const health = ref(null)
const config = ref({})
const loading = ref(false)
const proxyLoading = ref(false)
const modelsLoading = ref(false)
const testing = ref(false)
const now = ref(Date.now())
let interval = null
@@ -63,7 +56,7 @@ const healthStats = computed(() => {
avg,
p50: percentile(sorted, 0.5),
p95: percentile(sorted, 0.95),
max: sorted[sorted.length - 1],
max: Math.round(sorted[sorted.length - 1])
}
})
const chartBars = computed(() => {
@@ -78,17 +71,22 @@ const displayRequests = computed(() => {
})
const displayModels = computed(() => {
if (models.value.length > 0) {
return models.value.slice(0, 5).map((model) => ({ ...model, online: true }))
return models.value.map((model) => ({ ...model, online: true }))
}
return []
})
const successRate = computed(() => {
const total = Number(tokenStats.value.totalRequests || 0)
if (!total) return '0%'
return `${Math.round((Number(tokenStats.value.successRequests || 0) / total) * 100)}%`
})
function parseDurationMs(duration) {
const text = String(duration || '').trim()
if (!text) return 0
if (text.endsWith('ms')) return Number.parseFloat(text)
if (text.endsWith('s')) return Number.parseFloat(text) * 1000
return Number.parseFloat(text) || 0
if (text.endsWith('ms')) return Math.round(Number.parseFloat(text))
if (text.endsWith('s')) return Math.round(Number.parseFloat(text) * 1000)
return Math.round(Number.parseFloat(text) || 0)
}
function percentile(sorted, p) {
@@ -97,12 +95,20 @@ function percentile(sorted, p) {
return Math.round(sorted[index])
}
function formatNumber(value) {
const n = Number(value || 0)
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`
if (n >= 10000) return `${Math.round(n / 1000)}K`
return n.toLocaleString('zh-CN')
}
async function refresh() {
try {
const nextStatus = await GetStatus()
status.value = nextStatus
emit('status', nextStatus)
requests.value = await GetRequests()
tokenStats.value = await GetTokenStats()
config.value = await GetConfig()
if (nextStatus.running) {
models.value = await GetModels()
@@ -113,7 +119,7 @@ async function refresh() {
}
async function refreshModels() {
loading.value = true
modelsLoading.value = true
try {
models.value = await RefreshModels()
emit('log', 'info', `模型探测完成:${models.value.length}`)
@@ -121,7 +127,7 @@ async function refreshModels() {
} catch (e) {
emit('log', 'error', '模型探测失败:' + (e.message || String(e)) + '。请确认 Lingma 插件已启动并登录;自动探测失败时可到设置页手动填写 WebSocketws://127.0.0.1:36510/,或 Windows Named Pipe\\\\.\\pipe\\lingma-xxxx。')
} finally {
loading.value = false
modelsLoading.value = false
}
}
@@ -141,7 +147,7 @@ async function copyModelName(model) {
}
async function toggleProxy() {
loading.value = true
proxyLoading.value = true
try {
if (isRunning.value) {
await StopProxy()
@@ -154,13 +160,13 @@ async function toggleProxy() {
} catch (e) {
emit('log', 'error', '代理切换失败:' + (e.message || String(e)))
} finally {
loading.value = false
proxyLoading.value = false
}
}
async function restartProxy() {
if (!isRunning.value) return
loading.value = true
proxyLoading.value = true
try {
await StopProxy()
await StartProxy()
@@ -169,7 +175,7 @@ async function restartProxy() {
} catch (e) {
emit('log', 'error', '代理重启失败:' + (e.message || String(e)))
} finally {
loading.value = false
proxyLoading.value = false
}
}
@@ -241,9 +247,9 @@ onUnmounted(() => {
<strong>{{ sessionLabel }}</strong>
</div>
<div class="strip-actions">
<button :class="{ active: !isRunning }" type="button" :disabled="loading || isRunning" @click="toggleProxy">启动</button>
<button :class="{ active: isRunning }" type="button" :disabled="loading || !isRunning" @click="toggleProxy">停止</button>
<button type="button" :disabled="loading || !isRunning" @click="restartProxy">重启</button>
<button :class="{ active: !isRunning }" type="button" :disabled="proxyLoading || isRunning" @click="toggleProxy">启动</button>
<button :class="{ active: isRunning }" type="button" :disabled="proxyLoading || !isRunning" @click="toggleProxy">停止</button>
<button type="button" :disabled="proxyLoading || !isRunning" @click="restartProxy">重启</button>
</div>
</section>
@@ -257,12 +263,7 @@ onUnmounted(() => {
<span class="status-chip ok">Healthy</span>
</div>
<div class="activity-chart" aria-label="延迟趋势图">
<span
v-for="(height, index) in chartBars"
:key="index"
class="bar"
:style="{ height: `${height}%`, opacity: 0.55 + index / 45 }"
></span>
<span v-for="(height, index) in chartBars" :key="index" class="bar" :style="{ height: `${height}%`, opacity: 0.55 + index / 45 }"></span>
<span v-if="chartBars.length === 0" class="chart-empty">暂无请求</span>
</div>
<div class="health-stats">
@@ -278,22 +279,13 @@ onUnmounted(() => {
<div>
<h2>Models</h2>
</div>
<button class="secondary-button" type="button" :disabled="loading || !isRunning" @click="refreshModels">探测模型</button>
<button class="btn-sm-outline" type="button" :disabled="modelsLoading || !isRunning" @click="refreshModels">
{{ modelsLoading ? '探测中...' : '探测模型' }}
</button>
</div>
<div class="model-card-list hidden-scrollbar">
<button
v-for="model in displayModels"
:key="model.id"
class="model-row model-choice"
type="button"
:title="`复制模型 ID${model.id}`"
@click="copyModelName(model)"
>
<span
class="model-brand-icon"
:style="{ '--model-icon': `url(${modelIcon(model).src})`, '--model-icon-color': modelIcon(model).color }"
aria-hidden="true"
></span>
<button v-for="model in displayModels" :key="model.id" class="model-row model-choice" type="button" :title="`复制模型 ID${model.id}`" @click="copyModelName(model)">
<span class="model-brand-icon" :style="{ '--model-icon': `url(${modelIcon(model).src})`, '--model-icon-color': modelIcon(model).color }" aria-hidden="true"></span>
<div>
<div class="model-name">{{ model.name || model.id }}</div>
</div>
@@ -301,107 +293,112 @@ onUnmounted(() => {
</button>
</div>
<div v-if="displayModels.length === 0" class="empty-state compact">暂无模型启动代理后点击探测模型</div>
<button class="link-row" type="button" @click="emit('open-models')">查看全部模型 <i class="bi bi-chevron-right"></i></button>
</div>
<div class="glass-panel area-config">
<div class="panel-header">
<div class="panel-header compact-header">
<div>
<h2>Configuration</h2>
<p>首页只展示关键配置完整项在设置页查看</p>
</div>
<span class="status-chip ok">Valid</span>
</div>
<div class="setting-row">
<div>
<div class="cell-main">Host</div>
<div class="cell-sub">{{ config.Host || '127.0.0.1' }}</div>
<div class="config-summary">
<div class="config-summary-item">
<label>监听地址</label>
<strong>{{ config.Host || '127.0.0.1' }}:{{ config.Port || 8095 }}</strong>
</div>
<div class="config-summary-item">
<label>传输方式</label>
<strong>{{ transportLabel }}</strong>
</div>
<div class="config-summary-item">
<label>会话策略</label>
<strong>{{ config.SessionMode || 'Reuse' }}</strong>
</div>
<div class="config-summary-item">
<label>超时</label>
<strong>{{ config.Timeout || 120 }} </strong>
</div>
<div class="config-summary-item span-2">
<label>工作目录</label>
<strong :title="config.Cwd || '未配置'">{{ config.Cwd || '未配置' }}</strong>
</div>
<div v-if="config.CurrentFilePath" class="config-summary-item span-2">
<label>当前文件</label>
<strong :title="config.CurrentFilePath">{{ config.CurrentFilePath }}</strong>
</div>
<span class="status-chip ok"><i class="bi bi-check"></i></span>
</div>
<div class="setting-row">
</div>
<div class="glass-panel area-usage">
<div class="panel-header">
<div>
<div class="cell-main">Port</div>
<div class="cell-sub">{{ config.Port || 8095 }}</div>
<h2>Token 统计</h2>
<p>按代理返回的 usage 累计流式缺失字段时只统计可获得部分</p>
</div>
<span class="status-chip ok"><i class="bi bi-check"></i></span>
<span class="status-chip ok">Persisted</span>
</div>
<div class="setting-row">
<div class="usage-grid">
<div>
<div class="cell-main">Transport</div>
<div class="cell-sub">{{ transportLabel }}</div>
<label> Token</label>
<strong>{{ formatNumber(tokenStats.totalTokens) }}</strong>
</div>
<div>
<label>输入</label>
<strong>{{ formatNumber(tokenStats.inputTokens) }}</strong>
</div>
<div>
<label>输出</label>
<strong>{{ formatNumber(tokenStats.outputTokens) }}</strong>
</div>
<div>
<label>成功率</label>
<strong>{{ successRate }}</strong>
</div>
<span class="status-chip ok"><i class="bi bi-check"></i></span>
</div>
<div class="setting-row">
<div>
<div class="cell-main">Session</div>
<div class="cell-sub">{{ config.SessionMode || 'Reuse' }}</div>
</div>
<span class="status-chip ok"><i class="bi bi-check"></i></span>
</div>
<div class="setting-row">
<div>
<div class="cell-main">Timeout (s)</div>
<div class="cell-sub">{{ config.Timeout || 120 }} </div>
</div>
<span class="status-chip ok"><i class="bi bi-check"></i></span>
</div>
<div class="setting-row">
<div>
<div class="cell-main">CWD</div>
<div class="cell-sub">{{ config.Cwd || '未配置' }}</div>
</div>
<span class="status-chip ok"><i class="bi bi-check"></i></span>
</div>
<div class="setting-row">
<div>
<div class="cell-main">Current File</div>
<div class="cell-sub">{{ config.CurrentFilePath || '未配置' }}</div>
</div>
<span class="status-chip ok"><i class="bi bi-check"></i></span>
<div class="usage-foot">
<span>累计请求 {{ formatNumber(tokenStats.totalRequests) }} </span>
<span v-if="tokenStats.lastModel">最近模型 {{ tokenStats.lastModel }}</span>
</div>
</div>
<div class="table-panel area-requests">
<div class="table-toolbar">
<div>
<div class="panel-header" style="margin: 0">
<div class="table-toolbar">
<div class="panel-header toolbar-header">
<h2>Recent Requests</h2>
</div>
<button type="button" class="btn-sm-outline" @click="emit('open-requests')">
查看全部请求 <i class="bi bi-chevron-right"></i>
</button>
</div>
<button class="secondary-button" type="button" @click="emit('open-requests')">查看全部</button>
</div>
<div v-if="displayRequests.length > 0" class="table-scroll hidden-scrollbar">
<table class="data-table">
<thead>
<tr>
<th>Time</th>
<th>Method</th>
<th>Path</th>
<th>Model</th>
<th>Status</th>
<th>Duration</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr v-for="(request, index) in displayRequests" :key="index">
<td>{{ request.time }}</td>
<td>{{ request.method }}</td>
<td>{{ request.path }}</td>
<td>{{ request.model || '-' }}</td>
<td><span class="status-chip" :class="statusClass(request.statusCode)">{{ request.statusCode }}</span></td>
<td>{{ request.duration }}</td>
<td>{{ request.size || '-' }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="empty-state compact">暂无请求记录连接客户端后会显示真实调用</div>
<div class="table-footer">
<span>Showing {{ displayRequests.length }} of {{ requests.length }}</span>
<button type="button" @click="emit('open-requests')">查看全部请求 <i class="bi bi-chevron-right"></i></button>
</div>
<div v-if="displayRequests.length > 0" class="table-scroll hidden-scrollbar">
<table class="data-table">
<thead>
<tr>
<th>Time</th>
<th>Method</th>
<th>Path</th>
<th>Model</th>
<th>Status</th>
<th>Duration</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr v-for="(request, index) in displayRequests" :key="index">
<td>{{ request.time }}</td>
<td>{{ request.method }}</td>
<td>{{ request.path }}</td>
<td>{{ request.model || '-' }}</td>
<td><span class="status-chip" :class="statusClass(request.statusCode)">{{ request.statusCode }}</span></td>
<td>{{ request.duration }}</td>
<td>{{ request.size || '-' }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="empty-state compact">暂无请求记录连接客户端后会显示真实调用</div>
</div>
</section>
</div>

View File

@@ -17,20 +17,81 @@ const filtered = computed(() => {
return models.value.filter((model) => `${model.id} ${model.name}`.toLowerCase().includes(q))
})
function modelTag(model) {
function modelSpec(model) {
const text = `${model.id} ${model.name}`.toLowerCase()
if (text.includes('coder')) return '工具优先'
if (text.includes('thinking')) return '推理'
if (text.includes('kimi')) return '长文本'
if (text.includes('minimax')) return '通用'
return 'Lingma'
if (text.includes('kmodel') || text.includes('kimi')) {
return {
context: '256K',
capability: '文本/图像/视频/工具',
source: 'Kimi 官方',
}
}
if (text.includes('mmodel') || text.includes('minimax')) {
return {
context: '200K',
capability: 'Agent / Tool Use',
source: 'MiniMax 官方',
}
}
if (text.includes('coder')) {
return {
context: '1M',
capability: '思考 / Function Calling / 结构化输出',
source: '阿里云百炼 Qwen3-Coder',
}
}
if (text.includes('thinking')) {
return {
context: '256K',
capability: '思考 / Function Calling / 推理',
source: '阿里云百炼 Qwen3',
}
}
if (text.includes('qwen_max') || text.includes('qwen3-max')) {
return {
context: '256K',
capability: '思考 / Function Calling / 内置工具',
source: '阿里云百炼 Qwen3-Max',
}
}
if (text.includes('qmodel') || text.includes('qwen3.6')) {
return {
context: '1M',
capability: 'Function Calling / 内置工具 / 结构化输出',
source: '阿里云百炼 Qwen3.6-Plus',
}
}
if (text.includes('auto')) {
return {
context: '自动',
capability: 'Lingma 自动路由',
source: '账号返回',
}
}
return {
context: '未公开',
capability: '通用',
source: '账号返回',
}
}
async function loadCachedModels() {
loading.value = true
try {
status.value = await GetStatus()
models.value = await GetModels()
} catch (e) {
emit('log', 'error', '模型缓存读取失败:' + (e.message || String(e)))
} finally {
loading.value = false
}
}
async function refresh() {
loading.value = true
try {
status.value = await GetStatus()
models.value = status.value.running ? await RefreshModels() : await GetModels()
models.value = await RefreshModels()
emit('log', 'info', `模型列表刷新完成:${models.value.length}`)
} catch (e) {
emit('log', 'error', '模型列表刷新失败:' + (e.message || String(e)) + '。自动探测失败时请到设置页手动填写 WebSocketws://127.0.0.1:36510/,或 Windows Named Pipe\\\\.\\pipe\\lingma-xxxx。')
@@ -54,7 +115,7 @@ async function copyModelName(model) {
}
}
onMounted(refresh)
onMounted(loadCachedModels)
</script>
<template>
@@ -111,7 +172,11 @@ onMounted(refresh)
<div class="model-name">{{ model.name || model.id }}</div>
<div class="model-meta">{{ model.id }}</div>
</div>
<span class="status-chip" :class="modelTag(model) === '工具优先' ? 'ok' : 'warn'">{{ modelTag(model) }}</span>
<div class="model-specs">
<span class="spec-chip strong">{{ modelSpec(model).context }}</span>
<span class="spec-chip">{{ modelSpec(model).capability }}</span>
<span class="spec-chip muted-chip">{{ modelSpec(model).source }}</span>
</div>
</button>
</div>
<div v-else class="empty-state">启动代理并刷新后会显示模型</div>

View File

@@ -14,7 +14,7 @@ const activeStatus = ref('all')
const filtered = computed(() => {
const q = query.value.trim().toLowerCase()
return requests.value.filter((request) => {
const matchesQuery = !q || `${request.method} ${request.path} ${request.statusCode}`.toLowerCase().includes(q)
const matchesQuery = !q || `${request.method} ${request.path} ${request.statusCode} ${request.model || ''}`.toLowerCase().includes(q)
const code = Number(request.statusCode)
const matchesStatus =
activeStatus.value === 'all' ||
@@ -117,7 +117,10 @@ onUnmounted(() => {
<section class="table-panel requests-panel">
<div class="table-toolbar">
<input v-model="query" class="search-input" type="search" placeholder="搜索路径、方法或状态码" />
<div class="toolbar-search-wrap">
<input v-model="query" class="search-input toolbar-search-input" type="search" placeholder="搜索路径、方法或状态码" />
<span class="muted toolbar-count">Showing {{ filtered.length }} of {{ requests.length }}</span>
</div>
<div class="segmented">
<button :class="{ active: activeStatus === 'all' }" type="button" @click="activeStatus = 'all'">全部</button>
<button :class="{ active: activeStatus === 'ok' }" type="button" @click="activeStatus = 'ok'">成功</button>
@@ -133,6 +136,7 @@ onUnmounted(() => {
<th>时间</th>
<th>方法</th>
<th>路径</th>
<th>模型</th>
<th>状态</th>
<th>耗时</th>
</tr>
@@ -150,6 +154,7 @@ onUnmounted(() => {
<div class="cell-main">{{ request.path }}</div>
<div class="cell-sub">{{ request.reqBody ? '包含请求体' : '无请求体' }}</div>
</td>
<td>{{ request.model || '-' }}</td>
<td><span class="status-chip" :class="statusClass(request.statusCode)">{{ request.statusCode }}</span></td>
<td>{{ request.duration }}</td>
</tr>

View File

@@ -8,6 +8,8 @@ const config = ref({})
const detection = ref(null)
const saving = ref(false)
const openSelect = ref('')
const fallbackModelsText = ref('')
const isIPCBackend = computed(() => (config.value.Backend || 'ipc') === 'ipc')
const selectOptions = {
Backend: [
@@ -54,6 +56,9 @@ function chooseOption(field, value) {
onMounted(async () => {
try {
config.value = await GetConfig()
fallbackModelsText.value = Array.isArray(config.value.RemoteFallbackModels)
? config.value.RemoteFallbackModels.join('\n')
: ''
await refreshDetection()
} catch (e) {
emit('log', 'error', '配置加载失败:' + (e.message || String(e)))
@@ -71,6 +76,10 @@ async function refreshDetection() {
async function save() {
saving.value = true
try {
config.value.RemoteFallbackModels = fallbackModelsText.value
.split(/\n|,/)
.map((item) => item.trim())
.filter(Boolean)
await UpdateConfig(config.value)
await refreshDetection()
emit('log', 'info', '配置已保存,代理已按需重启')
@@ -95,7 +104,7 @@ async function save() {
</button>
</div>
<section class="grid-2">
<section class="grid-2 settings-grid">
<div class="glass-panel">
<div class="panel-header">
<div>
@@ -156,6 +165,23 @@ async function save() {
<label>超时秒数</label>
<input v-model.number="config.Timeout" type="number" min="1" />
</div>
<div class="field span-2 switch-field">
<div>
<label>远端超时兜底</label>
<p>远端 API 超时限流或 5xx 且尚未流式输出时自动切换到下一个可用模型</p>
</div>
<label class="switch">
<input v-model="config.RemoteFallbackEnabled" type="checkbox" />
<span></span>
</label>
</div>
<div class="field span-2">
<label>兜底模型顺序</label>
<textarea
v-model="fallbackModelsText"
placeholder="kmodel&#10;mmodel&#10;dashscope_qwen3_coder&#10;dashscope_qmodel"
></textarea>
</div>
<div class="field span-2">
<label>WebSocket 地址</label>
<input v-model="config.WebSocketURL" type="text" placeholder="留空自动探测 Lingma WebSocket" />
@@ -231,10 +257,16 @@ async function save() {
<div class="panel-header">
<div>
<h2>会话与环境</h2>
<p>影响 Lingma 会话上下文和工具执行环境</p>
<p>仅在 IPC 插件模式下生效影响 Lingma 会话上下文和工具执行环境</p>
</div>
<span class="status-chip" :class="isIPCBackend ? 'ok' : 'warn'">{{ isIPCBackend ? '仅 IPC 生效' : '远端模式忽略' }}</span>
</div>
<div class="form-grid">
<div v-if="!isIPCBackend" class="hint-box compact-hint">
<strong>当前为远端 API 模式</strong>
<span>右侧这组参数不会参与远端请求只在切换到 IPC 插件模式后生效</span>
</div>
<fieldset class="settings-fieldset" :disabled="!isIPCBackend">
<div class="form-grid compact-form-grid">
<div class="field">
<label>模式</label>
<div class="custom-select" :class="{ open: openSelect === 'Mode' }">
@@ -301,9 +333,10 @@ async function save() {
</div>
<div class="field span-2">
<label>工作目录</label>
<textarea v-model="config.Cwd" placeholder="Lingma 创建 session 时使用的 cwd"></textarea>
<input v-model="config.Cwd" type="text" placeholder="Lingma 创建 session 时使用的 cwd" />
</div>
</div>
</fieldset>
</div>
</section>
</div>

View File

@@ -11,12 +11,16 @@ export function GetConfig():Promise<service.Config>;
export function GetDetectionInfo():Promise<main.DetectionInfo>;
export function GetLogs():Promise<Array<main.AppLog>>;
export function GetModels():Promise<Array<main.ModelInfo>>;
export function GetRequests():Promise<Array<main.RequestRecord>>;
export function GetStatus():Promise<main.ProxyStatus>;
export function GetTokenStats():Promise<main.TokenStats>;
export function HideWindow():Promise<void>;
export function MinimizeWindow():Promise<void>;

View File

@@ -18,6 +18,10 @@ export function GetDetectionInfo() {
return window['go']['main']['App']['GetDetectionInfo']();
}
export function GetLogs() {
return window['go']['main']['App']['GetLogs']();
}
export function GetModels() {
return window['go']['main']['App']['GetModels']();
}
@@ -30,6 +34,10 @@ export function GetStatus() {
return window['go']['main']['App']['GetStatus']();
}
export function GetTokenStats() {
return window['go']['main']['App']['GetTokenStats']();
}
export function HideWindow() {
return window['go']['main']['App']['HideWindow']();
}

View File

@@ -1,5 +1,21 @@
export namespace main {
export class AppLog {
time: string;
level: string;
message: string;
static createFrom(source: any = {}) {
return new AppLog(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.time = source["time"];
this.level = source["level"];
this.message = source["message"];
}
}
export class DetectionInfo {
listenUrl: string;
backend: string;
@@ -86,6 +102,9 @@ export namespace main {
statusCode: number;
duration: string;
size?: string;
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
reqBody?: string;
respBody?: string;
@@ -102,10 +121,39 @@ export namespace main {
this.statusCode = source["statusCode"];
this.duration = source["duration"];
this.size = source["size"];
this.inputTokens = source["inputTokens"];
this.outputTokens = source["outputTokens"];
this.totalTokens = source["totalTokens"];
this.reqBody = source["reqBody"];
this.respBody = source["respBody"];
}
}
export class TokenStats {
totalRequests: number;
successRequests: number;
inputTokens: number;
outputTokens: number;
totalTokens: number;
byModel?: Record<string, number>;
lastModel?: string;
lastUpdated?: string;
static createFrom(source: any = {}) {
return new TokenStats(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.totalRequests = source["totalRequests"];
this.successRequests = source["successRequests"];
this.inputTokens = source["inputTokens"];
this.outputTokens = source["outputTokens"];
this.totalTokens = source["totalTokens"];
this.byModel = source["byModel"];
this.lastModel = source["lastModel"];
this.lastUpdated = source["lastUpdated"];
}
}
}
@@ -128,6 +176,8 @@ export namespace service {
ShellType: string;
SessionMode: string;
Timeout: number;
RemoteFallbackEnabled: boolean;
RemoteFallbackModels: string[];
static createFrom(source: any = {}) {
return new Config(source);
@@ -151,6 +201,8 @@ export namespace service {
this.ShellType = source["ShellType"];
this.SessionMode = source["SessionMode"];
this.Timeout = source["Timeout"];
this.RemoteFallbackEnabled = source["RemoteFallbackEnabled"];
this.RemoteFallbackModels = source["RemoteFallbackModels"];
}
}