@aflueza/log-analysis 0.0.0-stage → 0.2.1-rc.2

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/lib/index.js ADDED
@@ -0,0 +1,540 @@
1
+ import { LogAnalyzer } from './analyzer.js';
2
+ import { ParserRulesDatabase, ParserRulesDatabaseError } from './database/parser-rules-database.js';
3
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
4
+ import z from '@deepseek-ai/schemastery';
5
+ const CHANNEL = '/log-analysis';
6
+ const analyzer = new LogAnalyzer();
7
+ let parserRules;
8
+ const parserSuggestionControllers = new Map();
9
+ export const name = 'log-analysis';
10
+ export const Config = z.object({
11
+ analysisProvider: z.string().default('').volatile(),
12
+ analysisModel: z.string().default('').volatile(),
13
+ analysisReasoningEffort: z.string().default('').volatile(),
14
+ });
15
+ export const inject = ['connection', 'webServer', 'tools', 'llm', 'settings'];
16
+ export function apply(ctx, config) {
17
+ parserRules = new ParserRulesDatabase(dshHomePath('plugins', 'log-analysis', 'dsh-log-analysis.sqlite'));
18
+ ctx.effect(() => () => {
19
+ parserRules?.close();
20
+ parserRules = undefined;
21
+ }, 'log-analysis: close parser rules database');
22
+ // The web profile does not mount a console logger exporter by default. Keep
23
+ // this plugin's operational records visible to the process log without
24
+ // routing unrelated DSH messages to stdout.
25
+ ctx.effect(() => ctx.logger.exporter({
26
+ export: (message) => {
27
+ const first = message.args[0];
28
+ if (typeof first !== 'string' || !first.startsWith('log-analysis:'))
29
+ return;
30
+ // eslint-disable-next-line no-console
31
+ console.log(`[${message.type.toUpperCase()}]`, ...message.args);
32
+ },
33
+ }), 'log-analysis: console exporter');
34
+ ctx.inject(['connection', 'webServer', 'llm', 'settings'], (rpcCtx) => {
35
+ const webServer = rpcCtx.webServer;
36
+ const runtime = rpcCtx;
37
+ rpcCtx.effect(() => {
38
+ const unconfigure = runtime.settings.configure({ auto: false }, rpcCtx.fiber);
39
+ const unregister = webServer.register({
40
+ kind: 'prefix',
41
+ path: CHANNEL,
42
+ handler: (request, response) => handleRpcRequest(rpcCtx.connection, runtime, request, response),
43
+ });
44
+ return () => {
45
+ unregister();
46
+ unconfigure();
47
+ };
48
+ }, 'log-analysis: RPC route');
49
+ });
50
+ ctx.inject(['tools'], (toolCtx) => {
51
+ const tools = toolCtx.tools;
52
+ const dispose = tools.register(rawTool('log_analysis_summary', '查询当前日志分析会话的概览统计、Top 错误、趋势和最近异常。', {}, async () => ({ summary: analyzer.summary() })));
53
+ return dispose;
54
+ });
55
+ }
56
+ function rawTool(name, description, parameters, execute) {
57
+ return {
58
+ name,
59
+ description,
60
+ parameters: { type: 'object', properties: parameters },
61
+ output: {
62
+ schema: { type: 'json' },
63
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
64
+ },
65
+ execute,
66
+ };
67
+ }
68
+ async function handleRpcRequest(connection, runtime, request, response) {
69
+ const rejection = connection.requestRejection(request);
70
+ if (rejection !== undefined) {
71
+ response.writeHead(rejection);
72
+ response.end(rejection === 401 ? 'unauthorized' : 'forbidden');
73
+ return;
74
+ }
75
+ const endpoint = endpointFromRequest(request);
76
+ if (request.method !== 'POST' || endpoint === null) {
77
+ response.writeHead(404);
78
+ response.end('not found');
79
+ return;
80
+ }
81
+ const mediaType = request.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase();
82
+ if (mediaType !== 'application/json') {
83
+ response.writeHead(415);
84
+ response.end('content type must be application/json');
85
+ return;
86
+ }
87
+ let message;
88
+ try {
89
+ message = parseClientRequest(await readJsonBody(request));
90
+ }
91
+ catch {
92
+ response.writeHead(400);
93
+ response.end('invalid rpc request');
94
+ return;
95
+ }
96
+ if (message.method !== endpoint) {
97
+ writeRpcResult(response, message.rpcId, fail('invalid-payload', `method ${JSON.stringify(message.method)} does not match endpoint ${JSON.stringify(endpoint)}`));
98
+ return;
99
+ }
100
+ writeRpcResult(response, message.rpcId, await dispatch(endpoint, message.payload, runtime));
101
+ }
102
+ function parseClientRequest(value) {
103
+ if (!isRecord(value) ||
104
+ value.type !== 'client-request' ||
105
+ typeof value.rpcId !== 'string' ||
106
+ typeof value.method !== 'string' ||
107
+ !('payload' in value)) {
108
+ throw new TypeError('invalid client request envelope');
109
+ }
110
+ return {
111
+ type: 'client-request',
112
+ rpcId: value.rpcId,
113
+ method: value.method,
114
+ payload: value.payload,
115
+ };
116
+ }
117
+ function endpointFromRequest(request) {
118
+ const pathname = new URL(request.url ?? '/', 'http://x').pathname;
119
+ const prefix = `${CHANNEL}/`;
120
+ if (!pathname.startsWith(prefix))
121
+ return null;
122
+ return pathname.slice(prefix.length) || null;
123
+ }
124
+ async function readJsonBody(request) {
125
+ const chunks = [];
126
+ let size = 0;
127
+ for await (const chunk of request) {
128
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
129
+ size += buffer.byteLength;
130
+ if (size > 16 * 1024 * 1024)
131
+ throw new RangeError('request body exceeds 16 MiB');
132
+ chunks.push(buffer);
133
+ }
134
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
135
+ }
136
+ function writeRpcResult(response, rpcId, result) {
137
+ response.writeHead(200, { 'content-type': 'application/json' });
138
+ response.end(JSON.stringify({ type: 'server-response', rpcId, result }));
139
+ }
140
+ async function dispatch(endpoint, payload, runtime) {
141
+ try {
142
+ return ok(await dispatchEndpoint(endpoint, payload, runtime));
143
+ }
144
+ catch (error) {
145
+ const failure = error instanceof AnalysisRpcError
146
+ ? error
147
+ : error instanceof ParserRulesDatabaseError
148
+ ? new AnalysisRpcError(error.code, error.message)
149
+ : new AnalysisRpcError('analysis-failed', error instanceof Error ? error.message : String(error));
150
+ return fail(failure.code, failure.message);
151
+ }
152
+ }
153
+ async function dispatchEndpoint(endpoint, payload, runtime) {
154
+ switch (endpoint) {
155
+ case 'parser-rules/list':
156
+ return parserRules?.list() ?? [];
157
+ case 'parser-rules/get':
158
+ return parserRules?.get(String(payload?.id));
159
+ case 'parser-rules/create': {
160
+ const input = payload;
161
+ if (typeof input.name !== 'string' ||
162
+ !input.rule ||
163
+ (input.sampleRaw !== undefined && typeof input.sampleRaw !== 'string'))
164
+ throw new AnalysisRpcError('invalid-payload', '解析规则参数无效');
165
+ return await parserRules.create(input.name, input.rule, String(input.sampleRaw ?? ''));
166
+ }
167
+ case 'parser-rules/update': {
168
+ const input = payload;
169
+ if (typeof input.id !== 'string' ||
170
+ typeof input.name !== 'string' ||
171
+ !input.rule ||
172
+ (input.sampleRaw !== undefined && typeof input.sampleRaw !== 'string'))
173
+ throw new AnalysisRpcError('invalid-payload', '解析规则参数无效');
174
+ return await parserRules.update(input.id, input.name, input.rule, String(input.sampleRaw ?? ''));
175
+ }
176
+ case 'parser-rules/delete': {
177
+ const input = payload;
178
+ if (typeof input?.id !== 'string')
179
+ throw new AnalysisRpcError('invalid-payload', '解析规则参数无效');
180
+ await parserRules.delete(input.id);
181
+ return { id: input.id, deleted: true };
182
+ }
183
+ case 'parser-rules/validate': {
184
+ const input = payload;
185
+ if (!input?.rule || typeof input.sampleRaw !== 'string' || typeof input.fileName !== 'string')
186
+ throw new AnalysisRpcError('invalid-payload', '规则验证参数无效');
187
+ return analyzer.previewRuleOnSample(input.rule, input.sampleRaw, input.fileName);
188
+ }
189
+ case 'analysis/models':
190
+ return modelCatalog(runtime);
191
+ case 'analysis/model-info': {
192
+ const input = payload;
193
+ if (typeof input?.provider !== 'string' || typeof input.model !== 'string')
194
+ throw new AnalysisRpcError('invalid-payload', '模型信息请求无效');
195
+ const info = await runtime.llm.resolveModelInfo(input.provider, input.model);
196
+ return info.reasoning ? { reasoning: info.reasoning } : {};
197
+ }
198
+ case 'analysis/parser/suggest/cancel': {
199
+ const input = payload;
200
+ if (!input || (input.sessionId !== undefined && input.sessionId !== analyzer.sessionId))
201
+ throw new AnalysisRpcError('analysis-session-expired', '分析会话已失效,请重新开始分析');
202
+ if (typeof input.requestId !== 'string')
203
+ throw new AnalysisRpcError('invalid-payload', '识别取消请求无效');
204
+ const controller = parserSuggestionControllers.get(input.requestId);
205
+ controller?.abort(new Error('日志格式识别已取消'));
206
+ return { requestId: input.requestId, cancelled: Boolean(controller) };
207
+ }
208
+ case 'analysis/start': {
209
+ const input = payload;
210
+ const files = input?.files;
211
+ if (!Array.isArray(files) || files.some((file) => !isFileInput(file))) {
212
+ throw new Error('analysis/start requires file IDs, names, and sizes');
213
+ }
214
+ const samples = input?.samples;
215
+ if (samples !== undefined &&
216
+ (!Array.isArray(samples) || samples.some((sample) => !isParserSample(sample))))
217
+ throw new Error('analysis/start requires bounded log samples');
218
+ const query = input?.query;
219
+ if (query !== undefined &&
220
+ (query === null || typeof query !== 'object' || Array.isArray(query))) {
221
+ throw new Error('analysis/start query must be an object');
222
+ }
223
+ return analyzer.start(files, (samples ?? []), (query ?? {}));
224
+ }
225
+ case 'analysis/sample/open': {
226
+ const input = payload;
227
+ if (input?.sessionId !== analyzer.sessionId || typeof input.fileId !== 'string')
228
+ throw new AnalysisRpcError('invalid-payload', '样例分块打开请求无效');
229
+ const level = input.level === undefined || input.level === 'ALL'
230
+ ? undefined
231
+ : String(input.level);
232
+ return analyzer.openSampleChunk(input.fileId, level);
233
+ }
234
+ case 'analysis/sample/chunk': {
235
+ const input = payload;
236
+ if (input?.sessionId !== analyzer.sessionId ||
237
+ typeof input.fileId !== 'string' ||
238
+ typeof input.offset !== 'number' ||
239
+ typeof input.content !== 'string')
240
+ throw new AnalysisRpcError('invalid-payload', '样例分块请求无效');
241
+ if (Buffer.byteLength(input.content, 'utf8') > 512 * 1024)
242
+ throw new AnalysisRpcError('chunk-too-large', '单个文件分块不能超过 512 KiB');
243
+ const byteLength = input.byteLength === undefined ? undefined : Number(input.byteLength);
244
+ if (byteLength !== undefined &&
245
+ (!Number.isSafeInteger(byteLength) || byteLength < 0 || byteLength > 512 * 1024))
246
+ throw new AnalysisRpcError('invalid-payload', '样例分块字节长度无效');
247
+ return analyzer.appendSampleChunk(input.fileId, input.offset, input.content, input.eof === true, byteLength);
248
+ }
249
+ case 'analysis/sample/finish': {
250
+ const input = payload;
251
+ if (input?.sessionId !== analyzer.sessionId || typeof input.fileId !== 'string')
252
+ throw new AnalysisRpcError('invalid-payload', '样例分块完成请求无效');
253
+ return analyzer.finishSampleChunk(input.fileId);
254
+ }
255
+ case 'analysis/sample/cancel': {
256
+ const input = payload;
257
+ if (input?.sessionId !== analyzer.sessionId || typeof input.fileId !== 'string')
258
+ throw new AnalysisRpcError('invalid-payload', '样例分块取消请求无效');
259
+ analyzer.cancelSampleChunk(input.fileId);
260
+ return { fileId: input.fileId, cancelled: true };
261
+ }
262
+ case 'analysis/parser/suggest': {
263
+ const input = payload;
264
+ const hasExternalSample = typeof input?.sampleRaw === 'string';
265
+ if (!hasExternalSample && input?.sessionId !== analyzer.sessionId)
266
+ throw new AnalysisRpcError('analysis-session-expired', '分析会话已失效,请重新开始分析');
267
+ if (typeof input?.requestId !== 'string' ||
268
+ (hasExternalSample
269
+ ? !String(input.sampleRaw).trim() ||
270
+ String(input.sampleRaw).length > 120_000 ||
271
+ typeof input.fileName !== 'string'
272
+ : typeof input.fileId !== 'string' || typeof input.sampleVersion !== 'number')) {
273
+ throw new AnalysisRpcError('invalid-payload', '智能识别请求缺少样例信息');
274
+ }
275
+ if (parserSuggestionControllers.has(input.requestId))
276
+ throw new AnalysisRpcError('invalid-payload', '识别请求 ID 已在使用');
277
+ const controller = new AbortController();
278
+ parserSuggestionControllers.set(input.requestId, controller);
279
+ try {
280
+ const config = activeConfig(runtime);
281
+ if (!config.analysisProvider || !config.analysisModel)
282
+ throw new AnalysisRpcError('analysis-model-not-configured', '请先在设置中选择分析模型');
283
+ const provider = runtime.llm
284
+ .listProviders()
285
+ .find((item) => item.id === config.analysisProvider);
286
+ if (!provider) {
287
+ await runtime.settings.update('log-analysis', {
288
+ analysisProvider: '',
289
+ analysisModel: '',
290
+ analysisReasoningEffort: '',
291
+ });
292
+ throw new AnalysisRpcError('analysis-model-unavailable', '分析模型不可用,请重新选择');
293
+ }
294
+ const models = await runtime.llm.listModels(config.analysisProvider);
295
+ const selectedModelInfo = models.find((item) => item.id === config.analysisModel);
296
+ if (!selectedModelInfo) {
297
+ await runtime.settings.update('log-analysis', {
298
+ analysisProvider: '',
299
+ analysisModel: '',
300
+ analysisReasoningEffort: '',
301
+ });
302
+ throw new AnalysisRpcError('analysis-model-unavailable', '分析模型不可用,请重新选择');
303
+ }
304
+ const resolvedModelInfo = await runtime.llm.resolveModelInfo(config.analysisProvider, config.analysisModel);
305
+ const efforts = resolvedModelInfo.reasoning?.efforts ?? [];
306
+ const reasoningEffort = config.analysisReasoningEffort &&
307
+ efforts.some((effort) => effort.id === config.analysisReasoningEffort)
308
+ ? config.analysisReasoningEffort
309
+ : (resolvedModelInfo.reasoning?.defaultEffort ?? efforts[0]?.id);
310
+ const fileId = hasExternalSample
311
+ ? `rule-management:${input.requestId}`
312
+ : String(input.fileId);
313
+ const requestedSampleVersion = hasExternalSample ? -1 : Number(input.sampleVersion);
314
+ runtime.logger.info(`log-analysis: parser suggestion started provider=${config.analysisProvider} model=${config.analysisModel} fileId=${fileId} sampleVersion=${requestedSampleVersion}`);
315
+ try {
316
+ const result = await analyzer.suggestParser(config.analysisProvider, config.analysisModel, async function* (selectedProvider, selectedModel, prompt) {
317
+ yield* runtime.llm.stream({
318
+ provider: selectedProvider,
319
+ model: selectedModel,
320
+ ...(reasoningEffort ? { reasoningEffort } : {}),
321
+ signal: controller.signal,
322
+ messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
323
+ });
324
+ }, fileId, requestedSampleVersion, controller.signal, (phase, content) => {
325
+ runtime.logger.info(`log-analysis: parser suggestion model ${phase} requestId=${input.requestId}\n${content}`);
326
+ }, hasExternalSample
327
+ ? { content: String(input.sampleRaw), fileName: String(input.fileName) }
328
+ : undefined);
329
+ runtime.logger.info(`log-analysis: parser suggestion completed format=${result.format} records=${result.records.length} validationErrors=${result.validationErrors.length} executionErrors=${result.executionErrors.length}`);
330
+ return result;
331
+ }
332
+ catch (error) {
333
+ if (controller.signal.aborted)
334
+ throw new AnalysisRpcError('analysis-parser-suggestion-cancelled', '日志格式识别已取消');
335
+ const message = error instanceof Error ? error.message : String(error);
336
+ runtime.logger.error(`log-analysis: parser suggestion failed provider=${config.analysisProvider} model=${config.analysisModel} fileId=${input.fileId} sampleVersion=${input.sampleVersion} error=${message}`);
337
+ throw new AnalysisRpcError('analysis-parser-suggestion-failed', message);
338
+ }
339
+ }
340
+ finally {
341
+ if (parserSuggestionControllers.get(input.requestId) === controller)
342
+ parserSuggestionControllers.delete(input.requestId);
343
+ }
344
+ }
345
+ case 'analysis/parser/sample': {
346
+ const input = payload;
347
+ if (input?.sessionId !== analyzer.sessionId ||
348
+ typeof input.fileId !== 'string' ||
349
+ typeof input.content !== 'string')
350
+ throw new AnalysisRpcError('invalid-payload', '样例更新请求无效或会话已失效');
351
+ if (input.source !== 'default' && input.source !== 'level' && input.source !== 'manual')
352
+ throw new AnalysisRpcError('invalid-payload', '样例来源无效');
353
+ const level = input.level === undefined ? undefined : String(input.level);
354
+ if (level !== undefined &&
355
+ !['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'UNKNOWN'].includes(level)) {
356
+ throw new AnalysisRpcError('invalid-payload', '样例级别无效');
357
+ }
358
+ if (input.rule !== undefined)
359
+ return analyzer.previewParserRule(input.rule, input.fileId);
360
+ return analyzer.setSample(input.fileId, input.content, input.source, level);
361
+ }
362
+ case 'analysis/file': {
363
+ const input = payload;
364
+ if (!input ||
365
+ typeof input.fileId !== 'string' ||
366
+ typeof input.ruleId !== 'string' ||
367
+ typeof input.content !== 'string') {
368
+ throw new Error('analysis/file requires a file ID, rule ID, and complete text content');
369
+ }
370
+ const record = parserRules.get(input.ruleId);
371
+ return analyzer.analyzeFile(input.fileId, input.content, record.id, record.rule);
372
+ }
373
+ case 'analysis/file/open': {
374
+ const input = payload;
375
+ if (input?.sessionId !== analyzer.sessionId ||
376
+ typeof input.fileId !== 'string' ||
377
+ typeof input.ruleId !== 'string')
378
+ throw new AnalysisRpcError('invalid-payload', '文件分块打开请求无效');
379
+ if (input.query !== undefined &&
380
+ (input.query === null || typeof input.query !== 'object' || Array.isArray(input.query)))
381
+ throw new AnalysisRpcError('invalid-payload', '分析条件无效');
382
+ if (input.query !== undefined)
383
+ analyzer.setQuery(input.query);
384
+ const record = parserRules.get(input.ruleId);
385
+ return analyzer.openFileChunk(input.fileId, record.id, record.rule);
386
+ }
387
+ case 'analysis/file/chunk': {
388
+ const input = payload;
389
+ if (input?.sessionId !== analyzer.sessionId ||
390
+ typeof input.fileId !== 'string' ||
391
+ typeof input.offset !== 'number' ||
392
+ typeof input.content !== 'string')
393
+ throw new AnalysisRpcError('invalid-payload', '文件分块请求无效');
394
+ if (Buffer.byteLength(input.content, 'utf8') > 512 * 1024)
395
+ throw new AnalysisRpcError('chunk-too-large', '单个文件分块不能超过 512 KiB');
396
+ const byteLength = input.byteLength === undefined ? undefined : Number(input.byteLength);
397
+ if (byteLength !== undefined &&
398
+ (!Number.isSafeInteger(byteLength) || byteLength < 0 || byteLength > 512 * 1024))
399
+ throw new AnalysisRpcError('invalid-payload', '文件分块字节长度无效');
400
+ return analyzer.appendFileChunk(input.fileId, input.offset, input.content, input.eof === true, byteLength);
401
+ }
402
+ case 'analysis/file/finish': {
403
+ const input = payload;
404
+ if (input?.sessionId !== analyzer.sessionId || typeof input.fileId !== 'string')
405
+ throw new AnalysisRpcError('invalid-payload', '文件分块完成请求无效');
406
+ return analyzer.finishFileChunk(input.fileId);
407
+ }
408
+ case 'analysis/file/cancel': {
409
+ const input = payload;
410
+ if (input?.sessionId !== analyzer.sessionId || typeof input.fileId !== 'string')
411
+ throw new AnalysisRpcError('invalid-payload', '文件分块取消请求无效');
412
+ return analyzer.cancelFileChunk(input.fileId);
413
+ }
414
+ case 'analysis/file-failed': {
415
+ const input = payload;
416
+ if (!input || typeof input.fileId !== 'string' || typeof input.message !== 'string') {
417
+ throw new Error('analysis/file-failed requires a file ID and error message');
418
+ }
419
+ return analyzer.failFile(input.fileId, input.message);
420
+ }
421
+ case 'analysis/remove-file': {
422
+ const input = payload;
423
+ if (!input || typeof input.fileId !== 'string') {
424
+ throw new Error('analysis/remove-file requires a file ID');
425
+ }
426
+ return analyzer.removeFile(input.fileId);
427
+ }
428
+ case 'analysis/summary': {
429
+ const input = payload;
430
+ if (typeof input?.ruleId !== 'string' || !input.ruleId.trim())
431
+ throw new AnalysisRpcError('invalid-payload', '查询分析摘要必须提供解析规则 ID');
432
+ const rule = parserRules.get(input.ruleId);
433
+ return analyzer.summary(rule);
434
+ }
435
+ case 'analysis/entry':
436
+ return analyzer.entry(String(payload.id));
437
+ case 'analysis/clear':
438
+ analyzer.clear();
439
+ return analyzer.summary();
440
+ case 'analysis/cancel':
441
+ analyzer.cancel();
442
+ return analyzer.summary();
443
+ }
444
+ }
445
+ class AnalysisRpcError extends Error {
446
+ code;
447
+ constructor(code, message) {
448
+ super(message);
449
+ this.code = code;
450
+ }
451
+ }
452
+ function activeConfig(runtime) {
453
+ const value = runtime.settings.describe().find((row) => row.ns === 'log-analysis')?.value;
454
+ if (!isRecord(value))
455
+ return { analysisProvider: '', analysisModel: '', analysisReasoningEffort: '' };
456
+ return {
457
+ analysisProvider: typeof value.analysisProvider === 'string' ? value.analysisProvider : '',
458
+ analysisModel: typeof value.analysisModel === 'string' ? value.analysisModel : '',
459
+ analysisReasoningEffort: typeof value.analysisReasoningEffort === 'string' ? value.analysisReasoningEffort : '',
460
+ };
461
+ }
462
+ async function modelCatalog(runtime) {
463
+ const config = activeConfig(runtime);
464
+ const providers = await Promise.all(runtime.llm.listProviders().map(async (provider) => {
465
+ try {
466
+ const models = await runtime.llm.listModels(provider.id);
467
+ return { ...provider, models: models.map(({ id, name }) => ({ id, name })) };
468
+ }
469
+ catch {
470
+ return { ...provider, models: [] };
471
+ }
472
+ }));
473
+ const selected = providers.find((provider) => provider.id === config.analysisProvider);
474
+ const wasConfigured = Boolean(config.analysisProvider || config.analysisModel);
475
+ const selectedModel = selected?.models.find((model) => model.id === config.analysisModel);
476
+ const configured = Boolean(selectedModel);
477
+ if ((config.analysisProvider || config.analysisModel) && !configured) {
478
+ await runtime.settings.update('log-analysis', {
479
+ analysisProvider: '',
480
+ analysisModel: '',
481
+ analysisReasoningEffort: '',
482
+ });
483
+ }
484
+ else if (configured) {
485
+ let reasoning;
486
+ try {
487
+ reasoning = (await runtime.llm.resolveModelInfo(config.analysisProvider, config.analysisModel)).reasoning;
488
+ }
489
+ catch {
490
+ reasoning = undefined;
491
+ }
492
+ const efforts = reasoning?.efforts ?? [];
493
+ if ((!config.analysisReasoningEffort ||
494
+ !efforts.some((effort) => effort.id === config.analysisReasoningEffort)) &&
495
+ (reasoning || config.analysisReasoningEffort)) {
496
+ await runtime.settings.update('log-analysis', {
497
+ analysisReasoningEffort: reasoning?.defaultEffort ?? efforts[0]?.id ?? '',
498
+ });
499
+ }
500
+ if (reasoning && selected) {
501
+ selected.models = selected.models.map((model) => model.id === config.analysisModel ? { ...model, reasoning } : model);
502
+ }
503
+ }
504
+ return {
505
+ providers,
506
+ configured,
507
+ invalidated: wasConfigured && !configured,
508
+ ...(configured
509
+ ? { selectedProvider: config.analysisProvider, selectedModel: config.analysisModel }
510
+ : {}),
511
+ ...(configured && config.analysisReasoningEffort
512
+ ? { selectedReasoningEffort: config.analysisReasoningEffort }
513
+ : {}),
514
+ };
515
+ }
516
+ function isFileInput(value) {
517
+ if (value === null || typeof value !== 'object')
518
+ return false;
519
+ const file = value;
520
+ return (typeof file.id === 'string' &&
521
+ typeof file.name === 'string' &&
522
+ typeof file.size === 'number' &&
523
+ Number.isFinite(file.size) &&
524
+ file.size >= 0);
525
+ }
526
+ function isParserSample(value) {
527
+ return (isRecord(value) &&
528
+ typeof value.fileId === 'string' &&
529
+ typeof value.content === 'string' &&
530
+ value.content.length <= 512_000);
531
+ }
532
+ function ok(value) {
533
+ return { ok: true, value };
534
+ }
535
+ function fail(code, message) {
536
+ return { ok: false, error: { code, message, details: {} } };
537
+ }
538
+ function isRecord(value) {
539
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
540
+ }
@@ -0,0 +1 @@
1
+ export {};