feat: add ai cost tracking foundation
This commit is contained in:
@@ -136,6 +136,14 @@ const modelForm = reactive({
|
||||
extraParams: "",
|
||||
remark: "",
|
||||
timeoutSecond: 30,
|
||||
inputPricePer1k: null as number | null,
|
||||
outputPricePer1k: null as number | null,
|
||||
currency: "CNY",
|
||||
usageScenarios: "",
|
||||
allowSummary: 1,
|
||||
allowReport: 1,
|
||||
allowFixedInfo: 1,
|
||||
allowDeepChat: 1,
|
||||
});
|
||||
|
||||
const quickModelForm = reactive({
|
||||
@@ -212,6 +220,9 @@ const nullableModelFields = [
|
||||
"responseFormat",
|
||||
"extraParams",
|
||||
"remark",
|
||||
"inputPricePer1k",
|
||||
"outputPricePer1k",
|
||||
"usageScenarios",
|
||||
] as const;
|
||||
|
||||
const systemSettingValues = reactive<Record<string, SystemSettingValue>>({});
|
||||
@@ -289,6 +300,7 @@ async function loadDashboard() {
|
||||
|
||||
async function refreshStorage() { storageLoading.value = true; try { storageStats.value = await api.storageStats(true); ElMessage.success("存储统计已刷新"); } finally { storageLoading.value = false; } }
|
||||
function formatBytes(value: unknown) { if (typeof value !== "number") return "无法统计"; const units = ["B", "KB", "MB", "GB", "TB"]; let size = value, i = 0; while (size >= 1024 && i < units.length - 1) { size /= 1024; i += 1; } return `${size.toFixed(i ? 2 : 0)} ${units[i]}`; }
|
||||
function formatMoney(value?: number | null, currency = "CNY") { if (typeof value !== "number") return "-"; return `${currency || "CNY"} ${value.toFixed(6)}`; }
|
||||
async function cleanupRetrievalLogs() { if (!cleanupBefore.value) return ElMessage.warning("请选择清理日期"); const estimate = await api.estimateRetrievalCleanup(cleanupBefore.value); await ElMessageBox.confirm(`预计清理 ${estimate.estimatedCount} 条检索日志及关联明细,操作日志不会被删除。`, "确认清理", { type: "warning" }); const result = await api.cleanupRetrievalLogs(cleanupBefore.value); ElMessage.success(`已清理 ${result.deleted} 条日志`); await loadCurrentMenu(); }
|
||||
async function saveRetention() { await api.saveRetrievalRetention(retentionDays.value); ElMessage.success(retentionDays.value == null ? "已设为永久保留" : `已设为保留 ${retentionDays.value} 天`); await loadCurrentMenu(); }
|
||||
|
||||
@@ -629,6 +641,14 @@ async function quickAddModel() {
|
||||
extraParams: null,
|
||||
remark: "快速添加",
|
||||
timeoutSecond: 30,
|
||||
inputPricePer1k: null,
|
||||
outputPricePer1k: null,
|
||||
currency: "CNY",
|
||||
usageScenarios: "正式对话",
|
||||
allowSummary: 1,
|
||||
allowReport: 1,
|
||||
allowFixedInfo: 1,
|
||||
allowDeepChat: 1,
|
||||
});
|
||||
ElMessage.success(`${provider.label} 模型已新增`);
|
||||
quickModelForm.apiKey = "";
|
||||
@@ -715,6 +735,14 @@ function editModel(row: ModelItem) {
|
||||
extraParams: row.extraParams ?? "",
|
||||
remark: row.remark ?? "",
|
||||
timeoutSecond: row.timeoutSecond,
|
||||
inputPricePer1k: row.inputPricePer1k ?? null,
|
||||
outputPricePer1k: row.outputPricePer1k ?? null,
|
||||
currency: row.currency || "CNY",
|
||||
usageScenarios: row.usageScenarios ?? "",
|
||||
allowSummary: row.allowSummary ?? 1,
|
||||
allowReport: row.allowReport ?? 1,
|
||||
allowFixedInfo: row.allowFixedInfo ?? 1,
|
||||
allowDeepChat: row.allowDeepChat ?? 1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -742,6 +770,14 @@ function resetModelForm() {
|
||||
extraParams: "",
|
||||
remark: "",
|
||||
timeoutSecond: 30,
|
||||
inputPricePer1k: null,
|
||||
outputPricePer1k: null,
|
||||
currency: "CNY",
|
||||
usageScenarios: "",
|
||||
allowSummary: 1,
|
||||
allowReport: 1,
|
||||
allowFixedInfo: 1,
|
||||
allowDeepChat: 1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1001,6 +1037,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
<div class="stat"><span>输入 Token</span><strong>{{ stats?.inputToken ?? 0 }}</strong></div>
|
||||
<div class="stat"><span>输出 Token</span><strong>{{ stats?.outputToken ?? 0 }}</strong></div>
|
||||
<div class="stat"><span>总 Token</span><strong>{{ stats?.totalToken ?? 0 }}</strong></div>
|
||||
<div class="stat"><span>估算成本</span><strong>{{ formatMoney(stats?.estimatedCost, stats?.costCurrency || 'CNY') }}</strong></div>
|
||||
</div>
|
||||
<section class="storage-panel" v-loading="storageLoading"><div class="storage-panel-head"><div><h3>项目存储占用</h3><p>数据库、Redis 与附件分别统计;无权限或不支持时显示“无法统计”。</p></div><el-button :loading="storageLoading" @click="refreshStorage">手动刷新</el-button></div><div class="storage-grid"><div><span>项目总量</span><strong>{{ formatBytes(storageStats?.totalBytes) }}</strong></div><div><span>数据库</span><strong>{{ formatBytes(storageStats?.detail?.database?.bytes) }}</strong></div><div><span>Redis</span><strong>{{ formatBytes(storageStats?.detail?.redis?.bytes) }}</strong></div><div><span>附件/文件</span><strong>{{ formatBytes(storageStats?.detail?.files?.bytes) }}</strong></div><div><span>近7天增长</span><strong>{{ formatBytes(storageStats?.growth7DaysBytes) }}</strong></div><div><span>近30天增长</span><strong>{{ formatBytes(storageStats?.growth30DaysBytes) }}</strong></div></div><small>最后统计:{{ storageStats?.createdAt || '尚未统计' }}</small></section>
|
||||
</template>
|
||||
@@ -1292,7 +1329,27 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
<el-form-item label="超时秒数">
|
||||
<el-input-number v-model="modelForm.timeoutSecond" :min="1" :max="300" />
|
||||
</el-form-item>
|
||||
<el-form-item label="输入单价/千Token">
|
||||
<el-input-number v-model="modelForm.inputPricePer1k" :min="0" :precision="6" :step="0.001" placeholder="不填则不估算成本" />
|
||||
</el-form-item>
|
||||
<el-form-item label="输出单价/千Token">
|
||||
<el-input-number v-model="modelForm.outputPricePer1k" :min="0" :precision="6" :step="0.001" placeholder="不填则不估算成本" />
|
||||
</el-form-item>
|
||||
<el-form-item label="币种">
|
||||
<el-input v-model="modelForm.currency" placeholder="CNY / USD" maxlength="10" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="适用场景">
|
||||
<el-input v-model="modelForm.usageScenarios" placeholder="例如:正式对话、摘要、固定信息、报告批处理" />
|
||||
</el-form-item>
|
||||
<el-form-item label="可用能力">
|
||||
<div class="entitlement-capabilities">
|
||||
<el-checkbox v-model="modelForm.allowFixedInfo" :true-value="1" :false-value="0">固定信息</el-checkbox>
|
||||
<el-checkbox v-model="modelForm.allowDeepChat" :true-value="1" :false-value="0">深度对话</el-checkbox>
|
||||
<el-checkbox v-model="modelForm.allowSummary" :true-value="1" :false-value="0">摘要沉淀</el-checkbox>
|
||||
<el-checkbox v-model="modelForm.allowReport" :true-value="1" :false-value="0">周期报告</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="modelForm.remark" placeholder="用途、额度、注意事项等" />
|
||||
</el-form-item>
|
||||
@@ -1306,6 +1363,10 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
<el-table-column prop="provider" label="Provider" />
|
||||
<el-table-column prop="apiType" label="协议" width="170" />
|
||||
<el-table-column prop="modelName" label="模型" />
|
||||
<el-table-column label="估算单价" width="180">
|
||||
<template #default="{ row }">{{ row.currency || 'CNY' }} {{ row.inputPricePer1k ?? '-' }}/{{ row.outputPricePer1k ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="usageScenarios" label="适用场景" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="baseUrl" label="Base URL" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="authType" label="鉴权" width="110" />
|
||||
<el-table-column prop="enabled" label="启用" width="90" />
|
||||
@@ -1521,6 +1582,9 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalToken" label="Token" width="90" />
|
||||
<el-table-column label="估算成本" width="130">
|
||||
<template #default="{ row }">{{ formatMoney(row.estimatedCost, row.currency || 'CNY') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="costMs" label="耗时(ms)" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="100" />
|
||||
<el-table-column prop="errorMessage" label="错误" min-width="220" show-overflow-tooltip />
|
||||
@@ -1726,6 +1790,10 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
<span>输入 Token:{{ selectedAiLog.inputToken || '-' }}</span>
|
||||
<span>输出 Token:{{ selectedAiLog.outputToken || '-' }}</span>
|
||||
<span>总 Token:{{ selectedAiLog.totalToken || '-' }}</span>
|
||||
<span>估算成本:{{ formatMoney(selectedAiLog.estimatedCost, selectedAiLog.currency || 'CNY') }}</span>
|
||||
<span>问题类型:{{ selectedAiLog.questionType || '-' }}</span>
|
||||
<span>知识命中:{{ selectedAiLog.knowledgeHit ? '是' : '否' }}</span>
|
||||
<span>路由原因:{{ selectedAiLog.routeReason || '-' }}</span>
|
||||
<span>耗时:{{ selectedAiLog.costMs || '-' }}ms</span>
|
||||
<span>时间:{{ selectedAiLog.createdAt }}</span>
|
||||
</section>
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface DashboardStats {
|
||||
inputToken: number;
|
||||
outputToken: number;
|
||||
totalToken: number;
|
||||
estimatedCost?: number | null;
|
||||
costCurrency?: string | null;
|
||||
}
|
||||
|
||||
export interface PromptDetail {
|
||||
@@ -164,6 +166,14 @@ export interface ModelItem {
|
||||
remark?: string | null;
|
||||
timeoutSecond: number;
|
||||
enabled: number;
|
||||
inputPricePer1k?: number | null;
|
||||
outputPricePer1k?: number | null;
|
||||
currency: string;
|
||||
usageScenarios?: string | null;
|
||||
allowSummary: number;
|
||||
allowReport: number;
|
||||
allowFixedInfo: number;
|
||||
allowDeepChat: number;
|
||||
}
|
||||
|
||||
export interface AgentGenerationConfig {
|
||||
@@ -376,6 +386,11 @@ export interface AiLogRecord {
|
||||
outputToken?: number | null;
|
||||
totalToken?: number | null;
|
||||
costMs?: number | null;
|
||||
estimatedCost?: number | null;
|
||||
currency?: string | null;
|
||||
routeReason?: string | null;
|
||||
questionType?: string | null;
|
||||
knowledgeHit?: boolean;
|
||||
status: string;
|
||||
errorMessage?: string | null;
|
||||
prompt?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user