opencode-metrics-plugin 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +80 -53
- package/assets/agc-apiclient.json +10 -0
- package/dist/api.d.ts +3 -0
- package/dist/api.js +4 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.js +2 -3
- package/dist/metrics/configLoader.d.ts +20 -0
- package/dist/metrics/configLoader.js +99 -0
- package/dist/metrics/dirs.d.ts +2 -0
- package/dist/metrics/dirs.js +1 -0
- package/dist/metrics/engine/engine.d.ts +8 -4
- package/dist/metrics/engine/engine.js +105 -27
- package/dist/metrics/engine/state.d.ts +17 -0
- package/dist/metrics/engine/state.js +2 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.js +1 -0
- package/dist/metrics/runtime.d.ts +4 -0
- package/dist/metrics/runtime.js +10 -0
- package/dist/metrics/snapshot/flush.d.ts +17 -0
- package/dist/metrics/snapshot/flush.js +34 -4
- package/dist/metrics/snapshot/merge.d.ts +10 -2
- package/dist/metrics/snapshot/merge.js +47 -14
- package/dist/metrics/snapshot/steps.d.ts +15 -1
- package/dist/metrics/snapshot/steps.js +112 -48
- package/dist/metrics/upload/agcUploader.d.ts +65 -0
- package/dist/metrics/upload/agcUploader.js +244 -0
- package/dist/metrics/upload/packageArchiver.d.ts +34 -0
- package/dist/metrics/upload/packageArchiver.js +213 -0
- package/dist/metrics/upload/scenarios.d.ts +20 -0
- package/dist/metrics/upload/scenarios.js +43 -0
- package/dist/metrics/upload/types.d.ts +111 -0
- package/dist/metrics/upload/types.js +43 -0
- package/dist/metrics/upload/uploadManager.d.ts +25 -0
- package/dist/metrics/upload/uploadManager.js +252 -0
- package/dist/metrics/upload/uploadQueue.d.ts +34 -0
- package/dist/metrics/upload/uploadQueue.js +77 -0
- package/dist/metrics/upload/workspaceResolver.d.ts +75 -0
- package/dist/metrics/upload/workspaceResolver.js +275 -0
- package/dist/plugin.d.ts +8 -5
- package/dist/plugin.js +12 -7
- package/dist/shared/log.d.ts +0 -1
- package/dist/shared/log.js +0 -3
- package/package.json +8 -2
package/dist/metrics/index.d.ts
CHANGED
|
@@ -6,4 +6,7 @@ export { createEventLogger } from './eventlog/event-logger.js';
|
|
|
6
6
|
export type { EventLogger, EventLoggerOptions } from './eventlog/event-logger.js';
|
|
7
7
|
export { createMetricsRuntime } from './runtime.js';
|
|
8
8
|
export type { MetricsRuntime, MetricsRuntimeOptions } from './runtime.js';
|
|
9
|
+
export { createUploadManager } from './upload/uploadManager.js';
|
|
10
|
+
export type { UploadManager, UploadManagerOptions } from './upload/uploadManager.js';
|
|
11
|
+
export type { UploadConfig, ScenarioConfig, ArtifactRule, UploadPackage } from './upload/types.js';
|
|
9
12
|
export * from './types.js';
|
package/dist/metrics/index.js
CHANGED
|
@@ -3,4 +3,5 @@ export { createMetricsEngine } from './engine/engine.js';
|
|
|
3
3
|
export { configureDirs, defaultDirs, getDirs, resolveDirs, getMetricsDir, getEventsDir, getLogFile } from './dirs.js';
|
|
4
4
|
export { createEventLogger } from './eventlog/event-logger.js';
|
|
5
5
|
export { createMetricsRuntime } from './runtime.js';
|
|
6
|
+
export { createUploadManager } from './upload/uploadManager.js';
|
|
6
7
|
export * from './types.js';
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { MetricsEngineOptions } from './engine/engine.js';
|
|
2
2
|
import type { MetricsDirs } from './dirs.js';
|
|
3
|
+
import type { UploadConfig } from './upload/types.js';
|
|
3
4
|
export interface MetricsRuntimeOptions extends Omit<MetricsEngineOptions, 'enabled'> {
|
|
4
5
|
dirs?: Partial<MetricsDirs>;
|
|
5
6
|
enabled?: boolean;
|
|
6
7
|
/** 事件写盘(events/<sessionId>.log;steps 全文的数据源) */
|
|
7
8
|
eventLogging?: boolean;
|
|
9
|
+
/** 上传子系统配置(DESIGN.md §13.8);enabled:true 才会创建管理器 */
|
|
10
|
+
upload?: UploadConfig;
|
|
8
11
|
}
|
|
9
12
|
/**
|
|
10
13
|
* 组装一整套"通用统计插件"的运行时:事件记录 + 指标引擎。
|
|
@@ -14,6 +17,7 @@ export declare function createMetricsRuntime(opts?: MetricsRuntimeOptions): {
|
|
|
14
17
|
dirs: MetricsDirs;
|
|
15
18
|
eventLogger: import("./eventlog/event-logger.js").EventLogger | null;
|
|
16
19
|
engine: import("./types.js").MetricsEngine;
|
|
20
|
+
uploadManager: import("./upload/uploadManager.js").UploadManager | null;
|
|
17
21
|
/** opencode event hook:`async ({ event }) => { runtime.event({ event }) }` */
|
|
18
22
|
event(input: {
|
|
19
23
|
event: {
|
package/dist/metrics/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createMetricsEngine } from './engine/engine.js';
|
|
2
2
|
import { createEventLogger } from './eventlog/event-logger.js';
|
|
3
3
|
import { resolveDirs } from './dirs.js';
|
|
4
|
+
import { createUploadManager } from './upload/uploadManager.js';
|
|
4
5
|
/**
|
|
5
6
|
* 组装一整套"通用统计插件"的运行时:事件记录 + 指标引擎。
|
|
6
7
|
* 目录为实例级(不产生全局副作用):defaultDirs → 进程级默认(configureDirs)→ opts.dirs 三层合并。
|
|
@@ -11,18 +12,27 @@ export function createMetricsRuntime(opts = {}) {
|
|
|
11
12
|
enabled: opts.enabled,
|
|
12
13
|
dirs,
|
|
13
14
|
onFlush: opts.onFlush,
|
|
15
|
+
checkpointFlushIntervalMs: opts.checkpointFlushIntervalMs,
|
|
14
16
|
});
|
|
15
17
|
const eventLogger = (opts.eventLogging ?? true)
|
|
16
18
|
? createEventLogger(true, false, { eventsDir: dirs.eventsDir })
|
|
17
19
|
: null;
|
|
20
|
+
// 上传管理器旁路挂在 runtime.event 上(在 engine.ingest 之后,保证触发时快照已落盘)
|
|
21
|
+
const uploadManager = createUploadManager({
|
|
22
|
+
config: opts.upload,
|
|
23
|
+
dirs,
|
|
24
|
+
onBeforeArchive: () => eventLogger?.flush(),
|
|
25
|
+
});
|
|
18
26
|
return {
|
|
19
27
|
dirs,
|
|
20
28
|
eventLogger,
|
|
21
29
|
engine,
|
|
30
|
+
uploadManager,
|
|
22
31
|
/** opencode event hook:`async ({ event }) => { runtime.event({ event }) }` */
|
|
23
32
|
event(input) {
|
|
24
33
|
eventLogger?.log(input.event);
|
|
25
34
|
engine.ingest(input.event);
|
|
35
|
+
uploadManager?.observe(input.event);
|
|
26
36
|
},
|
|
27
37
|
};
|
|
28
38
|
}
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import type { MetricsDirs } from "../dirs.js";
|
|
2
2
|
import type { SessionMetricsState } from "../engine/state.js";
|
|
3
3
|
import type { MetricsOutput } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* 把会话当前状态落盘为快照文件(读旧文件 → 合并 → 原子覆盖写)。
|
|
6
|
+
* 可重复调用,不会造成数据重复累计。
|
|
7
|
+
*
|
|
8
|
+
* @param sessionId 会话 id,快照文件名为 `<sessionId>.json`
|
|
9
|
+
* @param state 会话状态,写盘成功后原地刷新差量基线
|
|
10
|
+
* @param now 本次快照的结束时间(ms)
|
|
11
|
+
* @param dirs 输出目录;缺省用进程级默认目录
|
|
12
|
+
* @returns 本次由内存态构建的快照(未与旧文件合并的那份)
|
|
13
|
+
*/
|
|
4
14
|
declare function flushMetrics(sessionId: string, state: SessionMetricsState, now: number, dirs?: Partial<MetricsDirs>): MetricsOutput;
|
|
15
|
+
/**
|
|
16
|
+
* 处理一轮会话结束:保存本轮 round 快照并落盘。
|
|
17
|
+
*
|
|
18
|
+
* @param state 会话状态(就地结算与重置;重复 idle 直接跳过)
|
|
19
|
+
* @param sessionId 会话 id
|
|
20
|
+
* @param dirs 输出目录;缺省用进程级默认目录
|
|
21
|
+
*/
|
|
5
22
|
declare function handleSessionIdle(state: SessionMetricsState, sessionId: string, dirs?: Partial<MetricsDirs>): MetricsOutput | undefined;
|
|
6
23
|
export { flushMetrics, handleSessionIdle };
|
|
@@ -14,12 +14,25 @@ function filterByKeys(source, keys) {
|
|
|
14
14
|
}
|
|
15
15
|
return result;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* 把会话当前状态落盘为快照文件(读旧文件 → 合并 → 原子覆盖写)。
|
|
19
|
+
* 可重复调用,不会造成数据重复累计。
|
|
20
|
+
*
|
|
21
|
+
* @param sessionId 会话 id,快照文件名为 `<sessionId>.json`
|
|
22
|
+
* @param state 会话状态,写盘成功后原地刷新差量基线
|
|
23
|
+
* @param now 本次快照的结束时间(ms)
|
|
24
|
+
* @param dirs 输出目录;缺省用进程级默认目录
|
|
25
|
+
* @returns 本次由内存态构建的快照(未与旧文件合并的那份)
|
|
26
|
+
*/
|
|
17
27
|
function flushMetrics(sessionId, state, now, dirs) {
|
|
18
28
|
// Finalize agent usage for current agent
|
|
19
29
|
if (state.agent.current) {
|
|
20
30
|
const elapsed = now - state.agent.lastSwitchTime;
|
|
21
|
-
const
|
|
22
|
-
state.agent.usage.
|
|
31
|
+
const agent = state.agent.current;
|
|
32
|
+
const prev = state.agent.usage.get(agent) ?? 0;
|
|
33
|
+
state.agent.usage.set(agent, prev + elapsed);
|
|
34
|
+
// 重置计时起点,避免下次 flush 重复累计这段时长
|
|
35
|
+
state.agent.lastSwitchTime = now;
|
|
23
36
|
}
|
|
24
37
|
const endTime = now;
|
|
25
38
|
const duration = endTime - state.startTime;
|
|
@@ -318,20 +331,36 @@ function flushMetrics(sessionId, state, now, dirs) {
|
|
|
318
331
|
try {
|
|
319
332
|
const existingRaw = fs.readFileSync(filePath, "utf-8");
|
|
320
333
|
const existingOutput = JSON.parse(existingRaw);
|
|
321
|
-
|
|
334
|
+
// 同进程多次 flush(checkpoint)按差量并入,跨进程 baseline 为空(全量并入)
|
|
335
|
+
merged = { ...mergeMetricsOutput(existingOutput, output, state._flushBaseline), source: "live" };
|
|
322
336
|
}
|
|
323
337
|
catch {
|
|
324
|
-
// File doesn't exist or is corrupted
|
|
338
|
+
// File doesn't exist or is corrupted — use fresh output
|
|
325
339
|
}
|
|
326
340
|
const tmpPath = filePath + ".tmp";
|
|
327
341
|
fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2));
|
|
328
342
|
fs.renameSync(tmpPath, filePath);
|
|
343
|
+
// 写盘成功后刷新差量基线
|
|
344
|
+
state._flushBaseline = {
|
|
345
|
+
errorCodeCounts: Object.fromEntries(output.codeStats.errorCodes.map(e => [e.code, e.count])),
|
|
346
|
+
warningCounts: { ...output.codeStats.warnings.byType },
|
|
347
|
+
warningEntriesTotal: output.codeStats.warnings.entries.length,
|
|
348
|
+
moduleTimings: Object.fromEntries(output.codeStats.moduleTimings.map(m => [m.module, { totalDuration: m.totalDurationMs, taskCount: m.taskCount }])),
|
|
349
|
+
fixCycleCount: output.codeStats.fixCycles.cycles.length,
|
|
350
|
+
};
|
|
329
351
|
}
|
|
330
352
|
catch (err) {
|
|
331
353
|
log.info("Failed to flush metrics", { sessionId, error: String(err) });
|
|
332
354
|
}
|
|
333
355
|
return output;
|
|
334
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* 处理一轮会话结束:保存本轮 round 快照并落盘。
|
|
359
|
+
*
|
|
360
|
+
* @param state 会话状态(就地结算与重置;重复 idle 直接跳过)
|
|
361
|
+
* @param sessionId 会话 id
|
|
362
|
+
* @param dirs 输出目录;缺省用进程级默认目录
|
|
363
|
+
*/
|
|
335
364
|
function handleSessionIdle(state, sessionId, dirs) {
|
|
336
365
|
const now = Date.now();
|
|
337
366
|
// Build round snapshot
|
|
@@ -384,6 +413,7 @@ function handleSessionIdle(state, sessionId, dirs) {
|
|
|
384
413
|
// Flush metrics to disk
|
|
385
414
|
state.endTime = now;
|
|
386
415
|
state._idleFlushed = true;
|
|
416
|
+
state._checkpointDirty = false;
|
|
387
417
|
return flushMetrics(sessionId, state, now, dirs);
|
|
388
418
|
}
|
|
389
419
|
export { flushMetrics, handleSessionIdle };
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { MetricsOutput } from "../types.js";
|
|
2
|
-
import type { SessionMetricsState } from "../engine/state.js";
|
|
2
|
+
import type { FlushBaseline, SessionMetricsState } from "../engine/state.js";
|
|
3
3
|
declare function mergeChildMetrics(parent: SessionMetricsState, child: SessionMetricsState, meta: {
|
|
4
4
|
childId: string;
|
|
5
5
|
agentName: string;
|
|
6
6
|
title: string;
|
|
7
7
|
}): void;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* 合并磁盘旧快照与内存新快照:覆盖类字段取新值,累加类字段只补差量。
|
|
10
|
+
*
|
|
11
|
+
* @param existing 磁盘上的旧快照;null 时直接返回 fresh
|
|
12
|
+
* @param fresh 由当前内存态构建的新快照
|
|
13
|
+
* @param baseline 上次 flush 时累加字段的累计值,用于计算本次差量;null 时累加字段全量并入
|
|
14
|
+
* @returns 合并后的快照,可安全反复写盘
|
|
15
|
+
*/
|
|
16
|
+
export declare function mergeMetricsOutput(existing: MetricsOutput | null, fresh: MetricsOutput, baseline?: FlushBaseline | null): MetricsOutput;
|
|
9
17
|
export { mergeChildMetrics };
|
|
@@ -123,7 +123,15 @@ function mergeChildMetrics(parent, child, meta) {
|
|
|
123
123
|
// Merge planning data
|
|
124
124
|
parent.planningCalls.push(...child.planningCalls);
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
/**
|
|
127
|
+
* 合并磁盘旧快照与内存新快照:覆盖类字段取新值,累加类字段只补差量。
|
|
128
|
+
*
|
|
129
|
+
* @param existing 磁盘上的旧快照;null 时直接返回 fresh
|
|
130
|
+
* @param fresh 由当前内存态构建的新快照
|
|
131
|
+
* @param baseline 上次 flush 时累加字段的累计值,用于计算本次差量;null 时累加字段全量并入
|
|
132
|
+
* @returns 合并后的快照,可安全反复写盘
|
|
133
|
+
*/
|
|
134
|
+
export function mergeMetricsOutput(existing, fresh, baseline) {
|
|
127
135
|
if (!existing)
|
|
128
136
|
return fresh;
|
|
129
137
|
// startTime: min of both
|
|
@@ -191,53 +199,78 @@ export function mergeMetricsOutput(existing, fresh) {
|
|
|
191
199
|
for (const s of fresh.skillSearches)
|
|
192
200
|
searchMap.set(`${s.query}:${s.skillPath}`, s);
|
|
193
201
|
const skillSearches = [...searchMap.values()];
|
|
194
|
-
// codeStats.errorCodes: merge by code
|
|
202
|
+
// codeStats.errorCodes: merge by code with per-code increment(差量 = fresh - baseline)
|
|
195
203
|
const errorCodeMap = new Map();
|
|
196
204
|
for (const e of existing.codeStats.errorCodes)
|
|
197
205
|
errorCodeMap.set(e.code, { ...e });
|
|
198
206
|
for (const e of fresh.codeStats.errorCodes) {
|
|
207
|
+
const increment = Math.max(0, e.count - (baseline?.errorCodeCounts[e.code] ?? 0));
|
|
208
|
+
if (increment === 0)
|
|
209
|
+
continue;
|
|
199
210
|
const prev = errorCodeMap.get(e.code);
|
|
200
211
|
if (prev) {
|
|
201
|
-
prev.count +=
|
|
212
|
+
prev.count += increment;
|
|
202
213
|
// keep existing type/message
|
|
203
214
|
}
|
|
204
215
|
else {
|
|
205
|
-
errorCodeMap.set(e.code, { ...e });
|
|
216
|
+
errorCodeMap.set(e.code, { ...e, count: increment });
|
|
206
217
|
}
|
|
207
218
|
}
|
|
208
219
|
const errorCodes = [...errorCodeMap.values()];
|
|
209
|
-
// codeStats.warnings: merge by type
|
|
220
|
+
// codeStats.warnings: merge by type with increment; entries 只并入新增尾部(fresh 侧数组只追加)
|
|
210
221
|
const warnByType = { ...existing.codeStats.warnings.byType };
|
|
211
222
|
const warnEntries = [...existing.codeStats.warnings.entries];
|
|
212
223
|
let warnTotal = existing.codeStats.warnings.total;
|
|
213
224
|
for (const [type, count] of Object.entries(fresh.codeStats.warnings.byType)) {
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
const increment = Math.max(0, count - (baseline?.warningCounts[type] ?? 0));
|
|
226
|
+
if (increment === 0)
|
|
227
|
+
continue;
|
|
228
|
+
warnByType[type] = (warnByType[type] || 0) + increment;
|
|
229
|
+
warnTotal += increment;
|
|
230
|
+
}
|
|
231
|
+
const warnEntriesBaseline = baseline?.warningEntriesTotal ?? 0;
|
|
232
|
+
if (fresh.codeStats.warnings.entries.length > warnEntriesBaseline) {
|
|
233
|
+
warnEntries.push(...fresh.codeStats.warnings.entries.slice(warnEntriesBaseline));
|
|
216
234
|
}
|
|
217
|
-
warnEntries.push(...fresh.codeStats.warnings.entries);
|
|
218
235
|
const warnings = { total: warnTotal, byType: warnByType, entries: warnEntries };
|
|
219
|
-
// codeStats.moduleTimings: merge by module
|
|
236
|
+
// codeStats.moduleTimings: merge by module with duration/taskCount increment, max slowestTask
|
|
220
237
|
const moduleMap = new Map();
|
|
221
238
|
for (const m of existing.codeStats.moduleTimings)
|
|
222
239
|
moduleMap.set(m.module, { ...m, slowestTask: { ...m.slowestTask } });
|
|
223
240
|
for (const m of fresh.codeStats.moduleTimings) {
|
|
241
|
+
const base = baseline?.moduleTimings[m.module];
|
|
242
|
+
const durationIncrement = Math.max(0, m.totalDurationMs - (base?.totalDuration ?? 0));
|
|
243
|
+
const taskIncrement = Math.max(0, m.taskCount - (base?.taskCount ?? 0));
|
|
244
|
+
if (durationIncrement === 0 && taskIncrement === 0)
|
|
245
|
+
continue;
|
|
224
246
|
const prev = moduleMap.get(m.module);
|
|
225
247
|
if (prev) {
|
|
226
|
-
prev.totalDurationMs +=
|
|
227
|
-
prev.taskCount +=
|
|
248
|
+
prev.totalDurationMs += durationIncrement;
|
|
249
|
+
prev.taskCount += taskIncrement;
|
|
228
250
|
if (m.slowestTask.duration > prev.slowestTask.duration) {
|
|
229
251
|
prev.slowestTask = { ...m.slowestTask };
|
|
230
252
|
}
|
|
231
253
|
}
|
|
232
254
|
else {
|
|
233
|
-
moduleMap.set(m.module, {
|
|
255
|
+
moduleMap.set(m.module, {
|
|
256
|
+
module: m.module,
|
|
257
|
+
totalDurationMs: durationIncrement,
|
|
258
|
+
taskCount: taskIncrement,
|
|
259
|
+
slowestTask: { ...m.slowestTask },
|
|
260
|
+
});
|
|
234
261
|
}
|
|
235
262
|
}
|
|
236
263
|
const moduleTimings = [...moduleMap.values()];
|
|
237
|
-
// codeStats.fixCycles:
|
|
264
|
+
// codeStats.fixCycles: existing 全保留 + fresh 增量尾部(fresh 侧 cycles 只追加)
|
|
265
|
+
const fixCycleBaseline = baseline?.fixCycleCount ?? 0;
|
|
238
266
|
const fixCycles = {
|
|
239
267
|
...fresh.codeStats.fixCycles,
|
|
240
|
-
cycles: [
|
|
268
|
+
cycles: [
|
|
269
|
+
...existing.codeStats.fixCycles.cycles,
|
|
270
|
+
...(fresh.codeStats.fixCycles.cycles.length > fixCycleBaseline
|
|
271
|
+
? fresh.codeStats.fixCycles.cycles.slice(fixCycleBaseline)
|
|
272
|
+
: []),
|
|
273
|
+
],
|
|
241
274
|
};
|
|
242
275
|
// codeStats.firstBuildPerRound: fresh accumulates all rounds, use fresh
|
|
243
276
|
const firstBuildPerRound = fresh.codeStats.firstBuildPerRound;
|
|
@@ -8,6 +8,13 @@ declare function buildSteps(maps: {
|
|
|
8
8
|
reasoningMap: Map<string, string>;
|
|
9
9
|
toolOutputMap: Map<string, string>;
|
|
10
10
|
}): StepData[];
|
|
11
|
+
/**
|
|
12
|
+
* 从会话事件日志解析全文内容(text/reasoning/工具输出),增量读取只解析新增字节。
|
|
13
|
+
*
|
|
14
|
+
* @param sessionId 会话 id,日志文件名为 `<sessionId>.log`
|
|
15
|
+
* @param opts eventsDir 指定日志目录;filterMsgIds 只收集这些消息
|
|
16
|
+
* @returns textMap/reasoningMap(按 messageID)与 toolOutputMap(按 callID);日志不存在时为空表
|
|
17
|
+
*/
|
|
11
18
|
declare function extractStepContent(sessionId: string, opts?: {
|
|
12
19
|
eventsDir?: string;
|
|
13
20
|
filterMsgIds?: Set<string>;
|
|
@@ -16,4 +23,11 @@ declare function extractStepContent(sessionId: string, opts?: {
|
|
|
16
23
|
reasoningMap: Map<string, string>;
|
|
17
24
|
toolOutputMap: Map<string, string>;
|
|
18
25
|
};
|
|
19
|
-
|
|
26
|
+
/**
|
|
27
|
+
* 清除事件日志的增量解析缓存。
|
|
28
|
+
*
|
|
29
|
+
* @param sessionId 只清该会话的缓存;缺省清空全部
|
|
30
|
+
* @param eventsDir 日志目录,须与 extractStepContent 使用的路径一致
|
|
31
|
+
*/
|
|
32
|
+
declare function clearStepContentCache(sessionId?: string, eventsDir?: string): void;
|
|
33
|
+
export { buildSteps, extractStepContent, clearStepContentCache };
|
|
@@ -30,61 +30,125 @@ function buildSteps(maps, contentMaps) {
|
|
|
30
30
|
};
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
const stepContentCaches = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* 解析事件块,把 text/reasoning 累积到对应消息、工具输出累积到对应调用。
|
|
36
|
+
*
|
|
37
|
+
* @param blocks 已切分的事件块字符串数组
|
|
38
|
+
* @param cache 累积结果的映射表(text/reasoning/工具输出)
|
|
39
|
+
* @param filterMsgIds 只收集这些消息 id;缺省收集全部
|
|
40
|
+
*/
|
|
41
|
+
function parseEventBlocks(blocks, cache, filterMsgIds) {
|
|
42
|
+
for (const block of blocks) {
|
|
43
|
+
const dataIdx = block.indexOf(" DATA: ");
|
|
44
|
+
if (dataIdx < 0)
|
|
45
|
+
continue;
|
|
46
|
+
const jsonStr = block.slice(dataIdx + 8);
|
|
47
|
+
if (!jsonStr.trim())
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const event = JSON.parse(jsonStr);
|
|
51
|
+
const props = event;
|
|
52
|
+
const part = props.part;
|
|
53
|
+
if (!part)
|
|
49
54
|
continue;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!part)
|
|
55
|
+
const partType = part.type;
|
|
56
|
+
const msgId = part.messageID;
|
|
57
|
+
if (partType === "text" && msgId) {
|
|
58
|
+
if (filterMsgIds && !filterMsgIds.has(msgId))
|
|
55
59
|
continue;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const toolState = part.state;
|
|
73
|
-
const status = toolState?.status;
|
|
74
|
-
if (callID && (status === "completed" || status === "error")) {
|
|
75
|
-
const output = toolState?.output || toolState?.raw || "";
|
|
76
|
-
toolOutputMap.set(callID, output);
|
|
77
|
-
}
|
|
60
|
+
const text = part.text || "";
|
|
61
|
+
cache.textMap.set(msgId, (cache.textMap.get(msgId) || "") + text);
|
|
62
|
+
}
|
|
63
|
+
if (partType === "reasoning" && msgId) {
|
|
64
|
+
if (filterMsgIds && !filterMsgIds.has(msgId))
|
|
65
|
+
continue;
|
|
66
|
+
const text = part.text || "";
|
|
67
|
+
cache.reasoningMap.set(msgId, (cache.reasoningMap.get(msgId) || "") + text);
|
|
68
|
+
}
|
|
69
|
+
if (partType === "tool") {
|
|
70
|
+
const callID = part.callID;
|
|
71
|
+
const toolState = part.state;
|
|
72
|
+
const status = toolState?.status;
|
|
73
|
+
if (callID && (status === "completed" || status === "error")) {
|
|
74
|
+
const output = toolState?.output || toolState?.raw || "";
|
|
75
|
+
cache.toolOutputMap.set(callID, output);
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Skip unparseable blocks
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 从会话事件日志解析全文内容(text/reasoning/工具输出),增量读取只解析新增字节。
|
|
86
|
+
*
|
|
87
|
+
* @param sessionId 会话 id,日志文件名为 `<sessionId>.log`
|
|
88
|
+
* @param opts eventsDir 指定日志目录;filterMsgIds 只收集这些消息
|
|
89
|
+
* @returns textMap/reasoningMap(按 messageID)与 toolOutputMap(按 callID);日志不存在时为空表
|
|
90
|
+
*/
|
|
91
|
+
function extractStepContent(sessionId, opts) {
|
|
92
|
+
const filePath = path.join(opts?.eventsDir ?? getEventsDir(), `${sessionId}.log`);
|
|
93
|
+
let cache = stepContentCaches.get(filePath);
|
|
94
|
+
if (!cache) {
|
|
95
|
+
cache = {
|
|
96
|
+
offset: 0,
|
|
97
|
+
remainder: "",
|
|
98
|
+
textMap: new Map(),
|
|
99
|
+
reasoningMap: new Map(),
|
|
100
|
+
toolOutputMap: new Map(),
|
|
101
|
+
};
|
|
102
|
+
stepContentCaches.set(filePath, cache);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const stat = fs.statSync(filePath);
|
|
106
|
+
if (stat.size < cache.offset) {
|
|
107
|
+
// 轮转/截断:重置缓存全量重解析
|
|
108
|
+
cache.offset = 0;
|
|
109
|
+
cache.remainder = "";
|
|
110
|
+
cache.textMap.clear();
|
|
111
|
+
cache.reasoningMap.clear();
|
|
112
|
+
cache.toolOutputMap.clear();
|
|
113
|
+
}
|
|
114
|
+
if (stat.size > cache.offset) {
|
|
115
|
+
const length = stat.size - cache.offset;
|
|
116
|
+
const buffer = Buffer.alloc(length);
|
|
117
|
+
const fd = fs.openSync(filePath, "r");
|
|
118
|
+
try {
|
|
119
|
+
fs.readSync(fd, buffer, 0, length, cache.offset);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
fs.closeSync(fd);
|
|
123
|
+
}
|
|
124
|
+
const content = cache.remainder + buffer.toString("utf-8");
|
|
125
|
+
const blocks = content.split(/^(?=\[\d{4}-\d{2}-\d{2} )/m);
|
|
126
|
+
// 末块无结尾换行 = 写入未完成,留到下次(写入器每个完整块必以 \n 结尾)
|
|
127
|
+
let holdback = "";
|
|
128
|
+
if (!content.endsWith("\n")) {
|
|
129
|
+
holdback = blocks.length > 0 ? blocks.pop() : "";
|
|
82
130
|
}
|
|
131
|
+
parseEventBlocks(blocks, cache, opts?.filterMsgIds);
|
|
132
|
+
cache.remainder = holdback;
|
|
133
|
+
cache.offset = stat.size;
|
|
83
134
|
}
|
|
84
135
|
}
|
|
85
136
|
catch {
|
|
86
|
-
// File doesn't exist or read error
|
|
137
|
+
// File doesn't exist or read error — return whatever cached (usually empty maps)
|
|
138
|
+
}
|
|
139
|
+
return { textMap: cache.textMap, reasoningMap: cache.reasoningMap, toolOutputMap: cache.toolOutputMap };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 清除事件日志的增量解析缓存。
|
|
143
|
+
*
|
|
144
|
+
* @param sessionId 只清该会话的缓存;缺省清空全部
|
|
145
|
+
* @param eventsDir 日志目录,须与 extractStepContent 使用的路径一致
|
|
146
|
+
*/
|
|
147
|
+
function clearStepContentCache(sessionId, eventsDir) {
|
|
148
|
+
if (sessionId === undefined) {
|
|
149
|
+
stepContentCaches.clear();
|
|
150
|
+
return;
|
|
87
151
|
}
|
|
88
|
-
|
|
152
|
+
stepContentCaches.delete(path.join(eventsDir ?? getEventsDir(), `${sessionId}.log`));
|
|
89
153
|
}
|
|
90
|
-
export { buildSteps, extractStepContent };
|
|
154
|
+
export { buildSteps, extractStepContent, clearStepContentCache };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type UploadPackage } from "./types.js";
|
|
2
|
+
/** 上传结果:失败时给出原因与是否可重试(4xx 不可重试,5xx/429/网络错误可重试) */
|
|
3
|
+
export type UploadOutcome = {
|
|
4
|
+
ok: true;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
retryable: boolean;
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
/** AGC 凭证与域名(agc-apiclient.json 内容,域名已按 region 解析) */
|
|
11
|
+
export interface AgcCredentials {
|
|
12
|
+
client_id: string;
|
|
13
|
+
client_secret: string;
|
|
14
|
+
project_id: string;
|
|
15
|
+
region: string;
|
|
16
|
+
bucket_name: string;
|
|
17
|
+
api_domain: string;
|
|
18
|
+
storage_domain: string;
|
|
19
|
+
/** token 缓存目录;默认 ~/.agc_cache(与 upload_agc.py 共用缓存),测试可覆盖 */
|
|
20
|
+
tokenCacheDir?: string;
|
|
21
|
+
}
|
|
22
|
+
/** AGC 凭证默认文件:包内 assets/agc-apiclient.json(opencode.json 未配置 agcConfigPath 时使用) */
|
|
23
|
+
export declare function defaultAgcConfigPath(): string;
|
|
24
|
+
export type LoadCredentialsResult = {
|
|
25
|
+
ok: true;
|
|
26
|
+
credentials: AgcCredentials;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 读取并校验 agc-apiclient.json:必填字段齐全、域名按 region 映射
|
|
33
|
+
* (显式配置的 api_domain/storage_domain 优先)。失败返回原因,不抛异常。
|
|
34
|
+
*
|
|
35
|
+
* @param configPath agc-apiclient.json 绝对路径
|
|
36
|
+
* @param tokenCacheDir token 缓存目录覆盖(测试用)
|
|
37
|
+
* @returns 成功返回完整凭证;失败返回原因
|
|
38
|
+
*/
|
|
39
|
+
export declare function loadAgcCredentials(configPath: string, tokenCacheDir?: string): LoadCredentialsResult;
|
|
40
|
+
export interface AgcUploader {
|
|
41
|
+
/**
|
|
42
|
+
* 上传一次触发的两个文件(归档包 + 快照)到
|
|
43
|
+
* YYYYMMDD/<projectName>-<sessionId>/<agentName>-<trigger>-v<n>.tar.gz|.json,
|
|
44
|
+
* 两者都成功才算成功;任一失败返回该失败(整任务由队列重试)。
|
|
45
|
+
*
|
|
46
|
+
* @param uploadPackage 归档产物(含双文件与路径元数据)
|
|
47
|
+
*/
|
|
48
|
+
uploadPackage(uploadPackage: UploadPackage): Promise<UploadOutcome>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 创建 AGC 上传器(token 自动获取与缓存 + 流式 PUT)。
|
|
52
|
+
*
|
|
53
|
+
* @param credentials 已解析的凭证
|
|
54
|
+
*/
|
|
55
|
+
export declare function createAgcUploader(credentials: AgcCredentials): AgcUploader;
|
|
56
|
+
/**
|
|
57
|
+
* 缩写 agent 名用于云端路径段:按 "-" 分段取各段首字母(harmonyos-convert→hc),
|
|
58
|
+
* 单段名取前 3 字符(explore→exp),空名兜底 agent。
|
|
59
|
+
*
|
|
60
|
+
* @param agentName 触发时会话的 agent 名
|
|
61
|
+
* @returns 缩写后的安全路径段
|
|
62
|
+
*/
|
|
63
|
+
export declare function abbreviateAgentName(agentName: string): string;
|
|
64
|
+
/** 归档包云端对象完整路径(上传成功日志用) */
|
|
65
|
+
export declare function remoteArchiveKey(uploadPackage: UploadPackage): string;
|