@jetcrab/pi-desk-usage 0.0.0-stage → 1.0.1-dev.9001
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/LICENSE +201 -0
- package/README.md +4 -2
- package/dist/browser/chunks/browser-PWPSWHE7.js +12 -0
- package/dist/browser/chunks/browser-session-view-FVGFXMQB.js +4 -0
- package/dist/browser/chunks/chunk-JHHRG2FT.js +1 -0
- package/dist/browser/entry.js +1 -0
- package/dist/l4-usage-aggregate.d.ts +9 -0
- package/dist/l4-usage-aggregate.js +59 -0
- package/dist/l4-usage-calendar.d.ts +31 -0
- package/dist/l4-usage-calendar.js +186 -0
- package/dist/l4-usage-files.d.ts +5 -0
- package/dist/l4-usage-files.js +97 -0
- package/dist/l4-usage-format.d.ts +1 -0
- package/dist/l4-usage-format.js +12 -0
- package/dist/pi-desk.d.ts +2 -0
- package/dist/pi-desk.js +18 -0
- package/dist/protocol.d.ts +189 -0
- package/dist/protocol.js +1 -0
- package/dist/usage-runtime.d.ts +53 -0
- package/dist/usage-runtime.js +816 -0
- package/dist/usage-scan.d.ts +82 -0
- package/dist/usage-scan.js +467 -0
- package/dist/usage-timeline.d.ts +15 -0
- package/dist/usage-timeline.js +342 -0
- package/dist/usage-worker.d.ts +1 -0
- package/dist/usage-worker.js +13 -0
- package/package.json +56 -3
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import { getAgentDir, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { PluginMethodError } from '@jetcrab/pi-desk-sdk/session';
|
|
6
|
+
import { usageBucketRanges, usageBucketIndex, zonedHourStart } from './l4-usage-calendar.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { addTotals, aggregateEventModels, emptyStructureCounts, emptyTotals, hasUsage, promptTokens, totalEvents } from './l4-usage-aggregate.js';
|
|
9
|
+
import { descriptor, listStandardSessionFiles, nestedParentSessionId, pathKey } from './l4-usage-files.js';
|
|
10
|
+
import { createUsageActivities, createUsageTimeline } from './usage-timeline.js';
|
|
11
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
12
|
+
const HALF_HOUR_MS = HOUR_MS / 2;
|
|
13
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
14
|
+
const MAX_RANGE_MS = 370 * DAY_MS;
|
|
15
|
+
const INDEX_REUSE_MS = 30_000;
|
|
16
|
+
const CACHE_IDLE_MS = 30 * 60 * 1000;
|
|
17
|
+
const MAX_SESSION_PAGE = 50;
|
|
18
|
+
const reportInputSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
startAt: z.number().int().nonnegative(),
|
|
21
|
+
endAt: z.number().int().positive(),
|
|
22
|
+
force: z.boolean().optional()
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
const sessionListInputSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
startAt: z.number().int().nonnegative(),
|
|
28
|
+
endAt: z.number().int().positive(),
|
|
29
|
+
model: z.string().trim().min(1).max(200).optional(),
|
|
30
|
+
offset: z.number().int().min(0).max(100_000),
|
|
31
|
+
limit: z.number().int().min(1).max(MAX_SESSION_PAGE)
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
const pluginSourceSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
workId: z.string().trim().min(1),
|
|
37
|
+
sessionId: z.string().trim().min(1),
|
|
38
|
+
branchId: z.string().trim().min(1)
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
const sessionAnalysisInputSchema = z.object({ source: pluginSourceSchema }).strict();
|
|
42
|
+
const sessionTimelineInputSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
source: pluginSourceSchema,
|
|
45
|
+
startAt: z.number().int().nonnegative().optional(),
|
|
46
|
+
endAt: z.number().int().positive().optional()
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
const sessionActivityInputSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
source: pluginSourceSchema,
|
|
52
|
+
startAt: z.number().int().nonnegative(),
|
|
53
|
+
endAt: z.number().int().positive(),
|
|
54
|
+
offset: z.number().int().min(0).max(100_000),
|
|
55
|
+
limit: z.number().int().min(1).max(200)
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
function defaultSessionsRoot() {
|
|
59
|
+
const configuredSessions = process.env.PI_CODING_AGENT_SESSION_DIR?.trim();
|
|
60
|
+
if (configuredSessions)
|
|
61
|
+
return configuredSessions;
|
|
62
|
+
const configuredAgent = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
63
|
+
const agentDir = configuredAgent || join(homedir(), '.pi', 'agent');
|
|
64
|
+
return join(agentDir, 'sessions');
|
|
65
|
+
}
|
|
66
|
+
function jsonObject(value) {
|
|
67
|
+
return JSON.parse(JSON.stringify(value));
|
|
68
|
+
}
|
|
69
|
+
function zodMessage(error) {
|
|
70
|
+
const issue = error.issues[0];
|
|
71
|
+
if (!issue)
|
|
72
|
+
return '输入格式无效';
|
|
73
|
+
const path = issue.path.length > 0 ? `${issue.path.join('.')}:` : '';
|
|
74
|
+
return `${path}${issue.message}`;
|
|
75
|
+
}
|
|
76
|
+
function parseInput(schema, input) {
|
|
77
|
+
try {
|
|
78
|
+
return schema.parse(input);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof z.ZodError)
|
|
82
|
+
throw new PluginMethodError(400, zodMessage(error));
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function validateRange(startAt, endAt) {
|
|
87
|
+
if (endAt <= startAt)
|
|
88
|
+
throw new PluginMethodError(400, 'endAt 必须晚于 startAt');
|
|
89
|
+
if (endAt - startAt > MAX_RANGE_MS) {
|
|
90
|
+
throw new PluginMethodError(400, '单次用量查询不能超过 370 天');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function signature(descriptor) {
|
|
94
|
+
return `${descriptor.size}:${descriptor.modifiedAt}`;
|
|
95
|
+
}
|
|
96
|
+
function overlaps(file, startAt, endAt) {
|
|
97
|
+
return file.createdAt < endAt && file.updatedAt >= startAt;
|
|
98
|
+
}
|
|
99
|
+
function seriesBucketMs(startAt, endAt, timeZone) {
|
|
100
|
+
const duration = endAt - startAt;
|
|
101
|
+
if (duration === DAY_MS && zonedHourStart(endAt, timeZone) === endAt) {
|
|
102
|
+
return HALF_HOUR_MS;
|
|
103
|
+
}
|
|
104
|
+
if (duration <= 48 * HOUR_MS)
|
|
105
|
+
return HOUR_MS;
|
|
106
|
+
if (duration <= 10 * DAY_MS)
|
|
107
|
+
return 6 * HOUR_MS;
|
|
108
|
+
return DAY_MS;
|
|
109
|
+
}
|
|
110
|
+
function fileTitle(file) {
|
|
111
|
+
return file.name ?? file.firstMessage ?? basename(file.cwd) ?? file.sessionId;
|
|
112
|
+
}
|
|
113
|
+
function aggregateModels(events, modelNames) {
|
|
114
|
+
return aggregateEventModels(events.map((item) => item.event), modelNames);
|
|
115
|
+
}
|
|
116
|
+
function cloneCounts(counts) {
|
|
117
|
+
return { ...counts };
|
|
118
|
+
}
|
|
119
|
+
export class UsageRuntime {
|
|
120
|
+
sessionsRoot;
|
|
121
|
+
scanFiles;
|
|
122
|
+
logger;
|
|
123
|
+
workSessions;
|
|
124
|
+
settings;
|
|
125
|
+
cache = new Map();
|
|
126
|
+
operation = Promise.resolve();
|
|
127
|
+
coverage = null;
|
|
128
|
+
skippedFiles = 0;
|
|
129
|
+
activeWorker = null;
|
|
130
|
+
disposed = false;
|
|
131
|
+
constructor(options = {}) {
|
|
132
|
+
this.sessionsRoot = resolve(options.sessionsRoot ?? defaultSessionsRoot());
|
|
133
|
+
this.scanFiles = options.scanFiles ?? ((files, signal) => this.scanWithWorker(files, signal));
|
|
134
|
+
this.logger = options.logger ?? console;
|
|
135
|
+
this.workSessions = options.workSessions ?? null;
|
|
136
|
+
this.settings = options.settings ?? null;
|
|
137
|
+
}
|
|
138
|
+
async report(input, signal) {
|
|
139
|
+
const parsed = parseInput(reportInputSchema, input);
|
|
140
|
+
validateRange(parsed.startAt, parsed.endAt);
|
|
141
|
+
await this.ensureIndexed(parsed.startAt, parsed.endAt, parsed.force === true, signal);
|
|
142
|
+
const events = this.ownedEvents(parsed.startAt, parsed.endAt);
|
|
143
|
+
const modelNames = await this.readModelNames();
|
|
144
|
+
const timeZone = this.settings?.getSnapshot().region.timeZone ?? 'UTC';
|
|
145
|
+
const bucketMs = seriesBucketMs(parsed.startAt, parsed.endAt, timeZone);
|
|
146
|
+
const snapshot = {
|
|
147
|
+
generatedAt: Date.now(),
|
|
148
|
+
startAt: parsed.startAt,
|
|
149
|
+
endAt: parsed.endAt,
|
|
150
|
+
bucketMs,
|
|
151
|
+
totals: this.total(events),
|
|
152
|
+
series: this.series(events, parsed.startAt, parsed.endAt, bucketMs, modelNames, timeZone),
|
|
153
|
+
models: aggregateModels(events, modelNames),
|
|
154
|
+
skippedFiles: this.skippedFiles
|
|
155
|
+
};
|
|
156
|
+
return jsonObject(snapshot);
|
|
157
|
+
}
|
|
158
|
+
async sessions(input, signal) {
|
|
159
|
+
const parsed = parseInput(sessionListInputSchema, input);
|
|
160
|
+
validateRange(parsed.startAt, parsed.endAt);
|
|
161
|
+
await this.ensureIndexed(parsed.startAt, parsed.endAt, false, signal);
|
|
162
|
+
const events = this.ownedEvents(parsed.startAt, parsed.endAt).filter((item) => !parsed.model || item.event.modelKey === parsed.model);
|
|
163
|
+
const modelNames = await this.readModelNames();
|
|
164
|
+
const sessions = this.sessionSnapshots(events, modelNames).sort((left, right) => promptTokens(right.totals) - promptTokens(left.totals) || right.updatedAt - left.updatedAt);
|
|
165
|
+
const page = {
|
|
166
|
+
total: sessions.length,
|
|
167
|
+
offset: parsed.offset,
|
|
168
|
+
limit: parsed.limit,
|
|
169
|
+
items: sessions.slice(parsed.offset, parsed.offset + parsed.limit)
|
|
170
|
+
};
|
|
171
|
+
return jsonObject(page);
|
|
172
|
+
}
|
|
173
|
+
async sessionAnalysis(input, signal) {
|
|
174
|
+
const parsed = parseInput(sessionAnalysisInputSchema, input);
|
|
175
|
+
const workSession = await this.requireWorkSession(parsed.source);
|
|
176
|
+
const snapshot = await this.runExclusive(async () => {
|
|
177
|
+
if (this.disposed)
|
|
178
|
+
throw new PluginMethodError(409, '用量 Runtime 已关闭');
|
|
179
|
+
if (signal.aborted)
|
|
180
|
+
throw new PluginMethodError(409, '会话分析已取消');
|
|
181
|
+
const details = await this.loadSessionDetails(workSession, signal);
|
|
182
|
+
const modelNames = await this.readModelNames();
|
|
183
|
+
const sources = details.sources.map((source) => source.kind === 'main'
|
|
184
|
+
? {
|
|
185
|
+
key: 'main',
|
|
186
|
+
kind: 'main',
|
|
187
|
+
title: source.title,
|
|
188
|
+
startedAt: source.startedAt,
|
|
189
|
+
updatedAt: source.updatedAt,
|
|
190
|
+
totals: totalEvents(source.events),
|
|
191
|
+
models: aggregateEventModels(source.events, modelNames),
|
|
192
|
+
counts: cloneCounts(source.counts),
|
|
193
|
+
error: source.error
|
|
194
|
+
}
|
|
195
|
+
: {
|
|
196
|
+
key: source.key,
|
|
197
|
+
kind: 'subagent',
|
|
198
|
+
taskId: source.taskId,
|
|
199
|
+
title: source.title,
|
|
200
|
+
agentType: source.agentType ?? 'subagent',
|
|
201
|
+
status: source.status ?? 'running',
|
|
202
|
+
runCount: source.runCount,
|
|
203
|
+
startedAt: source.startedAt,
|
|
204
|
+
updatedAt: source.updatedAt,
|
|
205
|
+
totals: totalEvents(source.events),
|
|
206
|
+
models: aggregateEventModels(source.events, modelNames),
|
|
207
|
+
counts: cloneCounts(source.counts),
|
|
208
|
+
error: source.error
|
|
209
|
+
});
|
|
210
|
+
const calls = details.sources
|
|
211
|
+
.flatMap((source) => source.events.map((event) => this.analysisCall(event, source.key, modelNames)))
|
|
212
|
+
.sort((left, right) => right.timestamp - left.timestamp)
|
|
213
|
+
.slice(0, 30);
|
|
214
|
+
const inheritedTotals = totalEvents(details.inheritedEvents);
|
|
215
|
+
const analysis = {
|
|
216
|
+
generatedAt: Date.now(),
|
|
217
|
+
session: {
|
|
218
|
+
sessionId: details.main.file.sessionId,
|
|
219
|
+
title: details.sources[0].title,
|
|
220
|
+
cwd: details.main.file.cwd,
|
|
221
|
+
startedAt: details.main.file.createdAt,
|
|
222
|
+
updatedAt: Math.max(...details.sources.map((source) => source.updatedAt))
|
|
223
|
+
},
|
|
224
|
+
inherited: hasUsage(inheritedTotals) ? inheritedTotals : null,
|
|
225
|
+
sources,
|
|
226
|
+
recentCalls: calls
|
|
227
|
+
};
|
|
228
|
+
return analysis;
|
|
229
|
+
});
|
|
230
|
+
await this.requireWorkSession(parsed.source);
|
|
231
|
+
return jsonObject(snapshot);
|
|
232
|
+
}
|
|
233
|
+
async sessionTimeline(input, signal) {
|
|
234
|
+
const parsed = parseInput(sessionTimelineInputSchema, input);
|
|
235
|
+
if ((parsed.startAt === undefined) !== (parsed.endAt === undefined)) {
|
|
236
|
+
throw new PluginMethodError(400, 'startAt 和 endAt 必须同时提供');
|
|
237
|
+
}
|
|
238
|
+
if (parsed.startAt !== undefined && parsed.endAt !== undefined) {
|
|
239
|
+
validateRange(parsed.startAt, parsed.endAt);
|
|
240
|
+
}
|
|
241
|
+
const workSession = await this.requireWorkSession(parsed.source);
|
|
242
|
+
const snapshot = await this.runExclusive(async () => {
|
|
243
|
+
if (this.disposed)
|
|
244
|
+
throw new PluginMethodError(409, '用量 Runtime 已关闭');
|
|
245
|
+
if (signal.aborted)
|
|
246
|
+
throw new PluginMethodError(409, '会话时间轴已取消');
|
|
247
|
+
const details = await this.loadSessionDetails(workSession, signal);
|
|
248
|
+
return createUsageTimeline(this.timelineInputs(details), parsed.startAt, parsed.endAt, this.settings?.getSnapshot().region.timeZone ?? 'UTC');
|
|
249
|
+
});
|
|
250
|
+
await this.requireWorkSession(parsed.source);
|
|
251
|
+
return jsonObject(snapshot);
|
|
252
|
+
}
|
|
253
|
+
async sessionActivities(input, signal) {
|
|
254
|
+
const parsed = parseInput(sessionActivityInputSchema, input);
|
|
255
|
+
validateRange(parsed.startAt, parsed.endAt);
|
|
256
|
+
const workSession = await this.requireWorkSession(parsed.source);
|
|
257
|
+
const snapshot = await this.runExclusive(async () => {
|
|
258
|
+
if (this.disposed)
|
|
259
|
+
throw new PluginMethodError(409, '会话活动查询已关闭');
|
|
260
|
+
if (signal.aborted)
|
|
261
|
+
throw new PluginMethodError(409, '会话活动查询已取消');
|
|
262
|
+
const details = await this.loadSessionDetails(workSession, signal);
|
|
263
|
+
const modelNames = await this.readModelNames();
|
|
264
|
+
const activities = createUsageActivities(this.timelineInputs(details), parsed.startAt, parsed.endAt)
|
|
265
|
+
.map((activity) => activity.kind === 'model'
|
|
266
|
+
? { ...activity, label: modelNames.get(activity.label) || activity.label }
|
|
267
|
+
: activity)
|
|
268
|
+
.sort((left, right) => right.timestamp - left.timestamp || right.id.localeCompare(left.id));
|
|
269
|
+
const page = {
|
|
270
|
+
startAt: parsed.startAt,
|
|
271
|
+
endAt: parsed.endAt,
|
|
272
|
+
total: activities.length,
|
|
273
|
+
offset: parsed.offset,
|
|
274
|
+
limit: parsed.limit,
|
|
275
|
+
items: activities.slice(parsed.offset, parsed.offset + parsed.limit)
|
|
276
|
+
};
|
|
277
|
+
return page;
|
|
278
|
+
});
|
|
279
|
+
await this.requireWorkSession(parsed.source);
|
|
280
|
+
return jsonObject(snapshot);
|
|
281
|
+
}
|
|
282
|
+
async dispose() {
|
|
283
|
+
if (this.disposed)
|
|
284
|
+
return;
|
|
285
|
+
this.disposed = true;
|
|
286
|
+
const worker = this.activeWorker;
|
|
287
|
+
this.activeWorker = null;
|
|
288
|
+
if (worker)
|
|
289
|
+
await worker.terminate();
|
|
290
|
+
await this.operation.catch(() => undefined);
|
|
291
|
+
this.cache.clear();
|
|
292
|
+
this.coverage = null;
|
|
293
|
+
}
|
|
294
|
+
async readModelNames() {
|
|
295
|
+
const agentDir = getAgentDir();
|
|
296
|
+
const models = await ModelRuntime.create({
|
|
297
|
+
authPath: join(agentDir, 'auth.json'),
|
|
298
|
+
modelsPath: join(agentDir, 'models.json')
|
|
299
|
+
});
|
|
300
|
+
return new Map(models
|
|
301
|
+
.getModels()
|
|
302
|
+
.map((model) => [
|
|
303
|
+
`${model.provider}/${model.id}`,
|
|
304
|
+
`${model.provider}/${model.name || model.id}`
|
|
305
|
+
]));
|
|
306
|
+
}
|
|
307
|
+
total(events) {
|
|
308
|
+
const totals = emptyTotals();
|
|
309
|
+
for (const item of events)
|
|
310
|
+
addTotals(totals, item.event.totals);
|
|
311
|
+
return totals;
|
|
312
|
+
}
|
|
313
|
+
series(events, startAt, endAt, bucketMs, modelNames, timeZone) {
|
|
314
|
+
const buckets = usageBucketRanges(startAt, endAt, bucketMs, timeZone).map((range) => ({
|
|
315
|
+
...range,
|
|
316
|
+
items: []
|
|
317
|
+
}));
|
|
318
|
+
for (const item of events) {
|
|
319
|
+
buckets[usageBucketIndex(buckets, item.event.timestamp)].items.push(item);
|
|
320
|
+
}
|
|
321
|
+
return buckets.map((bucket) => ({
|
|
322
|
+
startAt: bucket.startAt,
|
|
323
|
+
endAt: bucket.endAt,
|
|
324
|
+
totals: this.total(bucket.items),
|
|
325
|
+
models: aggregateModels(bucket.items, modelNames)
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
sessionSnapshots(events, modelNames) {
|
|
329
|
+
const indexedFiles = this.indexedFiles();
|
|
330
|
+
const mainsBySessionId = new Map(indexedFiles
|
|
331
|
+
.filter((indexed) => indexed.source === 'main')
|
|
332
|
+
.map((indexed) => [indexed.cache.file.sessionId, indexed]));
|
|
333
|
+
const groups = new Map();
|
|
334
|
+
for (const item of events) {
|
|
335
|
+
const file = item.indexed.cache.file;
|
|
336
|
+
const parentFromPath = file.parentSessionPath
|
|
337
|
+
? this.cache.get(pathKey(file.parentSessionPath))?.file.sessionId
|
|
338
|
+
: null;
|
|
339
|
+
const sessionId = item.indexed.source === 'main'
|
|
340
|
+
? file.sessionId
|
|
341
|
+
: (item.indexed.child?.parentSessionId ?? parentFromPath ?? file.sessionId);
|
|
342
|
+
let group = groups.get(sessionId);
|
|
343
|
+
if (!group) {
|
|
344
|
+
group = {
|
|
345
|
+
sessionId,
|
|
346
|
+
main: mainsBySessionId.get(sessionId) ?? null,
|
|
347
|
+
mainItems: [],
|
|
348
|
+
children: new Map()
|
|
349
|
+
};
|
|
350
|
+
groups.set(sessionId, group);
|
|
351
|
+
}
|
|
352
|
+
if (item.indexed.source === 'main') {
|
|
353
|
+
group.main = item.indexed;
|
|
354
|
+
group.mainItems.push(item);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
const taskId = item.indexed.child?.taskId ?? file.sessionId;
|
|
358
|
+
const childItems = group.children.get(taskId) ?? [];
|
|
359
|
+
childItems.push(item);
|
|
360
|
+
group.children.set(taskId, childItems);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return [...groups.values()].map((group) => {
|
|
364
|
+
const childItems = [...group.children.values()];
|
|
365
|
+
const allItems = [...group.mainItems, ...childItems.flat()];
|
|
366
|
+
const fallback = allItems[0].indexed.cache.file;
|
|
367
|
+
const mainFile = group.main?.cache.file ?? null;
|
|
368
|
+
const subagents = childItems
|
|
369
|
+
.map((items) => {
|
|
370
|
+
const indexed = items[0].indexed;
|
|
371
|
+
const file = indexed.cache.file;
|
|
372
|
+
return {
|
|
373
|
+
sessionId: file.sessionId,
|
|
374
|
+
taskId: indexed.child?.taskId ?? file.sessionId,
|
|
375
|
+
title: indexed.child?.title ?? fileTitle(file),
|
|
376
|
+
agentType: indexed.child?.agentType ?? null,
|
|
377
|
+
startedAt: Math.min(file.createdAt, ...items.map((item) => item.event.timestamp)),
|
|
378
|
+
updatedAt: Math.max(...items.map((item) => item.event.timestamp)),
|
|
379
|
+
totals: this.total(items),
|
|
380
|
+
models: aggregateModels(items, modelNames)
|
|
381
|
+
};
|
|
382
|
+
})
|
|
383
|
+
.sort((left, right) => promptTokens(right.totals) - promptTokens(left.totals) ||
|
|
384
|
+
right.updatedAt - left.updatedAt);
|
|
385
|
+
return {
|
|
386
|
+
sessionId: group.sessionId,
|
|
387
|
+
cwd: mainFile?.cwd ?? fallback.cwd,
|
|
388
|
+
title: mainFile ? fileTitle(mainFile) : fileTitle(fallback),
|
|
389
|
+
startedAt: Math.min(mainFile?.createdAt ?? Number.POSITIVE_INFINITY, ...allItems.map((item) => item.indexed.cache.file.createdAt)),
|
|
390
|
+
updatedAt: Math.max(...allItems.map((item) => item.event.timestamp)),
|
|
391
|
+
totals: this.total(allItems),
|
|
392
|
+
mainTotals: this.total(group.mainItems),
|
|
393
|
+
models: aggregateModels(allItems, modelNames),
|
|
394
|
+
subagents
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async loadSessionDetails(workSession, signal) {
|
|
399
|
+
const main = await this.loadSessionById(workSession.source.sessionId, signal);
|
|
400
|
+
if (pathKey(main.file.cwd) !== pathKey(workSession.cwd)) {
|
|
401
|
+
throw new PluginMethodError(409, '工作会话目录与 Pi Session 不一致');
|
|
402
|
+
}
|
|
403
|
+
const parent = main.file.parentSessionPath
|
|
404
|
+
? await this.loadExactFile(main.file.parentSessionPath, false, signal)
|
|
405
|
+
: null;
|
|
406
|
+
const inheritedEventIds = new Set(parent?.file.events.map((event) => event.identity) ?? []);
|
|
407
|
+
const inheritedMarkerIds = new Set(parent?.file.markers.map((marker) => marker.identity) ?? []);
|
|
408
|
+
const inheritedLaunchIds = new Set(parent?.file.childSessions.map((child) => child.identity) ?? []);
|
|
409
|
+
const inheritedToolIds = new Set(parent?.file.tools.map((tool) => tool.identity) ?? []);
|
|
410
|
+
const inheritedEvents = main.file.events.filter((event) => inheritedEventIds.has(event.identity));
|
|
411
|
+
const mainEvents = main.file.events.filter((event) => !inheritedEventIds.has(event.identity));
|
|
412
|
+
const mainMarkers = main.file.markers.filter((marker) => !inheritedMarkerIds.has(marker.identity));
|
|
413
|
+
const mainTools = main.file.tools.filter((tool) => !inheritedToolIds.has(tool.identity));
|
|
414
|
+
const mainTitle = fileTitle(main.file);
|
|
415
|
+
const sources = [
|
|
416
|
+
{
|
|
417
|
+
key: 'main',
|
|
418
|
+
kind: 'main',
|
|
419
|
+
taskId: null,
|
|
420
|
+
title: mainTitle,
|
|
421
|
+
agentType: null,
|
|
422
|
+
status: null,
|
|
423
|
+
runCount: 1,
|
|
424
|
+
launches: [main.file.createdAt],
|
|
425
|
+
startedAt: main.file.createdAt,
|
|
426
|
+
updatedAt: Math.max(main.file.createdAt, ...mainEvents.map((event) => event.timestamp), ...mainMarkers.map((marker) => marker.timestamp)),
|
|
427
|
+
events: mainEvents,
|
|
428
|
+
markers: mainMarkers,
|
|
429
|
+
tools: mainTools,
|
|
430
|
+
counts: cloneCounts(main.file.counts),
|
|
431
|
+
error: null
|
|
432
|
+
}
|
|
433
|
+
];
|
|
434
|
+
const launchesByTask = new Map();
|
|
435
|
+
for (const launch of main.file.childSessions) {
|
|
436
|
+
if (inheritedLaunchIds.has(launch.identity))
|
|
437
|
+
continue;
|
|
438
|
+
const launches = launchesByTask.get(launch.taskId) ?? [];
|
|
439
|
+
launches.push(launch);
|
|
440
|
+
launchesByTask.set(launch.taskId, launches);
|
|
441
|
+
}
|
|
442
|
+
const childFiles = new Map();
|
|
443
|
+
for (const launches of launchesByTask.values()) {
|
|
444
|
+
launches.sort((left, right) => left.startedAt - right.startedAt);
|
|
445
|
+
const latestLaunch = launches.at(-1);
|
|
446
|
+
const key = pathKey(latestLaunch.sessionFile);
|
|
447
|
+
if (childFiles.has(key))
|
|
448
|
+
continue;
|
|
449
|
+
const file = await descriptor(latestLaunch.sessionFile, true);
|
|
450
|
+
if (file)
|
|
451
|
+
childFiles.set(key, file);
|
|
452
|
+
}
|
|
453
|
+
await this.loadChanged([...childFiles.values()], signal);
|
|
454
|
+
for (const [taskId, orderedLaunches] of launchesByTask) {
|
|
455
|
+
const latestLaunch = orderedLaunches.at(-1);
|
|
456
|
+
const latestTerminal = main.file.childTerminals
|
|
457
|
+
.filter((terminal) => terminal.taskId === taskId)
|
|
458
|
+
.sort((left, right) => left.timestamp - right.timestamp)
|
|
459
|
+
.at(-1);
|
|
460
|
+
const status = latestTerminal && latestTerminal.timestamp >= latestLaunch.startedAt
|
|
461
|
+
? latestTerminal.status
|
|
462
|
+
: 'running';
|
|
463
|
+
const childKey = pathKey(latestLaunch.sessionFile);
|
|
464
|
+
const child = childFiles.has(childKey) ? this.cache.get(childKey) : null;
|
|
465
|
+
if (!child) {
|
|
466
|
+
sources.push({
|
|
467
|
+
key: taskId,
|
|
468
|
+
kind: 'subagent',
|
|
469
|
+
taskId,
|
|
470
|
+
title: latestLaunch.title,
|
|
471
|
+
agentType: latestLaunch.agentType,
|
|
472
|
+
status,
|
|
473
|
+
runCount: orderedLaunches.length,
|
|
474
|
+
launches: orderedLaunches.map((launch) => launch.startedAt),
|
|
475
|
+
startedAt: orderedLaunches[0].startedAt,
|
|
476
|
+
updatedAt: latestTerminal?.endedAt ?? latestLaunch.startedAt,
|
|
477
|
+
events: [],
|
|
478
|
+
markers: [],
|
|
479
|
+
tools: [],
|
|
480
|
+
counts: emptyStructureCounts(),
|
|
481
|
+
error: '子代理 Session 文件不存在或无法读取'
|
|
482
|
+
});
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const cutoff = parent ? orderedLaunches[0].startedAt : 0;
|
|
486
|
+
const childEvents = child.file.events.filter((event) => event.timestamp >= cutoff);
|
|
487
|
+
const childMarkers = child.file.markers.filter((marker) => marker.timestamp >= cutoff);
|
|
488
|
+
const childTools = child.file.tools.filter((tool) => tool.timestamp >= cutoff);
|
|
489
|
+
sources.push({
|
|
490
|
+
key: taskId,
|
|
491
|
+
kind: 'subagent',
|
|
492
|
+
taskId,
|
|
493
|
+
title: latestLaunch.title,
|
|
494
|
+
agentType: latestLaunch.agentType,
|
|
495
|
+
status,
|
|
496
|
+
runCount: orderedLaunches.length,
|
|
497
|
+
launches: orderedLaunches.map((launch) => launch.startedAt),
|
|
498
|
+
startedAt: orderedLaunches[0].startedAt,
|
|
499
|
+
updatedAt: Math.max(orderedLaunches[0].startedAt, latestTerminal?.endedAt ?? 0, ...childEvents.map((event) => event.timestamp), ...childMarkers.map((marker) => marker.timestamp)),
|
|
500
|
+
events: childEvents,
|
|
501
|
+
markers: childMarkers,
|
|
502
|
+
tools: childTools,
|
|
503
|
+
counts: cloneCounts(child.file.counts),
|
|
504
|
+
error: null
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return { main, inheritedEvents, sources };
|
|
508
|
+
}
|
|
509
|
+
async requireWorkSession(source) {
|
|
510
|
+
if (!this.workSessions)
|
|
511
|
+
throw new Error('用量 Runtime 缺少 WorkSession 能力');
|
|
512
|
+
const workSessions = await this.workSessions.listWorkSessions();
|
|
513
|
+
const workSession = workSessions.find((item) => item.source.workId === source.workId &&
|
|
514
|
+
item.source.sessionId === source.sessionId &&
|
|
515
|
+
item.source.branchId === source.branchId);
|
|
516
|
+
if (!workSession)
|
|
517
|
+
throw new PluginMethodError(409, '工作会话来源已变化');
|
|
518
|
+
return { source: workSession.source, cwd: workSession.cwd };
|
|
519
|
+
}
|
|
520
|
+
async loadSessionById(sessionId, signal) {
|
|
521
|
+
const cached = [...this.cache.values()].find((entry) => !entry.descriptor.nestedSubagent && entry.file.sessionId === sessionId);
|
|
522
|
+
if (cached) {
|
|
523
|
+
const current = await descriptor(cached.file.path, false);
|
|
524
|
+
if (current)
|
|
525
|
+
await this.loadChanged([current], signal);
|
|
526
|
+
const refreshed = this.cache.get(pathKey(cached.file.path));
|
|
527
|
+
if (refreshed)
|
|
528
|
+
return refreshed;
|
|
529
|
+
}
|
|
530
|
+
const standardFiles = await listStandardSessionFiles(this.sessionsRoot);
|
|
531
|
+
const suffix = `_${sessionId}.jsonl`.toLowerCase();
|
|
532
|
+
const candidates = standardFiles.filter((file) => !file.nestedSubagent && basename(file.path).toLowerCase().endsWith(suffix));
|
|
533
|
+
await this.loadChanged(candidates, signal);
|
|
534
|
+
const found = [...this.cache.values()].find((entry) => !entry.descriptor.nestedSubagent && entry.file.sessionId === sessionId);
|
|
535
|
+
if (!found)
|
|
536
|
+
throw new PluginMethodError(404, 'Pi Session 文件不存在');
|
|
537
|
+
return found;
|
|
538
|
+
}
|
|
539
|
+
async loadExactFile(path, nestedSubagent, signal) {
|
|
540
|
+
const current = await descriptor(path, nestedSubagent);
|
|
541
|
+
if (!current)
|
|
542
|
+
return null;
|
|
543
|
+
await this.loadChanged([current], signal);
|
|
544
|
+
return this.cache.get(pathKey(current.path)) ?? null;
|
|
545
|
+
}
|
|
546
|
+
timelineInputs(details) {
|
|
547
|
+
return details.sources.map((source) => {
|
|
548
|
+
const runs = source.kind === 'main'
|
|
549
|
+
? [{ startAt: source.startedAt, endAt: source.updatedAt }]
|
|
550
|
+
: source.launches.map((startAt, index) => ({
|
|
551
|
+
startAt,
|
|
552
|
+
endAt: source.launches[index + 1] ??
|
|
553
|
+
(source.status === 'running' ? null : source.updatedAt)
|
|
554
|
+
}));
|
|
555
|
+
return {
|
|
556
|
+
key: source.key,
|
|
557
|
+
kind: source.kind,
|
|
558
|
+
title: source.title,
|
|
559
|
+
agentType: source.agentType,
|
|
560
|
+
updatedAt: source.updatedAt,
|
|
561
|
+
runs,
|
|
562
|
+
events: source.events,
|
|
563
|
+
markers: source.markers,
|
|
564
|
+
tools: source.tools
|
|
565
|
+
};
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
analysisCall(event, sourceKey, modelNames) {
|
|
569
|
+
return {
|
|
570
|
+
timestamp: event.timestamp,
|
|
571
|
+
sourceKey,
|
|
572
|
+
kind: event.kind,
|
|
573
|
+
modelKey: event.modelKey,
|
|
574
|
+
requestedModel: event.requestedModelKey && event.requestedModelKey !== event.modelKey
|
|
575
|
+
? modelNames.get(event.requestedModelKey) || event.requestedModelKey
|
|
576
|
+
: null,
|
|
577
|
+
actualModel: modelNames.get(event.modelKey) || event.label,
|
|
578
|
+
totals: { ...event.totals }
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async ensureIndexed(startAt, endAt, force, signal) {
|
|
582
|
+
if (!force && this.isCovered(startAt, endAt))
|
|
583
|
+
return;
|
|
584
|
+
await this.runExclusive(async () => {
|
|
585
|
+
if (this.disposed)
|
|
586
|
+
throw new PluginMethodError(409, '用量 Runtime 已关闭');
|
|
587
|
+
if (signal.aborted)
|
|
588
|
+
throw new PluginMethodError(409, '用量查询已取消');
|
|
589
|
+
if (!force && this.isCovered(startAt, endAt))
|
|
590
|
+
return;
|
|
591
|
+
await this.refreshIndex(startAt, endAt, signal);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
isCovered(startAt, endAt) {
|
|
595
|
+
return (this.coverage !== null &&
|
|
596
|
+
startAt >= this.coverage.startAt &&
|
|
597
|
+
endAt <= this.coverage.endAt &&
|
|
598
|
+
Date.now() - this.coverage.checkedAt < INDEX_REUSE_MS);
|
|
599
|
+
}
|
|
600
|
+
async runExclusive(operation) {
|
|
601
|
+
const result = this.operation.then(operation, operation);
|
|
602
|
+
this.operation = result.then(() => undefined, () => undefined);
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
async refreshIndex(startAt, endAt, signal) {
|
|
606
|
+
const startedAt = Date.now();
|
|
607
|
+
const standardFiles = await listStandardSessionFiles(this.sessionsRoot);
|
|
608
|
+
const standardByKey = new Map(standardFiles.map((file) => [pathKey(file.path), file]));
|
|
609
|
+
const candidateByKey = new Map(standardFiles
|
|
610
|
+
.filter((file) => this.shouldLoad(file, startAt, endAt))
|
|
611
|
+
.map((file) => [pathKey(file.path), file]));
|
|
612
|
+
for (const file of candidateByKey.values()) {
|
|
613
|
+
if (!file.nestedSubagent)
|
|
614
|
+
continue;
|
|
615
|
+
const parentSessionId = nestedParentSessionId(file.path);
|
|
616
|
+
if (!parentSessionId)
|
|
617
|
+
continue;
|
|
618
|
+
const suffix = `_${parentSessionId}.jsonl`.toLowerCase();
|
|
619
|
+
const parent = standardFiles.find((candidate) => !candidate.nestedSubagent && basename(candidate.path).toLowerCase().endsWith(suffix));
|
|
620
|
+
if (parent)
|
|
621
|
+
candidateByKey.set(pathKey(parent.path), parent);
|
|
622
|
+
}
|
|
623
|
+
const candidates = [...candidateByKey.values()];
|
|
624
|
+
let skipped = await this.loadChanged(candidates, signal);
|
|
625
|
+
const childMetadata = this.childMetadata();
|
|
626
|
+
const legacyDescriptors = [];
|
|
627
|
+
for (const [key, child] of childMetadata) {
|
|
628
|
+
if (standardByKey.has(key))
|
|
629
|
+
continue;
|
|
630
|
+
const file = await descriptor(child.sessionFile, true);
|
|
631
|
+
if (file && this.shouldLoad(file, startAt, endAt))
|
|
632
|
+
legacyDescriptors.push(file);
|
|
633
|
+
}
|
|
634
|
+
skipped += await this.loadChanged(legacyDescriptors, signal);
|
|
635
|
+
const activeStandardKeys = new Set(standardByKey.keys());
|
|
636
|
+
for (const [key, entry] of this.cache) {
|
|
637
|
+
if (this.isWithinSessionsRoot(entry.file.path) && !activeStandardKeys.has(key)) {
|
|
638
|
+
this.cache.delete(key);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
this.pruneCache(Date.now());
|
|
642
|
+
this.skippedFiles = skipped;
|
|
643
|
+
this.coverage = { startAt, endAt, checkedAt: Date.now() };
|
|
644
|
+
this.logger.info(`[pi-desk-usage] stage=indexed elapsedMs=${Date.now() - startedAt} files=${candidates.length + legacyDescriptors.length} cached=${this.cache.size} skipped=${skipped}`);
|
|
645
|
+
}
|
|
646
|
+
shouldLoad(file, startAt, endAt) {
|
|
647
|
+
const cached = this.cache.get(pathKey(file.path));
|
|
648
|
+
if (cached && signature(cached.descriptor) === signature(file)) {
|
|
649
|
+
return overlaps(cached.file, startAt, endAt);
|
|
650
|
+
}
|
|
651
|
+
return file.modifiedAt >= startAt;
|
|
652
|
+
}
|
|
653
|
+
async loadChanged(files, signal) {
|
|
654
|
+
const changed = files.filter((file) => {
|
|
655
|
+
const cached = this.cache.get(pathKey(file.path));
|
|
656
|
+
return !cached || signature(cached.descriptor) !== signature(file);
|
|
657
|
+
});
|
|
658
|
+
if (changed.length === 0) {
|
|
659
|
+
const now = Date.now();
|
|
660
|
+
for (const file of files) {
|
|
661
|
+
const cached = this.cache.get(pathKey(file.path));
|
|
662
|
+
if (cached)
|
|
663
|
+
cached.lastAccessedAt = now;
|
|
664
|
+
}
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
const startedAt = Date.now();
|
|
668
|
+
const results = await this.scanFiles(changed, signal);
|
|
669
|
+
let skipped = 0;
|
|
670
|
+
const now = Date.now();
|
|
671
|
+
for (const result of results) {
|
|
672
|
+
const key = pathKey(result.descriptor.path);
|
|
673
|
+
if (!result.file) {
|
|
674
|
+
skipped += 1;
|
|
675
|
+
this.logger.warn('[pi-desk-usage] Session 文件解析失败', {
|
|
676
|
+
path: result.descriptor.path,
|
|
677
|
+
error: result.error
|
|
678
|
+
});
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (result.file.parseErrors > 0)
|
|
682
|
+
skipped += 1;
|
|
683
|
+
this.cache.set(key, {
|
|
684
|
+
descriptor: result.descriptor,
|
|
685
|
+
file: result.file,
|
|
686
|
+
lastAccessedAt: now
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
this.logger.info('[pi-desk-usage] 文件批量解析完成', {
|
|
690
|
+
files: changed.length,
|
|
691
|
+
skipped,
|
|
692
|
+
elapsedMs: Date.now() - startedAt
|
|
693
|
+
});
|
|
694
|
+
return skipped;
|
|
695
|
+
}
|
|
696
|
+
childMetadata() {
|
|
697
|
+
const result = new Map();
|
|
698
|
+
const mains = [...this.cache.values()].sort((left, right) => left.file.createdAt - right.file.createdAt);
|
|
699
|
+
for (const cache of mains) {
|
|
700
|
+
if (cache.descriptor.nestedSubagent)
|
|
701
|
+
continue;
|
|
702
|
+
for (const child of cache.file.childSessions) {
|
|
703
|
+
const key = pathKey(child.sessionFile);
|
|
704
|
+
if (result.has(key))
|
|
705
|
+
continue;
|
|
706
|
+
result.set(key, {
|
|
707
|
+
taskId: child.taskId,
|
|
708
|
+
agentType: child.agentType,
|
|
709
|
+
title: child.title,
|
|
710
|
+
parentSessionId: child.parentSessionId,
|
|
711
|
+
sessionFile: child.sessionFile
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
indexedFiles() {
|
|
718
|
+
const children = this.childMetadata();
|
|
719
|
+
return [...this.cache.values()].map((cache) => {
|
|
720
|
+
const referencedChild = children.get(pathKey(cache.file.path)) ?? null;
|
|
721
|
+
const inferredParentSessionId = cache.descriptor.nestedSubagent
|
|
722
|
+
? nestedParentSessionId(cache.file.path)
|
|
723
|
+
: null;
|
|
724
|
+
const child = referencedChild ??
|
|
725
|
+
(inferredParentSessionId
|
|
726
|
+
? {
|
|
727
|
+
taskId: cache.file.sessionId,
|
|
728
|
+
agentType: null,
|
|
729
|
+
title: fileTitle(cache.file),
|
|
730
|
+
parentSessionId: inferredParentSessionId
|
|
731
|
+
}
|
|
732
|
+
: null);
|
|
733
|
+
const source = cache.descriptor.nestedSubagent || child ? 'subagent' : 'main';
|
|
734
|
+
return { cache, source, child };
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
ownedEvents(startAt, endAt) {
|
|
738
|
+
const files = this.indexedFiles().sort((left, right) => left.cache.file.createdAt - right.cache.file.createdAt ||
|
|
739
|
+
left.cache.file.path.localeCompare(right.cache.file.path));
|
|
740
|
+
const seen = new Set();
|
|
741
|
+
const events = [];
|
|
742
|
+
const accessedAt = Date.now();
|
|
743
|
+
for (const indexed of files) {
|
|
744
|
+
let accessed = false;
|
|
745
|
+
for (const event of indexed.cache.file.events) {
|
|
746
|
+
if (event.timestamp < startAt || event.timestamp >= endAt || seen.has(event.identity)) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
seen.add(event.identity);
|
|
750
|
+
events.push({ indexed, event });
|
|
751
|
+
accessed = true;
|
|
752
|
+
}
|
|
753
|
+
if (accessed)
|
|
754
|
+
indexed.cache.lastAccessedAt = accessedAt;
|
|
755
|
+
}
|
|
756
|
+
return events;
|
|
757
|
+
}
|
|
758
|
+
pruneCache(now) {
|
|
759
|
+
for (const [key, entry] of this.cache) {
|
|
760
|
+
if (now - entry.lastAccessedAt > CACHE_IDLE_MS)
|
|
761
|
+
this.cache.delete(key);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
isWithinSessionsRoot(path) {
|
|
765
|
+
const value = relative(this.sessionsRoot, path);
|
|
766
|
+
return value === '' || (!value.startsWith(`..${sep}`) && value !== '..');
|
|
767
|
+
}
|
|
768
|
+
scanWithWorker(files, signal) {
|
|
769
|
+
if (files.length === 0)
|
|
770
|
+
return Promise.resolve([]);
|
|
771
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
772
|
+
const worker = new Worker(new URL('./usage-worker.js', import.meta.url), {
|
|
773
|
+
workerData: { files }
|
|
774
|
+
});
|
|
775
|
+
this.activeWorker = worker;
|
|
776
|
+
let settled = false;
|
|
777
|
+
const finish = () => {
|
|
778
|
+
if (this.activeWorker === worker)
|
|
779
|
+
this.activeWorker = null;
|
|
780
|
+
signal.removeEventListener('abort', abort);
|
|
781
|
+
};
|
|
782
|
+
const resolveOnce = (results) => {
|
|
783
|
+
if (settled)
|
|
784
|
+
return;
|
|
785
|
+
settled = true;
|
|
786
|
+
finish();
|
|
787
|
+
resolvePromise(results);
|
|
788
|
+
};
|
|
789
|
+
const rejectOnce = (error) => {
|
|
790
|
+
if (settled)
|
|
791
|
+
return;
|
|
792
|
+
settled = true;
|
|
793
|
+
finish();
|
|
794
|
+
rejectPromise(error instanceof Error ? error : new Error(String(error)));
|
|
795
|
+
};
|
|
796
|
+
const abort = () => {
|
|
797
|
+
void worker.terminate();
|
|
798
|
+
rejectOnce(new PluginMethodError(409, '用量查询已取消'));
|
|
799
|
+
};
|
|
800
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
801
|
+
worker.once('message', (message) => {
|
|
802
|
+
if (message.error)
|
|
803
|
+
rejectOnce(new Error(message.error));
|
|
804
|
+
else
|
|
805
|
+
resolveOnce(message.results ?? []);
|
|
806
|
+
});
|
|
807
|
+
worker.once('error', rejectOnce);
|
|
808
|
+
worker.once('exit', (code) => {
|
|
809
|
+
if (!settled)
|
|
810
|
+
rejectOnce(new Error(`用量 Worker 未返回结果即退出:${code}`));
|
|
811
|
+
});
|
|
812
|
+
if (signal.aborted)
|
|
813
|
+
abort();
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|