cortico 0.1.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/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +43 -0
- package/src/boot.ts +46 -0
- package/src/bot.ts +1342 -0
- package/src/config-file.ts +41 -0
- package/src/core/README.md +92 -0
- package/src/core/billing.ts +26 -0
- package/src/core/blobs.ts +106 -0
- package/src/core/bus.ts +311 -0
- package/src/core/config-schema.ts +201 -0
- package/src/core/config.ts +262 -0
- package/src/core/core.ts +699 -0
- package/src/core/cost.ts +244 -0
- package/src/core/event-store.ts +304 -0
- package/src/core/fork.ts +113 -0
- package/src/core/generation.ts +118 -0
- package/src/core/instance-lock.ts +215 -0
- package/src/core/ipc-logger.ts +105 -0
- package/src/core/language.ts +62 -0
- package/src/core/log-context.ts +38 -0
- package/src/core/loop.ts +1858 -0
- package/src/core/markers.ts +48 -0
- package/src/core/prefix.ts +121 -0
- package/src/core/run.ts +95 -0
- package/src/core/secrets.ts +18 -0
- package/src/core/session.ts +56 -0
- package/src/core/sessions.ts +261 -0
- package/src/core/state.ts +104 -0
- package/src/core/template.ts +72 -0
- package/src/core/timers.ts +129 -0
- package/src/core/tool-log.ts +135 -0
- package/src/core/transcript.ts +92 -0
- package/src/core/truncate.ts +121 -0
- package/src/core/types.ts +1140 -0
- package/src/core/usage-log.ts +79 -0
- package/src/core/util.ts +386 -0
- package/src/deploy-listing.ts +160 -0
- package/src/deploy.ts +162 -0
- package/src/extensions/README.md +72 -0
- package/src/extensions/dry-mount.ts +390 -0
- package/src/extensions/manifest.ts +153 -0
- package/src/extensions/runtime.ts +42 -0
- package/src/extensions.ts +519 -0
- package/src/launcher.ts +285 -0
- package/src/paths.ts +155 -0
- package/src/protocol/open-responses/LICENSE +201 -0
- package/src/protocol/open-responses/README.md +14 -0
- package/src/protocol/open-responses/context-helpers.ts +27 -0
- package/src/protocol/open-responses/context-log.ts +59 -0
- package/src/protocol/open-responses/context.ts +89 -0
- package/src/protocol/open-responses/generated.ts +115 -0
- package/src/protocol/open-responses/index.ts +33 -0
- package/src/protocol/open-responses/openapi.json +4230 -0
- package/src/protocol/open-responses/stream.ts +146 -0
- package/src/protocol/open-responses/tokens.ts +31 -0
- package/src/providers/README.md +86 -0
- package/src/providers/base.ts +111 -0
- package/src/providers/configuration.ts +97 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/settings.ts +443 -0
- package/src/providers/console/strings.ts +74 -0
- package/src/providers/console/types.ts +11 -0
- package/src/providers/llamacpp/archive.ts +148 -0
- package/src/providers/llamacpp/catalog.ts +247 -0
- package/src/providers/llamacpp/config.ts +36 -0
- package/src/providers/llamacpp/console/client.ts +6 -0
- package/src/providers/llamacpp/console/models-panel.ts +120 -0
- package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
- package/src/providers/llamacpp/console/server.ts +153 -0
- package/src/providers/llamacpp/index.ts +95 -0
- package/src/providers/llamacpp/native.ts +51 -0
- package/src/providers/llamacpp/options.ts +117 -0
- package/src/providers/llamacpp/runtime-store.ts +139 -0
- package/src/providers/llamacpp/runtime.ts +214 -0
- package/src/providers/llamacpp/server.ts +286 -0
- package/src/providers/llamacpp/strings.ts +230 -0
- package/src/providers/openai-responses-compat/config.ts +20 -0
- package/src/providers/openai-responses-compat/index.ts +89 -0
- package/src/providers/openai-responses-compat/native.ts +127 -0
- package/src/providers/openai-responses-compat/strings.ts +19 -0
- package/src/providers/pricebook.ts +75 -0
- package/src/providers/registry.ts +137 -0
- package/src/providers/strings.ts +71 -0
- package/src/providers/transport/chat.ts +156 -0
- package/src/providers/transport/errors.ts +48 -0
- package/src/providers/transport/history.ts +68 -0
- package/src/providers/transport/native-input.ts +83 -0
- package/src/providers/transport/native-types.ts +13 -0
- package/src/providers/transport/response-assembly.ts +191 -0
- package/src/providers/transport/response-http.ts +196 -0
- package/src/providers/transport/response-meters.ts +27 -0
- package/src/providers/transport/responses-input.ts +56 -0
- package/src/web/README.md +84 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
- package/src/web/client/console-pages/builtins.ts +10 -0
- package/src/web/client/console-pages/context.ts +135 -0
- package/src/web/client/console-pages/host.ts +633 -0
- package/src/web/client/console-pages/loader.ts +132 -0
- package/src/web/client/console-pages/strings.ts +90 -0
- package/src/web/client/console-pages/tools/strings.ts +49 -0
- package/src/web/client/console-pages/tools/view.ts +215 -0
- package/src/web/client/core/api.ts +200 -0
- package/src/web/client/core/language.ts +56 -0
- package/src/web/client/core/lifecycle.ts +132 -0
- package/src/web/client/core/router.ts +199 -0
- package/src/web/client/core/stream.ts +157 -0
- package/src/web/client/core/websocket.ts +82 -0
- package/src/web/client/features/appearance/index.ts +333 -0
- package/src/web/client/features/appearance/strings.ts +87 -0
- package/src/web/client/features/config/index.ts +11 -0
- package/src/web/client/features/config/strings.ts +41 -0
- package/src/web/client/features/config/view.ts +428 -0
- package/src/web/client/features/core/events.ts +197 -0
- package/src/web/client/features/core/index.ts +254 -0
- package/src/web/client/features/core/run.ts +56 -0
- package/src/web/client/features/core/runlog.ts +214 -0
- package/src/web/client/features/core/sessions.ts +53 -0
- package/src/web/client/features/core/strings.ts +157 -0
- package/src/web/client/features/extensions/index.ts +422 -0
- package/src/web/client/features/extensions/strings.ts +164 -0
- package/src/web/client/features/feature.ts +93 -0
- package/src/web/client/features/live/context.ts +280 -0
- package/src/web/client/features/live/fork.ts +156 -0
- package/src/web/client/features/live/index.ts +392 -0
- package/src/web/client/features/live/onboarding.ts +113 -0
- package/src/web/client/features/live/protocol.ts +122 -0
- package/src/web/client/features/live/sessions.ts +71 -0
- package/src/web/client/features/live/status.ts +68 -0
- package/src/web/client/features/live/strings.ts +252 -0
- package/src/web/client/features/live/timeline.ts +590 -0
- package/src/web/client/features/prompts/editor.ts +292 -0
- package/src/web/client/features/prompts/index.ts +346 -0
- package/src/web/client/features/prompts/strings.ts +136 -0
- package/src/web/client/features/prompts/view.ts +256 -0
- package/src/web/client/features/providers/index.ts +100 -0
- package/src/web/client/features/providers/strings.ts +23 -0
- package/src/web/client/features/settings/general.ts +28 -0
- package/src/web/client/features/settings/index.ts +82 -0
- package/src/web/client/features/settings/strings.ts +33 -0
- package/src/web/client/features/storage/index.ts +1 -0
- package/src/web/client/features/storage/strings.ts +45 -0
- package/src/web/client/features/storage/view.ts +154 -0
- package/src/web/client/features/usage/chart.ts +486 -0
- package/src/web/client/features/usage/index.ts +407 -0
- package/src/web/client/features/usage/labels.ts +46 -0
- package/src/web/client/features/usage/range.ts +36 -0
- package/src/web/client/features/usage/state.ts +37 -0
- package/src/web/client/features/usage/strings.ts +191 -0
- package/src/web/client/features/usage/tooltip.ts +86 -0
- package/src/web/client/features/usage/types.ts +96 -0
- package/src/web/client/features/worlds/index.ts +424 -0
- package/src/web/client/features/worlds/strings.ts +121 -0
- package/src/web/client/main.ts +273 -0
- package/src/web/client/shell/avatar.ts +233 -0
- package/src/web/client/shell/index.ts +549 -0
- package/src/web/client/shell/strings.ts +89 -0
- package/src/web/client/theme/handoff.ts +43 -0
- package/src/web/client/theme/palette.ts +109 -0
- package/src/web/client/theme/registry.ts +130 -0
- package/src/web/client/theme/storage.ts +72 -0
- package/src/web/client/theme/strings.ts +123 -0
- package/src/web/client/theme/studio.ts +366 -0
- package/src/web/client/ui/actions.ts +109 -0
- package/src/web/client/ui/data.ts +185 -0
- package/src/web/client/ui/dom.ts +23 -0
- package/src/web/client/ui/fields.ts +191 -0
- package/src/web/client/ui/format.ts +75 -0
- package/src/web/client/ui/icons.ts +183 -0
- package/src/web/client/ui/images.ts +91 -0
- package/src/web/client/ui/index.ts +89 -0
- package/src/web/client/ui/lamp.ts +125 -0
- package/src/web/client/ui/log.ts +98 -0
- package/src/web/client/ui/overlay.ts +184 -0
- package/src/web/client/ui/page.ts +8 -0
- package/src/web/client/ui/prompt-input.tsx +311 -0
- package/src/web/client/ui/sheet.ts +131 -0
- package/src/web/client/ui/strings.ts +71 -0
- package/src/web/console-pages.ts +367 -0
- package/src/web/files.ts +107 -0
- package/src/web/path-picker.ts +296 -0
- package/src/web/public/index.html +17 -0
- package/src/web/public/styles.css +1715 -0
- package/src/web/server.ts +1883 -0
- package/src/web/shared/client-panel.ts +603 -0
- package/src/web/shared/console-protocol.ts +560 -0
- package/src/web/shared/css.d.ts +1 -0
- package/src/web/shared/path-picker.ts +23 -0
- package/src/web/shared/theme.ts +256 -0
- package/src/web/theme-store.ts +37 -0
- package/src/world.ts +449 -0
- package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
- package/src/worlds/bilibili/README.md +176 -0
- package/src/worlds/bilibili/audience-admission.ts +995 -0
- package/src/worlds/bilibili/client.ts +528 -0
- package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
- package/src/worlds/bilibili/config.ts +185 -0
- package/src/worlds/bilibili/console/client.ts +202 -0
- package/src/worlds/bilibili/definition.ts +35 -0
- package/src/worlds/bilibili/gift-frame.ts +131 -0
- package/src/worlds/bilibili/normalize.ts +452 -0
- package/src/worlds/bilibili/overlay/announcement.ts +76 -0
- package/src/worlds/bilibili/overlay/assets.ts +82 -0
- package/src/worlds/bilibili/overlay/model.ts +494 -0
- package/src/worlds/bilibili/overlay/project.ts +202 -0
- package/src/worlds/bilibili/overlay/server.ts +286 -0
- package/src/worlds/bilibili/overlay/types.ts +217 -0
- package/src/worlds/bilibili/overlay/web/app.js +690 -0
- package/src/worlds/bilibili/overlay/web/editor.css +297 -0
- package/src/worlds/bilibili/overlay/web/editor.html +102 -0
- package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
- package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
- package/src/worlds/bilibili/overlay/web/styles.css +258 -0
- package/src/worlds/bilibili/protobuf.ts +123 -0
- package/src/worlds/bilibili/wire.ts +94 -0
- package/src/worlds/bilibili/world.ts +1389 -0
- package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
- package/src/worlds/console-fixture/console/client.ts +72 -0
- package/src/worlds/console-fixture/world.ts +73 -0
- package/src/worlds/index.ts +15 -0
- package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
- package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
- package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
- package/src/worlds/minecraft/README.md +917 -0
- package/src/worlds/minecraft/blueprint-plan.ts +915 -0
- package/src/worlds/minecraft/blueprint-registry.ts +566 -0
- package/src/worlds/minecraft/blueprint-repair.ts +236 -0
- package/src/worlds/minecraft/blueprint-resource.ts +272 -0
- package/src/worlds/minecraft/blueprint.ts +492 -0
- package/src/worlds/minecraft/body-lease.ts +340 -0
- package/src/worlds/minecraft/bridge.ts +749 -0
- package/src/worlds/minecraft/check.ts +651 -0
- package/src/worlds/minecraft/chests.ts +292 -0
- package/src/worlds/minecraft/client-launch.ts +272 -0
- package/src/worlds/minecraft/client-options.ts +85 -0
- package/src/worlds/minecraft/client-skins.ts +164 -0
- package/src/worlds/minecraft/client-window.ps1 +91 -0
- package/src/worlds/minecraft/client.ts +423 -0
- package/src/worlds/minecraft/combat-context.ts +109 -0
- package/src/worlds/minecraft/combat.ts +1552 -0
- package/src/worlds/minecraft/config.ts +499 -0
- package/src/worlds/minecraft/console/access.ts +218 -0
- package/src/worlds/minecraft/console/client.ts +244 -0
- package/src/worlds/minecraft/console/log.ts +110 -0
- package/src/worlds/minecraft/console/mount.ts +420 -0
- package/src/worlds/minecraft/console/skin.ts +257 -0
- package/src/worlds/minecraft/console/style.css +154 -0
- package/src/worlds/minecraft/console/world.ts +449 -0
- package/src/worlds/minecraft/deaths.ts +135 -0
- package/src/worlds/minecraft/definition.ts +18 -0
- package/src/worlds/minecraft/engine-child.ts +274 -0
- package/src/worlds/minecraft/engine-ipc.ts +98 -0
- package/src/worlds/minecraft/entity-facts.ts +132 -0
- package/src/worlds/minecraft/escape.ts +407 -0
- package/src/worlds/minecraft/executor.ts +14494 -0
- package/src/worlds/minecraft/explored.ts +156 -0
- package/src/worlds/minecraft/geometry.ts +205 -0
- package/src/worlds/minecraft/goal-plan.ts +647 -0
- package/src/worlds/minecraft/item-break.ts +126 -0
- package/src/worlds/minecraft/item-facts.ts +146 -0
- package/src/worlds/minecraft/item-pick.ts +67 -0
- package/src/worlds/minecraft/level-dat.ts +176 -0
- package/src/worlds/minecraft/log.ts +132 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
- package/src/worlds/minecraft/names.ts +377 -0
- package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
- package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
- package/src/worlds/minecraft/piglin.ts +49 -0
- package/src/worlds/minecraft/policy.ts +547 -0
- package/src/worlds/minecraft/precheck.ts +655 -0
- package/src/worlds/minecraft/proxy.ts +445 -0
- package/src/worlds/minecraft/ranged.ts +635 -0
- package/src/worlds/minecraft/readouts.ts +102 -0
- package/src/worlds/minecraft/round.ts +26 -0
- package/src/worlds/minecraft/search-observation.ts +92 -0
- package/src/worlds/minecraft/server-config.ts +467 -0
- package/src/worlds/minecraft/server.ts +658 -0
- package/src/worlds/minecraft/show.ts +84 -0
- package/src/worlds/minecraft/skills.ts +1763 -0
- package/src/worlds/minecraft/terrain.ts +1735 -0
- package/src/worlds/minecraft/window.ts +96 -0
- package/src/worlds/minecraft/works.ts +179 -0
- package/src/worlds/minecraft/world.ts +6438 -0
- package/src/worlds/qq/ENV_PROMPT.md +5 -0
- package/src/worlds/qq/config.ts +108 -0
- package/src/worlds/qq/console/client.ts +126 -0
- package/src/worlds/qq/console/events.ts +155 -0
- package/src/worlds/qq/console/gate.ts +232 -0
- package/src/worlds/qq/console/roster.ts +203 -0
- package/src/worlds/qq/console/style.css +65 -0
- package/src/worlds/qq/conversation.ts +28 -0
- package/src/worlds/qq/definition.ts +62 -0
- package/src/worlds/qq/driver.ts +375 -0
- package/src/worlds/qq/history-tools.ts +209 -0
- package/src/worlds/qq/normalize.ts +323 -0
- package/src/worlds/qq/qface-map.ts +282 -0
- package/src/worlds/qq/vision-prompt.ts +27 -0
- package/src/worlds/qq/vision.ts +642 -0
- package/src/worlds/qq/vlm.ts +142 -0
- package/src/worlds/qq/world.ts +1503 -0
- package/src/worlds/terminal/ENV_PROMPT.md +11 -0
- package/src/worlds/terminal/config.ts +9 -0
- package/src/worlds/terminal/definition.ts +14 -0
- package/src/worlds/terminal/world.ts +700 -0
- package/src/worlds/websearch/ENV_PROMPT.md +1 -0
- package/src/worlds/websearch/brave-client.ts +261 -0
- package/src/worlds/websearch/config.ts +75 -0
- package/src/worlds/websearch/definition.ts +20 -0
- package/src/worlds/websearch/world.ts +171 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Append-only usage ledger. Failed writes remain queued and are reported to the operator. */
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import type { UsageRecord } from './types.ts';
|
|
5
|
+
|
|
6
|
+
export class UsageLog {
|
|
7
|
+
private pending: UsageRecord[] = [];
|
|
8
|
+
private error: string | null = null;
|
|
9
|
+
private reconcile = false;
|
|
10
|
+
constructor(private readonly file: string, private readonly report: (message: string) => void = console.error) {}
|
|
11
|
+
|
|
12
|
+
append(rec: UsageRecord): void {
|
|
13
|
+
this.pending.push(structuredClone({ ...rec, version: 2, recordId: rec.recordId ?? rec.attempt?.id ?? crypto.randomUUID() }));
|
|
14
|
+
this.flush();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
flush(): void {
|
|
18
|
+
if (!this.pending.length) return;
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
21
|
+
if (this.reconcile) {
|
|
22
|
+
const ids = new Set(this.readDisk().map(record => record.recordId ?? record.attempt?.id));
|
|
23
|
+
this.pending = this.pending.filter(record => !ids.has(record.recordId));
|
|
24
|
+
this.reconcile = false;
|
|
25
|
+
}
|
|
26
|
+
while (this.pending.length) {
|
|
27
|
+
// A newline isolates a prior torn append; damaged lines remain available for inspection.
|
|
28
|
+
appendFileSync(this.file, '\n' + JSON.stringify(this.pending[0]) + '\n', 'utf8');
|
|
29
|
+
this.pending.shift();
|
|
30
|
+
}
|
|
31
|
+
this.error = null;
|
|
32
|
+
} catch (error) { this.reconcile = true; this.note(error); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
status(): { pending: number; error: string | null } { return { pending: this.pending.length, error: this.error }; }
|
|
36
|
+
|
|
37
|
+
private note(error: unknown): void {
|
|
38
|
+
const message = `Usage ledger ${this.file}: ${String(error)}`;
|
|
39
|
+
if (message !== this.error) this.report(message);
|
|
40
|
+
this.error = message;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private readDisk(): UsageRecord[] {
|
|
44
|
+
if (!existsSync(this.file)) return [];
|
|
45
|
+
const out: UsageRecord[] = [];
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
let damaged = 0;
|
|
48
|
+
for (const line of readFileSync(this.file, 'utf8').split(/\r?\n/)) {
|
|
49
|
+
if (!line.trim()) continue;
|
|
50
|
+
let record: UsageRecord;
|
|
51
|
+
try { record = JSON.parse(line) as UsageRecord; } catch { damaged++; continue; }
|
|
52
|
+
if (!record || record.version !== 2 || typeof record.ts !== 'string') { damaged++; continue; }
|
|
53
|
+
const id = record.recordId ?? record.attempt?.id;
|
|
54
|
+
if (id && seen.has(id)) continue;
|
|
55
|
+
if (id) seen.add(id);
|
|
56
|
+
out.push(record);
|
|
57
|
+
}
|
|
58
|
+
if (damaged) this.note(`${damaged} damaged ledger lines retained on disk`);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
readAll(): UsageRecord[] {
|
|
63
|
+
let records: UsageRecord[] = [];
|
|
64
|
+
try { records = this.readDisk(); } catch (error) { this.note(error); }
|
|
65
|
+
const ids = new Set(records.map(record => record.recordId ?? record.attempt?.id));
|
|
66
|
+
return [...records, ...this.pending.filter(record => !ids.has(record.recordId))];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
count(): number { return this.readAll().length; }
|
|
70
|
+
|
|
71
|
+
clear(): number {
|
|
72
|
+
const count = this.count();
|
|
73
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
74
|
+
writeFileSync(this.file, '', 'utf8');
|
|
75
|
+
this.pending = [];
|
|
76
|
+
this.error = null;
|
|
77
|
+
return count;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/core/util.ts
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { type ContextRecord } from '../protocol/open-responses/context.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 共享小工具:token估算、时间格式化、Logger实现。
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { appendFileSync, mkdirSync, existsSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { LOG_LEVEL_RANK, type EventEnvelope, type LogEmitOptions, type LogError, type LogInput, type Logger, type LogLevel, type LogRecord } from './types.ts';
|
|
9
|
+
import { currentAnchors } from './log-context.ts';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export { estimateTokens, estimateMessagesTokens } from '../protocol/open-responses/tokens.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 前 count 条消息按请求形状序列化后计算 SHA256,取前 12 位用于日志比较。
|
|
16
|
+
* 该指纹不参与运行控制,也不证明上游缓存是否命中。
|
|
17
|
+
*/
|
|
18
|
+
export function prefixFingerprint(messages: readonly ContextRecord[], count = PREFIX_FINGERPRINT_MESSAGES): string {
|
|
19
|
+
const parts = messages.slice(0, count).map(({ item }) => {
|
|
20
|
+
const { id: _id, ...wire } = item;
|
|
21
|
+
return JSON.stringify(wire);
|
|
22
|
+
});
|
|
23
|
+
return createHash('sha256').update(parts.join('\0'), 'utf8').digest('hex').slice(0, 12);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 指纹覆盖的消息条数,包含 system 前缀、合成首轮对话及其后的部分上下文。 */
|
|
27
|
+
export const PREFIX_FINGERPRINT_MESSAGES = 8;
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/** 超时后拒绝返回的 Promise;不会取消仍在运行的底层 Promise。 */
|
|
31
|
+
export function withDeadline<T>(work: Promise<T>, ms: number, what = '这一步'): Promise<T> {
|
|
32
|
+
return new Promise<T>((resolve, reject) => {
|
|
33
|
+
const timer = setTimeout(() => reject(new Error(`${what}超时(${Math.round(ms / 1000)}秒)`)), ms);
|
|
34
|
+
work.then(
|
|
35
|
+
(v) => { clearTimeout(timer); resolve(v); },
|
|
36
|
+
(e) => { clearTimeout(timer); reject(e as Error); },
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 当前时刻ISO 8601(带时区偏移,毫秒),按配置时区渲染 */
|
|
42
|
+
export function nowIso(timezone: string, d: Date = new Date()): string {
|
|
43
|
+
const fmt = new Intl.DateTimeFormat('sv-SE', {
|
|
44
|
+
timeZone: timezone,
|
|
45
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
46
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
47
|
+
hour12: false,
|
|
48
|
+
});
|
|
49
|
+
const parts = `${fmt.format(d).replace(' ', 'T')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
50
|
+
const offMin = -getTimezoneOffsetMinutes(timezone, d);
|
|
51
|
+
const sign = offMin >= 0 ? '+' : '-';
|
|
52
|
+
const abs = Math.abs(offMin);
|
|
53
|
+
const hh = String(Math.floor(abs / 60)).padStart(2, '0');
|
|
54
|
+
const mm = String(abs % 60).padStart(2, '0');
|
|
55
|
+
return `${parts}${sign}${hh}:${mm}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 目标时区相对UTC的偏移(分钟, UTC-本地=负东区),内部用 */
|
|
59
|
+
function getTimezoneOffsetMinutes(timezone: string, d: Date): number {
|
|
60
|
+
const utc = new Date(d.toLocaleString('en-US', { timeZone: 'UTC' }));
|
|
61
|
+
const loc = new Date(d.toLocaleString('en-US', { timeZone: timezone }));
|
|
62
|
+
return (utc.getTime() - loc.getTime()) / 60000;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 渲染"[HH:MM]"短时间(消息行用) */
|
|
66
|
+
export function shortTime(timezone: string, d: Date = new Date()): string {
|
|
67
|
+
return new Intl.DateTimeFormat('zh-CN', {
|
|
68
|
+
timeZone: timezone, hour: '2-digit', minute: '2-digit', hour12: false,
|
|
69
|
+
}).format(d);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function hourIn(timezone: string, d: Date = new Date()): number {
|
|
73
|
+
return parseInt(new Intl.DateTimeFormat('en-US', {
|
|
74
|
+
timeZone: timezone, hour: 'numeric', hour12: false,
|
|
75
|
+
}).format(d), 10) % 24;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 返回 World 提供的事件正文,不添加存储游标或语义文本。 */
|
|
79
|
+
export function renderEventLines(events: EventEnvelope[]): string {
|
|
80
|
+
return events.map((e) => e.text).join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 折叠窗口:同一 (area, event|msg 模板) 的 warn/error 在窗口内只落第一条,窗口末补一条计数。 */
|
|
84
|
+
const FOLD_MS = 30_000;
|
|
85
|
+
/** 同一 key 的错误上下文快照写入间隔 */
|
|
86
|
+
const INCIDENT_GAP_MS = 60_000;
|
|
87
|
+
/** 错误上下文快照附带的最近记录条数 */
|
|
88
|
+
const RING_SIZE = 300;
|
|
89
|
+
/** 单文件大小上限;轮转文件数在当前 run 内不设上限。 */
|
|
90
|
+
const ROTATE_BYTES = 64 * 1024 * 1024;
|
|
91
|
+
|
|
92
|
+
export interface RunlogLevels {
|
|
93
|
+
file: LogLevel;
|
|
94
|
+
console: LogLevel;
|
|
95
|
+
areas: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface RunlogOptions {
|
|
99
|
+
run?: string;
|
|
100
|
+
timezone?: string;
|
|
101
|
+
/** 现读:配置热改立即生效 */
|
|
102
|
+
levels?: () => RunlogLevels;
|
|
103
|
+
/** error 记录的上下文快照目录;未提供时不写快照。 */
|
|
104
|
+
incidentsDir?: string | null;
|
|
105
|
+
/** 是否同时打到 stdout(测试关掉) */
|
|
106
|
+
console?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const DEFAULT_LEVELS: RunlogLevels = { file: 'debug', console: 'info', areas: '' };
|
|
110
|
+
|
|
111
|
+
export function normalizeError(value: unknown): LogError {
|
|
112
|
+
if (value instanceof Error) {
|
|
113
|
+
return { name: value.name, message: value.message, ...(value.stack ? { stack: value.stack } : {}) };
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === 'string') return { name: 'Error', message: value };
|
|
116
|
+
if (value && typeof value === 'object') {
|
|
117
|
+
const v = value as { name?: unknown; message?: unknown; stack?: unknown };
|
|
118
|
+
if (typeof v.message === 'string') {
|
|
119
|
+
return {
|
|
120
|
+
name: typeof v.name === 'string' ? v.name : 'Error',
|
|
121
|
+
message: v.message,
|
|
122
|
+
...(typeof v.stack === 'string' ? { stack: v.stack } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { name: 'Error', message: String(value) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 解析 `a.b=level,c.*=level`;条目按前缀长度降序,最长前缀命中优先 */
|
|
130
|
+
function parseAreaLevels(spec: string): Array<{ prefix: string; level: LogLevel }> {
|
|
131
|
+
const out: Array<{ prefix: string; level: LogLevel }> = [];
|
|
132
|
+
for (const part of spec.split(',')) {
|
|
133
|
+
const [rawArea, rawLevel] = part.split('=').map((s) => s.trim());
|
|
134
|
+
if (!rawArea || !rawLevel) continue;
|
|
135
|
+
if (!(rawLevel in LOG_LEVEL_RANK)) continue;
|
|
136
|
+
const prefix = rawArea.endsWith('.*') ? rawArea.slice(0, -2) : rawArea.endsWith('*') ? rawArea.slice(0, -1) : rawArea;
|
|
137
|
+
out.push({ prefix: prefix.replace(/\.$/, ''), level: rawLevel as LogLevel });
|
|
138
|
+
}
|
|
139
|
+
return out.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function foldKey(input: { area: string; event?: string; msg: string }): string {
|
|
143
|
+
return `${input.area}|${input.event ?? input.msg.replace(/\d+(\.\d+)?/g, '#').slice(0, 80)}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function slug(text: string): string {
|
|
147
|
+
return text.replace(/[^\p{L}\p{N}]+/gu, '-').replace(/^-|-$/g, '').slice(0, 40) || 'error';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const ANCHOR_KEYS = ['sess', 'round', 'resp', 'call', 'ev', 'task'] as const;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 日志写入 data/runs/<run>/log.jsonl,并按独立门槛输出到 stdout。
|
|
154
|
+
* 写入时读取异步作用域的关联字段;重复 warn/error 合并计数,error 可另存诊断记录。
|
|
155
|
+
*/
|
|
156
|
+
export class Runlog {
|
|
157
|
+
private readonly file: string | null;
|
|
158
|
+
private readonly run: string;
|
|
159
|
+
private readonly timezone: string;
|
|
160
|
+
private readonly levels: () => RunlogLevels;
|
|
161
|
+
private readonly incidentsDir: string | null;
|
|
162
|
+
private readonly echo: boolean;
|
|
163
|
+
private seq = 0;
|
|
164
|
+
private bytes = 0;
|
|
165
|
+
private dirReady = false;
|
|
166
|
+
private areaSpec = '';
|
|
167
|
+
private areaLevels: Array<{ prefix: string; level: LogLevel }> = [];
|
|
168
|
+
private readonly ring: LogRecord[] = [];
|
|
169
|
+
private readonly folds = new Map<string, { count: number; record: LogRecord; timer: NodeJS.Timeout }>();
|
|
170
|
+
private readonly incidentAt = new Map<string, number>();
|
|
171
|
+
private writeListeners: Array<(entry: LogRecord) => void> = [];
|
|
172
|
+
|
|
173
|
+
constructor(file: string | null, opts: RunlogOptions = {}) {
|
|
174
|
+
this.file = file;
|
|
175
|
+
this.run = opts.run ?? 'r-none';
|
|
176
|
+
this.timezone = opts.timezone ?? 'Asia/Shanghai';
|
|
177
|
+
this.levels = opts.levels ?? (() => DEFAULT_LEVELS);
|
|
178
|
+
this.incidentsDir = opts.incidentsDir ?? null;
|
|
179
|
+
this.echo = opts.console ?? true;
|
|
180
|
+
if (file) {
|
|
181
|
+
const dir = dirname(file);
|
|
182
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
183
|
+
this.dirReady = true;
|
|
184
|
+
try { this.bytes = existsSync(file) ? statSync(file).size : 0; } catch { this.bytes = 0; }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get path(): string | null { return this.file; }
|
|
189
|
+
get runId(): string { return this.run; }
|
|
190
|
+
|
|
191
|
+
/** 日志追加订阅,供控制台实时观察。 */
|
|
192
|
+
onWrite(cb: (entry: LogRecord) => void): void {
|
|
193
|
+
this.writeListeners.push(cb);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 内存中最近的日志记录,包含未写入文件的记录。 */
|
|
197
|
+
recent(limit = RING_SIZE): LogRecord[] {
|
|
198
|
+
return this.ring.slice(-limit);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private fileThreshold(area: string): LogLevel {
|
|
202
|
+
const levels = this.levels();
|
|
203
|
+
if (levels.areas !== this.areaSpec) {
|
|
204
|
+
this.areaSpec = levels.areas;
|
|
205
|
+
this.areaLevels = parseAreaLevels(levels.areas);
|
|
206
|
+
}
|
|
207
|
+
for (const entry of this.areaLevels) {
|
|
208
|
+
if (area === entry.prefix || area.startsWith(`${entry.prefix}.`)) return entry.level;
|
|
209
|
+
}
|
|
210
|
+
return levels.file;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
write(input: LogInput): LogRecord | null {
|
|
214
|
+
const rank = LOG_LEVEL_RANK[input.level];
|
|
215
|
+
const toFile = rank >= LOG_LEVEL_RANK[this.fileThreshold(input.area)];
|
|
216
|
+
const toConsole = this.echo && rank >= LOG_LEVEL_RANK[this.levels().console];
|
|
217
|
+
if (!toFile && !toConsole) return null;
|
|
218
|
+
const record = this.build(input);
|
|
219
|
+
if (rank >= LOG_LEVEL_RANK.warn && this.fold(record)) return null;
|
|
220
|
+
this.commit(record, toFile, toConsole);
|
|
221
|
+
if (record.level === 'error') this.incident(record);
|
|
222
|
+
return record;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private build(input: LogInput): LogRecord {
|
|
226
|
+
const anchors = currentAnchors();
|
|
227
|
+
const record: LogRecord = {
|
|
228
|
+
ts: input.ts ?? nowIso(this.timezone),
|
|
229
|
+
run: this.run,
|
|
230
|
+
seq: ++this.seq,
|
|
231
|
+
level: input.level,
|
|
232
|
+
area: input.area,
|
|
233
|
+
msg: input.msg,
|
|
234
|
+
};
|
|
235
|
+
if (input.event !== undefined) record.event = input.event;
|
|
236
|
+
for (const key of ANCHOR_KEYS) {
|
|
237
|
+
const value = input[key] ?? anchors[key];
|
|
238
|
+
if (value !== undefined) (record as unknown as Record<string, unknown>)[key] = value;
|
|
239
|
+
}
|
|
240
|
+
if (input.durMs !== undefined) record.durMs = input.durMs;
|
|
241
|
+
let data = input.data;
|
|
242
|
+
let err = input.err;
|
|
243
|
+
if (data instanceof Error) { err ??= data; data = undefined; }
|
|
244
|
+
else if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
245
|
+
const bag = data as Record<string, unknown>;
|
|
246
|
+
const carried = bag.err instanceof Error ? bag.err : bag.error instanceof Error ? bag.error : undefined;
|
|
247
|
+
if (carried) {
|
|
248
|
+
err ??= carried;
|
|
249
|
+
const { err: _e, error: _error, ...rest } = bag;
|
|
250
|
+
data = Object.keys(rest).length ? rest : undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (data !== undefined) record.data = data;
|
|
254
|
+
if (err !== undefined) record.err = normalizeError(err);
|
|
255
|
+
return record;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 窗口内重复的 warn/error 只累计计数;返回 true 表示本条无需另行写入。 */
|
|
259
|
+
private fold(record: LogRecord): boolean {
|
|
260
|
+
const key = foldKey(record);
|
|
261
|
+
const open = this.folds.get(key);
|
|
262
|
+
if (open) { open.count++; return true; }
|
|
263
|
+
const timer = setTimeout(() => {
|
|
264
|
+
const entry = this.folds.get(key);
|
|
265
|
+
this.folds.delete(key);
|
|
266
|
+
if (!entry || entry.count === 0) return;
|
|
267
|
+
const summary: LogRecord = { ...entry.record, ts: nowIso(this.timezone), seq: ++this.seq, repeat: entry.count };
|
|
268
|
+
this.commit(summary, true, this.echo);
|
|
269
|
+
}, FOLD_MS);
|
|
270
|
+
timer.unref?.();
|
|
271
|
+
this.folds.set(key, { count: 0, record, timer });
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private commit(record: LogRecord, toFile: boolean, toConsole: boolean): void {
|
|
276
|
+
this.ring.push(record);
|
|
277
|
+
if (this.ring.length > RING_SIZE) this.ring.shift();
|
|
278
|
+
if (toFile && this.file) {
|
|
279
|
+
try {
|
|
280
|
+
if (!this.dirReady) { mkdirSync(dirname(this.file), { recursive: true }); this.dirReady = true; }
|
|
281
|
+
if (this.bytes > ROTATE_BYTES) this.rotate();
|
|
282
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
283
|
+
appendFileSync(this.file, line, 'utf8');
|
|
284
|
+
this.bytes += Buffer.byteLength(line, 'utf8');
|
|
285
|
+
} catch {
|
|
286
|
+
// 日志写不进去不影响运行
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const cb of this.writeListeners) {
|
|
290
|
+
try { cb(record); } catch { /* 观察者异常不影响日志 */ }
|
|
291
|
+
}
|
|
292
|
+
if (!toConsole) return;
|
|
293
|
+
const clock = record.ts.slice(11, 23);
|
|
294
|
+
const tag = record.event ? `${record.area}/${record.event}` : record.area;
|
|
295
|
+
const line = `[${clock}] ${record.level.toUpperCase().padEnd(5)} ${tag}: ${record.msg}${record.repeat ? ` (×${record.repeat + 1})` : ''}`;
|
|
296
|
+
if (record.level === 'error') console.error(line, record.err?.stack ?? record.err?.message ?? '', record.data ?? '');
|
|
297
|
+
else if (record.level === 'warn') console.warn(line, record.data ?? '');
|
|
298
|
+
else console.log(line);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** log.jsonl → log.1.jsonl,已有的代数各退一位 */
|
|
302
|
+
private rotate(): void {
|
|
303
|
+
if (!this.file) return;
|
|
304
|
+
const base = this.file.replace(/\.jsonl$/, '');
|
|
305
|
+
let n = 1;
|
|
306
|
+
while (existsSync(`${base}.${n}.jsonl`)) n++;
|
|
307
|
+
for (let i = n - 1; i >= 1; i--) renameSync(`${base}.${i}.jsonl`, `${base}.${i + 1}.jsonl`);
|
|
308
|
+
renameSync(this.file, `${base}.1.jsonl`);
|
|
309
|
+
this.bytes = 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private incident(record: LogRecord): void {
|
|
313
|
+
if (!this.incidentsDir) return;
|
|
314
|
+
const key = foldKey(record);
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
const last = this.incidentAt.get(key) ?? 0;
|
|
317
|
+
if (now - last < INCIDENT_GAP_MS) return;
|
|
318
|
+
this.incidentAt.set(key, now);
|
|
319
|
+
try {
|
|
320
|
+
mkdirSync(this.incidentsDir, { recursive: true });
|
|
321
|
+
const name = `${record.ts.slice(0, 23).replace(/[:.]/g, '-')}-${slug(record.event ?? record.msg)}.json`;
|
|
322
|
+
writeFileSync(join(this.incidentsDir, name), JSON.stringify({ record, anchors: currentAnchors(), recent: this.ring.slice(0, -1) }, null, 1), 'utf8');
|
|
323
|
+
} catch {
|
|
324
|
+
// 快照写入失败不影响运行
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
logger(area: string): Logger {
|
|
329
|
+
return makeLogger(this, area);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 清空当前 run 的日志文件(web运维动作);之后照常append */
|
|
333
|
+
clear(): void {
|
|
334
|
+
if (!this.file) return;
|
|
335
|
+
try {
|
|
336
|
+
writeFileSync(this.file, '', 'utf8');
|
|
337
|
+
this.bytes = 0;
|
|
338
|
+
} catch {
|
|
339
|
+
// 清空失败不致命
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function makeLogger(runlog: Runlog, area: string): Logger {
|
|
345
|
+
const emit = (level: LogLevel, msg: string, opts?: LogEmitOptions): void => {
|
|
346
|
+
runlog.write({ level, area, msg, ...opts });
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
trace: (m, d) => emit('trace', m, { data: d }),
|
|
350
|
+
debug: (m, d) => emit('debug', m, { data: d }),
|
|
351
|
+
info: (m, d) => emit('info', m, { data: d }),
|
|
352
|
+
warn: (m, d) => emit('warn', m, { data: d }),
|
|
353
|
+
error: (m, d) => emit('error', m, { data: d }),
|
|
354
|
+
emit,
|
|
355
|
+
child: (sub) => makeLogger(runlog, `${area}.${sub}`),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** 测试用:静默logger */
|
|
360
|
+
export function nullLogger(): Logger {
|
|
361
|
+
const l: Logger = {
|
|
362
|
+
trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {},
|
|
363
|
+
emit: () => {},
|
|
364
|
+
child: () => l,
|
|
365
|
+
};
|
|
366
|
+
return l;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 生成短随机id(tool_call补记、fork id等) */
|
|
370
|
+
export function shortId(prefix = ''): string {
|
|
371
|
+
return prefix + Math.random().toString(36).slice(2, 10);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 按 BOM 读取 UTF-8 或 UTF-16 LE 文本,返回值不含 BOM;无 BOM 时使用 UTF-8。
|
|
376
|
+
*/
|
|
377
|
+
export function readTextFile(file: string): string {
|
|
378
|
+
const bytes = readFileSync(file);
|
|
379
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
380
|
+
return bytes.subarray(2).toString('utf16le');
|
|
381
|
+
}
|
|
382
|
+
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
383
|
+
return bytes.subarray(3).toString('utf8');
|
|
384
|
+
}
|
|
385
|
+
return bytes.toString('utf8');
|
|
386
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 启动器菜单的数据:每份部署的代码包、展示名与配色,以及可选的 bot 代码包清单。
|
|
3
|
+
* 颜色在这里解析成 `#rrggbb`,启动器只负责画;配色取部署的 theme.json,没有则取 web.theme。
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { basename, resolve } from 'node:path';
|
|
7
|
+
import { normalizeStoredTheme, resolvePalette, type ThemeAppearance } from './web/shared/theme.ts';
|
|
8
|
+
import { THEME_FILE } from './web/theme-store.ts';
|
|
9
|
+
|
|
10
|
+
/** 菜单一行用得到的几个颜色。 */
|
|
11
|
+
export interface RowColors {
|
|
12
|
+
/** bot id */
|
|
13
|
+
accent: string;
|
|
14
|
+
/** 展示名 */
|
|
15
|
+
accent2: string;
|
|
16
|
+
/** 选中行的路径 */
|
|
17
|
+
ink: string;
|
|
18
|
+
/** 未选中行的路径 */
|
|
19
|
+
inkDim: string;
|
|
20
|
+
/** 读不出代码包时的那行原因 */
|
|
21
|
+
danger: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DeploymentRow {
|
|
25
|
+
name: string;
|
|
26
|
+
/** 相对部署根的路径,菜单里就显示它。 */
|
|
27
|
+
dir: string;
|
|
28
|
+
/** deployment.json 引用的代码包。 */
|
|
29
|
+
bot: string;
|
|
30
|
+
displayName: string;
|
|
31
|
+
colors: RowColors;
|
|
32
|
+
/** 代码包读不出来时的原因;这一行仍然列出,选中它启动会得到同样的错。 */
|
|
33
|
+
problem?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BotPackageRow {
|
|
37
|
+
id: string;
|
|
38
|
+
source: 'tree' | 'extension';
|
|
39
|
+
displayName: string;
|
|
40
|
+
colors: RowColors;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LauncherListing {
|
|
44
|
+
/** 部署根的绝对路径,菜单顶上打一行。 */
|
|
45
|
+
root: string;
|
|
46
|
+
deployments: DeploymentRow[];
|
|
47
|
+
packages: BotPackageRow[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 一个 bot 代码包的默认值里,菜单要用的那两项。 */
|
|
51
|
+
export interface BotDefaults {
|
|
52
|
+
displayName: string;
|
|
53
|
+
/** 代码包选的配色方案 id。 */
|
|
54
|
+
scheme: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ListingSource {
|
|
58
|
+
root: string;
|
|
59
|
+
deployments: readonly string[];
|
|
60
|
+
packages: readonly { id: string; source: 'tree' | 'extension' }[];
|
|
61
|
+
/** 读代码包的默认值;包不在或读不出就抛,原因进那一行。 */
|
|
62
|
+
defaultsOf(bot: string): Promise<BotDefaults>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 终端按深色底算;部署把明暗钉在浅色时才取浅色那份。 */
|
|
66
|
+
function appearanceOf(mode: string): ThemeAppearance {
|
|
67
|
+
return mode === 'light' ? 'light' : 'dark';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readJson(file: string): Record<string, unknown> | null {
|
|
71
|
+
if (!existsSync(file)) return null;
|
|
72
|
+
try {
|
|
73
|
+
const value = JSON.parse(readFileSync(file, 'utf8')) as unknown;
|
|
74
|
+
return value && typeof value === 'object' ? (value as Record<string, unknown>) : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 颜色:控制台存过的那套赢过代码包与部署配置里的默认方案 id;认不出的方案 id 落到框架
|
|
82
|
+
* 默认方案。dir 为 null = 还没有部署目录(新建流程里的代码包),只看方案 id。
|
|
83
|
+
*/
|
|
84
|
+
export function colorsOf(dir: string | null, defaultScheme: string): RowColors {
|
|
85
|
+
const stored = dir === null ? null : readJson(resolve(dir, THEME_FILE));
|
|
86
|
+
const state = stored
|
|
87
|
+
? normalizeStoredTheme(stored)
|
|
88
|
+
: normalizeStoredTheme({ selectedId: defaultScheme, mode: 'system', custom: [] });
|
|
89
|
+
const palette = resolvePalette(state, appearanceOf(state.mode));
|
|
90
|
+
return {
|
|
91
|
+
accent: palette.accent,
|
|
92
|
+
accent2: palette['accent-2'],
|
|
93
|
+
ink: palette.ink,
|
|
94
|
+
inkDim: palette['ink-dim'],
|
|
95
|
+
danger: palette.danger,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 部署配置里的 displayName 与 web.theme;文件不在或读不出就当没写。 */
|
|
100
|
+
function deploymentConfig(dir: string): { displayName?: string; scheme?: string } {
|
|
101
|
+
const raw = readJson(resolve(dir, 'config.json'));
|
|
102
|
+
const web = raw?.web as { theme?: unknown } | undefined;
|
|
103
|
+
return {
|
|
104
|
+
...(typeof raw?.displayName === 'string' ? { displayName: raw.displayName } : {}),
|
|
105
|
+
...(typeof web?.theme === 'string' ? { scheme: web.theme } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function buildListing(source: ListingSource): Promise<LauncherListing> {
|
|
110
|
+
const cache = new Map<string, Promise<BotDefaults>>();
|
|
111
|
+
const defaultsOf = (bot: string): Promise<BotDefaults> => {
|
|
112
|
+
const hit = cache.get(bot);
|
|
113
|
+
if (hit) return hit;
|
|
114
|
+
const pending = source.defaultsOf(bot);
|
|
115
|
+
cache.set(bot, pending);
|
|
116
|
+
return pending;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const deployments: DeploymentRow[] = [];
|
|
120
|
+
for (const name of source.deployments) {
|
|
121
|
+
const dir = resolve(source.root, name);
|
|
122
|
+
const manifest = readJson(resolve(dir, 'deployment.json'));
|
|
123
|
+
const bot = typeof manifest?.bot === 'string' ? manifest.bot : '';
|
|
124
|
+
const config = deploymentConfig(dir);
|
|
125
|
+
let defaults: BotDefaults | null = null;
|
|
126
|
+
let problem: string | undefined;
|
|
127
|
+
try {
|
|
128
|
+
defaults = await defaultsOf(bot);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
problem = err instanceof Error ? err.message : String(err);
|
|
131
|
+
}
|
|
132
|
+
deployments.push({
|
|
133
|
+
name,
|
|
134
|
+
dir: `${basename(source.root)}/${name}`,
|
|
135
|
+
bot,
|
|
136
|
+
displayName: config.displayName ?? defaults?.displayName ?? '',
|
|
137
|
+
colors: colorsOf(dir, config.scheme ?? defaults?.scheme ?? ''),
|
|
138
|
+
...(problem ? { problem } : {}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const packages: BotPackageRow[] = [];
|
|
143
|
+
for (const pkg of source.packages) {
|
|
144
|
+
let defaults: BotDefaults;
|
|
145
|
+
try {
|
|
146
|
+
defaults = await defaultsOf(pkg.id);
|
|
147
|
+
} catch {
|
|
148
|
+
// 装不上的包不能拿来建部署。
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
packages.push({
|
|
152
|
+
id: pkg.id,
|
|
153
|
+
source: pkg.source,
|
|
154
|
+
displayName: defaults.displayName,
|
|
155
|
+
colors: colorsOf(null, defaults.scheme),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { root: source.root, deployments, packages };
|
|
160
|
+
}
|