213 lines
5.5 KiB
JavaScript
213 lines
5.5 KiB
JavaScript
// exe-engine/src/meter/resource-meter.js
|
|
// EXE-Engine · 资源计量器
|
|
// 实时记录每次调用的 token 消耗、模型选择、响应时间
|
|
// PRJ-EXE-001 · Phase 0 · ZY-EXE-P0-003
|
|
// 版权:国作登字-2026-A-00037559
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const LOG_DIR = path.resolve(__dirname, '../../logs');
|
|
const METER_LOG = path.join(LOG_DIR, 'meter.json');
|
|
|
|
/**
|
|
* 资源计量器
|
|
*
|
|
* 本体论锚定:计量器 = 量墨水的刻度。
|
|
* 每一笔用了多少墨水,花了多少钱,都要记下来。
|
|
* 数据回流至 DC v1.0 采集系统,为算力资源模型提供计费基础数据。
|
|
*/
|
|
class ResourceMeter {
|
|
constructor() {
|
|
this._records = [];
|
|
this._summary = {
|
|
totalRequests: 0,
|
|
totalInputTokens: 0,
|
|
totalOutputTokens: 0,
|
|
totalCost: 0,
|
|
byModel: {},
|
|
byAgent: {},
|
|
byPool: {}
|
|
};
|
|
this._ensureLogDir();
|
|
this._loadExisting();
|
|
}
|
|
|
|
/**
|
|
* 记录一次调用消耗
|
|
*
|
|
* @param {object} record
|
|
* @param {string} record.requestId 请求 ID
|
|
* @param {string} record.agentId Agent ID
|
|
* @param {string} record.model 使用的模型
|
|
* @param {string} record.taskType 任务类型
|
|
* @param {string} record.resourcePool 资源池 ID
|
|
* @param {string} record.mode 接入模式 (byok | pool | hybrid)
|
|
* @param {number} record.inputTokens 输入 token 数
|
|
* @param {number} record.outputTokens 输出 token 数
|
|
* @param {number} record.cost 成本
|
|
* @param {string} record.costUnit 成本单位
|
|
* @param {number} record.latency 延迟 (ms)
|
|
* @param {string} record.status 状态 (success | error)
|
|
*/
|
|
record(record) {
|
|
const entry = {
|
|
...record,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
this._records.push(entry);
|
|
this._updateSummary(entry);
|
|
this._persist();
|
|
}
|
|
|
|
/**
|
|
* 获取计量摘要
|
|
* @returns {object}
|
|
*/
|
|
getSummary() {
|
|
return {
|
|
...this._summary,
|
|
recordCount: this._records.length,
|
|
lastUpdated: this._records.length > 0
|
|
? this._records[this._records.length - 1].timestamp
|
|
: null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 获取指定 Agent 的消耗统计
|
|
* @param {string} agentId
|
|
* @returns {object|null}
|
|
*/
|
|
getAgentUsage(agentId) {
|
|
return this._summary.byAgent[agentId] || null;
|
|
}
|
|
|
|
/**
|
|
* 获取指定资源池的消耗统计
|
|
* @param {string} poolId
|
|
* @returns {object|null}
|
|
*/
|
|
getPoolUsage(poolId) {
|
|
return this._summary.byPool[poolId] || null;
|
|
}
|
|
|
|
/**
|
|
* 获取最近 N 条记录
|
|
* @param {number} n
|
|
* @returns {object[]}
|
|
*/
|
|
getRecentRecords(n = 20) {
|
|
return this._records.slice(-n);
|
|
}
|
|
|
|
/**
|
|
* 清理过期记录(保留最近 500 条)
|
|
*/
|
|
cleanup() {
|
|
if (this._records.length > 500) {
|
|
this._records = this._records.slice(-500);
|
|
this._rebuildSummary();
|
|
this._persist();
|
|
}
|
|
}
|
|
|
|
// ── 内部方法 ──
|
|
|
|
_updateSummary(entry) {
|
|
this._summary.totalRequests++;
|
|
this._summary.totalInputTokens += entry.inputTokens || 0;
|
|
this._summary.totalOutputTokens += entry.outputTokens || 0;
|
|
this._summary.totalCost += entry.cost || 0;
|
|
|
|
// 按模型统计
|
|
if (entry.model) {
|
|
if (!this._summary.byModel[entry.model]) {
|
|
this._summary.byModel[entry.model] = { requests: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
|
}
|
|
const m = this._summary.byModel[entry.model];
|
|
m.requests++;
|
|
m.inputTokens += entry.inputTokens || 0;
|
|
m.outputTokens += entry.outputTokens || 0;
|
|
m.cost += entry.cost || 0;
|
|
}
|
|
|
|
// 按 Agent 统计
|
|
if (entry.agentId) {
|
|
if (!this._summary.byAgent[entry.agentId]) {
|
|
this._summary.byAgent[entry.agentId] = { requests: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
|
}
|
|
const a = this._summary.byAgent[entry.agentId];
|
|
a.requests++;
|
|
a.inputTokens += entry.inputTokens || 0;
|
|
a.outputTokens += entry.outputTokens || 0;
|
|
a.cost += entry.cost || 0;
|
|
}
|
|
|
|
// 按资源池统计
|
|
if (entry.resourcePool) {
|
|
if (!this._summary.byPool[entry.resourcePool]) {
|
|
this._summary.byPool[entry.resourcePool] = { requests: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
|
}
|
|
const p = this._summary.byPool[entry.resourcePool];
|
|
p.requests++;
|
|
p.inputTokens += entry.inputTokens || 0;
|
|
p.outputTokens += entry.outputTokens || 0;
|
|
p.cost += entry.cost || 0;
|
|
}
|
|
}
|
|
|
|
_rebuildSummary() {
|
|
this._summary = {
|
|
totalRequests: 0,
|
|
totalInputTokens: 0,
|
|
totalOutputTokens: 0,
|
|
totalCost: 0,
|
|
byModel: {},
|
|
byAgent: {},
|
|
byPool: {}
|
|
};
|
|
for (const entry of this._records) {
|
|
this._updateSummary(entry);
|
|
}
|
|
}
|
|
|
|
_ensureLogDir() {
|
|
if (!fs.existsSync(LOG_DIR)) {
|
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
_persist() {
|
|
try {
|
|
fs.writeFileSync(METER_LOG, JSON.stringify({
|
|
records: this._records.slice(-500),
|
|
summary: this._summary
|
|
}, null, 2));
|
|
} catch (err) {
|
|
this._lastPersistError = err.message;
|
|
}
|
|
}
|
|
|
|
_loadExisting() {
|
|
try {
|
|
if (fs.existsSync(METER_LOG)) {
|
|
const data = JSON.parse(fs.readFileSync(METER_LOG, 'utf-8'));
|
|
this._records = data.records || [];
|
|
if (data.summary) {
|
|
this._summary = data.summary;
|
|
} else {
|
|
this._rebuildSummary();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
this._lastLoadError = err.message;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = ResourceMeter;
|