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,41 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { readTextFile } from './core/util.ts';
|
|
4
|
+
|
|
5
|
+
export type JsonObject = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
export function readJsonObject(file: string): JsonObject {
|
|
8
|
+
if (!existsSync(file)) return {};
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(readTextFile(file)) as unknown;
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
12
|
+
throw new Error('root must be an object');
|
|
13
|
+
}
|
|
14
|
+
return value as JsonObject;
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error('config.json解析失败,拒绝写回(请手工检查文件)');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function updateJsonObject(file: string, update: (root: JsonObject) => void): void {
|
|
21
|
+
const root = readJsonObject(file);
|
|
22
|
+
update(root);
|
|
23
|
+
|
|
24
|
+
const suffix = `${process.pid}-${randomUUID()}`;
|
|
25
|
+
const tmp = `${file}.tmp-${suffix}`;
|
|
26
|
+
const backup = `${file}.bak-${suffix}`;
|
|
27
|
+
writeFileSync(tmp, `${JSON.stringify(root, null, 2)}\n`, 'utf8');
|
|
28
|
+
if (!existsSync(file)) {
|
|
29
|
+
renameSync(tmp, file);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
renameSync(file, backup);
|
|
34
|
+
try {
|
|
35
|
+
renameSync(tmp, file);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
renameSync(backup, file);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
rmSync(backup);
|
|
41
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Owner: `src/core/core.ts`, `src/core/types.ts`, `src/core/loop.ts`, `src/core/bus.ts`
|
|
2
|
+
|
|
3
|
+
# src/core
|
|
4
|
+
|
|
5
|
+
Core 管理 session、事件流与模型调用的生命周期,包括事件投递、调度、上下文交接、工具执行、
|
|
6
|
+
容量限制、错误隔离和 Provider。Persona 定义上下文语义;Core 使用的事件封装、错误和截断记号
|
|
7
|
+
在 `markers.ts` 中定义。
|
|
8
|
+
|
|
9
|
+
## 文件
|
|
10
|
+
|
|
11
|
+
| 文件 | 职责 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `types.ts` | 全部契约:`Persona`、`World`、`WorldHost`、`CoreApi`、事件、工具、日志、配置类型 |
|
|
14
|
+
| `core.ts` | Core 装配,`Persona` / `CoreApi` 的边界 |
|
|
15
|
+
| `loop.ts` | 主 session 的模型调用循环、事件投递、上下文交接执行 |
|
|
16
|
+
| `bus.ts` | `WakeBus`:合批事件总线、四种触发模式、FIFO 顺序、暂停与投递闸门 |
|
|
17
|
+
| `fork.ts` | 临时 session 的工具循环 |
|
|
18
|
+
| `session.ts`、`sessions.ts` | 常驻 session 的追加式上下文;各 session 的状态与用量记录 |
|
|
19
|
+
| `event-store.ts` | 按 run 分片的 JSONL 事件库,cursor 跨 run 全局单调 |
|
|
20
|
+
| `state.ts`、`run.ts`、`timers.ts` | `core-state.json`;run 目录;通用持久定时器 |
|
|
21
|
+
| `transcript.ts`、`tool-log.ts`、`usage-log.ts` | 主 session 副本、工具调用记录、用量记录 |
|
|
22
|
+
| `cost.ts`、`billing.ts`、`generation.ts` | 消耗汇总、计费余额、`ResponseClient` 与计量口径 |
|
|
23
|
+
| `prefix.ts`、`template.ts` | system 前缀装配、环境提示词三层覆盖、模板渲染 |
|
|
24
|
+
| `truncate.ts`、`markers.ts`、`blobs.ts` | 上下文压缩、结构记号、附件句柄(`log:` / `mem:`) |
|
|
25
|
+
| `config.ts`、`config-schema.ts` | `CORE_DEFAULTS`、深合并、配置项声明 |
|
|
26
|
+
| `instance-lock.ts`、`language.ts`、`secrets.ts` | 单实例锁、控制台语言、按名取密钥 |
|
|
27
|
+
| `log-context.ts`、`ipc-logger.ts`、`util.ts` | 日志关联字段、子进程日志回传、Logger 与 token 估算 |
|
|
28
|
+
|
|
29
|
+
## Core 类
|
|
30
|
+
|
|
31
|
+
构造时创建事件库、session、状态、定时器和日志,先调用 `persona.attach(core)`,再调用
|
|
32
|
+
`persona.declareSessions()`。声明中必须恰有一个同时设置 `receivesEvents` 和 `persistent`,否则抛错。
|
|
33
|
+
公开成员:`store`、`bus`、`session`、`state`、`timers`、`loop`、`llm`、`sessions`、`providers`、
|
|
34
|
+
`sessionDecls`;`spawnFork`、`resolveBlob` / `internBlobs`、`setWorldVisible`、`activeSpec` /
|
|
35
|
+
`activeProviderEntry`、`mountWorld` / `unmountWorld`、`start` / `stop`。
|
|
36
|
+
|
|
37
|
+
Persona 通过 `CoreApi` 访问:`injectInternal` / `injectDeferred` / `injectExternal`、
|
|
38
|
+
`requestContextHandoff`、`spawnFork`、`sessionInfo`、`llm`、`timers`、`deliveryGate`、
|
|
39
|
+
`personaState` / `savePersonaState`、`toolsTagged`、`blob`、`log`。
|
|
40
|
+
|
|
41
|
+
## 总线与唤醒
|
|
42
|
+
|
|
43
|
+
`WakeBus` 提供 `preempt`、`flush`、`debounce`、`piggyback` 四种触发模式。
|
|
44
|
+
debounce 的计划投递时刻为 `min(首件时刻 + maxBatchAgeMs, max(首件时刻 + minBatchAgeMs,
|
|
45
|
+
末件时刻 + quietGapMs))`;计数达到 `maxBatchSize` 时立即投递。
|
|
46
|
+
计数包含外部即时事件与候选,不包含内部事件、延迟渲染项或 piggyback 项。
|
|
47
|
+
piggyback 只入队,随后续唤醒一起投递。
|
|
48
|
+
|
|
49
|
+
操作者的 `paused` 和 Persona 的 `DeliveryGate` 控制投递;操作者暂停的优先级更高。
|
|
50
|
+
`nextBatch()` 仅支持一个消费者,每次按 FIFO 顺序取走整批。`batching` 使用共享配置引用,
|
|
51
|
+
更新后的值在下一次入队时参与计算。
|
|
52
|
+
|
|
53
|
+
投递水位 `lastDeliveredCursor` 持久化,队列不持久化。重启时补投水位之后的外部事件;
|
|
54
|
+
已被候选处理结果引用的原始归档不重复投递。内部事件仅在当次运行投递。
|
|
55
|
+
|
|
56
|
+
## 主循环
|
|
57
|
+
|
|
58
|
+
每次唤醒处理一批事件,可进行多轮模型调用。`SessionDecl.rounds()` 提供
|
|
59
|
+
`{ soft, hard, softHint? }`:到 soft 轮时将 `softHint` 追加到最后一条工具回执;到 hard 轮时
|
|
60
|
+
记录 warn 并结束本批。`endsTurn` 工具和自然结束共用结束处理;本批结束时尚未处理的事件
|
|
61
|
+
退回总线,进入下一批。
|
|
62
|
+
|
|
63
|
+
状态 0、429 或 5xx 的模型调用失败,可保留已记录的输出和工具回执,按 `ResubmitPolicy` 重试。
|
|
64
|
+
默认允许连续重试 2 次、每批最多 4 次,退避为 2 秒、10 秒;上下文超限、抢占、关机或轮数
|
|
65
|
+
达到硬上限时不重试。
|
|
66
|
+
|
|
67
|
+
参数不是合法 JSON 时返回 `TOOL_FAILED_BAD_ARGS`,不执行工具;未知工具返回 `UNKNOWN_TOOL`;
|
|
68
|
+
handler 异常转为失败回执。流式生成时 `EagerDispatch` 可提前执行完整的工具调用,遵守
|
|
69
|
+
`barrierAfter` 顺序,并按 call id 配对结果。回执超过 8000 字符时记录 warn。
|
|
70
|
+
|
|
71
|
+
## 上下文
|
|
72
|
+
|
|
73
|
+
`hardTokens = max(0, contextWindowOf(spec) − (spec.maxTokens ?? 0))`。
|
|
74
|
+
`contextWindowOf` 取 provider 探测值与手动 `contextWindow` 的较小者;两者均未知时不按窗口裁剪。
|
|
75
|
+
超过上限时,主循环在轮次边界结束本批,并在批末强制交接;上游报告输入超限时也请求交接。
|
|
76
|
+
|
|
77
|
+
`Persona.onHandoff(snapshot, { hardTokens })` 返回 `{ tail, trim? }`。Core 重建 system 前缀,
|
|
78
|
+
按 `hardTokens − estimate(prefix)` 限制保留上下文,其中 prefix 包括 system 前缀和 Persona 的合成
|
|
79
|
+
开头(`sessionHead()`)。Core 校验工具调用配对,并重置 session。
|
|
80
|
+
合成开头每次请求现取,不写入持久 session;Core 不据此判定远端缓存是否命中。
|
|
81
|
+
阶段预算与保留比例由 Persona 决定,模型配置来自当前 provider。
|
|
82
|
+
|
|
83
|
+
存储清单使用 `StorageOwner` 标识 Core、Persona、Memory 与 World 的归属。
|
|
84
|
+
页面组织和 Memory 展示名称由装配层处理。
|
|
85
|
+
|
|
86
|
+
## 错误隔离
|
|
87
|
+
|
|
88
|
+
装配层将 World 构造失败限制在该 World。`start()` 抛错时不挂载;`stop()` 失败或超时会记录
|
|
89
|
+
结果并继续卸载。`onOpening`、`onDelivery`、`onBatchEnd`、`onTurnEnded`、`onIdle` 异常记录
|
|
90
|
+
warn;`onHandoff` 异常记录 error 并使用默认交接策略。其他钩子的异常由调用方处理。
|
|
91
|
+
读取 `console()` 失败时省略该 World 的环境段,继续构建前缀。主循环异常退出时记录 error
|
|
92
|
+
并停止定时器。启动器处理未捕获的进程异常并执行关机流程。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Charge } from './generation.ts';
|
|
2
|
+
import type { UsageRecord } from './types.ts';
|
|
3
|
+
|
|
4
|
+
export interface BillingBalance {
|
|
5
|
+
currency: string;
|
|
6
|
+
basis: 'marginal' | 'equivalent';
|
|
7
|
+
knownAmount: number;
|
|
8
|
+
pricedCalls: number;
|
|
9
|
+
partialCalls: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function recordCharges(record: UsageRecord): readonly Charge[] { return record.attempt?.charges ?? record.charges ?? []; }
|
|
13
|
+
|
|
14
|
+
/** Currency and cost basis are separate ledgers. Unquoted rows remain visible in coverage. */
|
|
15
|
+
export function billingBalances(records: readonly UsageRecord[]): BillingBalance[] {
|
|
16
|
+
const balances = new Map<string, BillingBalance>();
|
|
17
|
+
for (const record of records) for (const charge of recordCharges(record)) {
|
|
18
|
+
const { currency, basis } = charge.quote;
|
|
19
|
+
const key = JSON.stringify([currency, basis]);
|
|
20
|
+
const balance = balances.get(key) ?? { currency, basis, knownAmount: 0, pricedCalls: 0, partialCalls: 0 };
|
|
21
|
+
balance.knownAmount += charge.knownAmount;
|
|
22
|
+
if (charge.amount === null) balance.partialCalls++; else balance.pricedCalls++;
|
|
23
|
+
balances.set(key, balance);
|
|
24
|
+
}
|
|
25
|
+
return [...balances.values()].sort((a,b) => a.basis.localeCompare(b.basis) || a.currency.localeCompare(b.currency));
|
|
26
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 附件以句柄存入事件库与 session。
|
|
3
|
+
* `log:<内容哈希>.<ext>` 引用 data/media/ 中按内容寻址、只增的日志附件。
|
|
4
|
+
* `mem:<后端标识>` 由 Persona 的 BlobStore 解析,后端决定标识格式。
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import type { BlobRef } from './types.ts';
|
|
10
|
+
|
|
11
|
+
export const LOG_SCHEME = 'log:';
|
|
12
|
+
export const MEM_SCHEME = 'mem:';
|
|
13
|
+
|
|
14
|
+
const EXT_BY_MIME: Record<string, string> = {
|
|
15
|
+
'image/jpeg': '.jpg',
|
|
16
|
+
'image/png': '.png',
|
|
17
|
+
'image/gif': '.gif',
|
|
18
|
+
'image/bmp': '.bmp',
|
|
19
|
+
'image/webp': '.webp',
|
|
20
|
+
'application/pdf': '.pdf',
|
|
21
|
+
'audio/wav': '.wav',
|
|
22
|
+
'audio/mpeg': '.mp3',
|
|
23
|
+
'text/plain': '.txt',
|
|
24
|
+
'application/json': '.json',
|
|
25
|
+
'application/zip': '.zip',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** 日志附件的文件名形状(不含 scheme);路径分隔符一律拒绝(防穿越)。 */
|
|
29
|
+
const LOG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
30
|
+
/** 短句柄:至少 8 位十六进制,可带扩展名 */
|
|
31
|
+
const LOG_PREFIX_RE = /^([0-9a-f]{8,40})(\.[A-Za-z0-9]+)?$/;
|
|
32
|
+
const LOG_HASH_CHARS = 20;
|
|
33
|
+
|
|
34
|
+
export function blobScheme(handle: string): 'log' | 'mem' | null {
|
|
35
|
+
if (handle.startsWith(MEM_SCHEME)) return 'mem';
|
|
36
|
+
if (handle.startsWith(LOG_SCHEME)) return 'log';
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 按扩展名反推 mime;认不出的扩展名 → octet-stream。 */
|
|
41
|
+
export function mimeOfHandle(handle: string): string {
|
|
42
|
+
const dot = handle.lastIndexOf('.');
|
|
43
|
+
const ext = dot >= 0 ? handle.slice(dot).toLowerCase() : '';
|
|
44
|
+
for (const [mime, e] of Object.entries(EXT_BY_MIME)) if (e === ext) return mime;
|
|
45
|
+
return 'application/octet-stream';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 一份附件进入 session 的那一行:句柄、mime、名字,后接它的文本形态。 */
|
|
49
|
+
export function blobLine(ref: BlobRef): string {
|
|
50
|
+
return `[blob ${ref.handle} ${ref.mime}${ref.name ? ` ${ref.name}` : ''}] ${ref.fallbackText}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 正文后接每份附件一行;没有附件时原样交回。 */
|
|
54
|
+
export function withBlobLines(text: string, refs: readonly BlobRef[] | undefined): string {
|
|
55
|
+
if (!refs || refs.length === 0) return text;
|
|
56
|
+
const lines = refs.map(blobLine).join('\n');
|
|
57
|
+
return text ? `${text}\n${lines}` : lines;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 字节按内容哈希存入 data/media/,重复写入不增加副本;删除由运维处理。
|
|
62
|
+
* 读取接受完整句柄或唯一的十六进制前缀(至少 8 位)。
|
|
63
|
+
*/
|
|
64
|
+
export class LogBlobStore {
|
|
65
|
+
private readonly dir: string;
|
|
66
|
+
|
|
67
|
+
constructor(dataDir: string) {
|
|
68
|
+
this.dir = join(dataDir, 'media');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 落盘并返回 `log:` 句柄。 */
|
|
72
|
+
put(bytes: Uint8Array, mime: string): string {
|
|
73
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, LOG_HASH_CHARS);
|
|
74
|
+
const ext = EXT_BY_MIME[mime] ?? '.bin';
|
|
75
|
+
const name = `${hash}${ext}`;
|
|
76
|
+
const path = join(this.dir, name);
|
|
77
|
+
if (!existsSync(path)) {
|
|
78
|
+
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
79
|
+
writeFileSync(path, bytes);
|
|
80
|
+
}
|
|
81
|
+
return `${LOG_SCHEME}${name}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 按句柄取回;句柄不合形状、前缀不唯一或文件不在了返回 null。 */
|
|
85
|
+
read(handle: string): { bytes: Buffer; mime: string } | null {
|
|
86
|
+
const name = this.resolveName(handle);
|
|
87
|
+
if (!name) return null;
|
|
88
|
+
try {
|
|
89
|
+
return { bytes: readFileSync(join(this.dir, name)), mime: mimeOfHandle(name) };
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private resolveName(handle: string): string | null {
|
|
96
|
+
if (!handle.startsWith(LOG_SCHEME)) return null;
|
|
97
|
+
const raw = handle.slice(LOG_SCHEME.length);
|
|
98
|
+
if (!LOG_NAME_RE.test(raw)) return null;
|
|
99
|
+
if (existsSync(join(this.dir, raw))) return raw;
|
|
100
|
+
const m = LOG_PREFIX_RE.exec(raw);
|
|
101
|
+
if (!m || !existsSync(this.dir)) return null;
|
|
102
|
+
const [, prefix, ext] = m;
|
|
103
|
+
const hits = readdirSync(this.dir).filter((f) => f.startsWith(prefix) && (!ext || f.endsWith(ext)));
|
|
104
|
+
return hits.length === 1 ? hits[0] : null;
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/core/bus.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WakeBus 按到达顺序向单个消费者交付整个队列。触发模式及默认值见 types.ts 的 TriggerMode。
|
|
3
|
+
* debounce 的投递时间为 min(首件 + maxBatchAge, max(首件 + minBatchAge, 末件 + quietGap));
|
|
4
|
+
* 可计数外部事件达到 maxBatchSize 时立即投递。
|
|
5
|
+
* 队列包含即时事件、延迟渲染项与候选项。延迟渲染项不参与关键词匹配或外部事件计数;
|
|
6
|
+
* 候选项按外部事件计数,关键词只检查 gateText。
|
|
7
|
+
* piggyback 不触发计时、关键词或溢出,也不单独获得投递许可;需要其他项触发投递。
|
|
8
|
+
* 人工暂停阻止所有投递。DeliveryGate 解除、关键词回调授权或溢出授权均放行整批。
|
|
9
|
+
*/
|
|
10
|
+
import type { DeliveryGate, EventOrigin, Logger, TriggerMode, WakeItem } from './types.ts';
|
|
11
|
+
import { nullLogger } from './util.ts';
|
|
12
|
+
|
|
13
|
+
export interface WakeBusOptions {
|
|
14
|
+
/** 末件到达之后安静这么久才投递 */
|
|
15
|
+
quietGapMs: number;
|
|
16
|
+
/** 首件到达起至少攒这么久 */
|
|
17
|
+
minBatchAgeMs: number;
|
|
18
|
+
/** 首件到达起最多攒这么久,到点强制投递 */
|
|
19
|
+
maxBatchAgeMs: number;
|
|
20
|
+
/** 积压事件达到这个条数就强制投递 */
|
|
21
|
+
maxBatchSize: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 每次 push 读取同一个配置对象,原位修改在下一次 push 生效。
|
|
25
|
+
|
|
26
|
+
/** 入队时固定 piggyback 标志,后续计数与调度沿用该值。 */
|
|
27
|
+
interface Queued {
|
|
28
|
+
item: WakeItem;
|
|
29
|
+
piggyback: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class WakeBus {
|
|
33
|
+
private opts: WakeBusOptions;
|
|
34
|
+
private queue: Queued[] = [];
|
|
35
|
+
private paused = false;
|
|
36
|
+
private gate: DeliveryGate | null = null;
|
|
37
|
+
/** 一次整批投递许可;人工暂停的优先级更高。 */
|
|
38
|
+
private bypassGateOnce = false;
|
|
39
|
+
/** 到期即投递的那一个定时器;每次 push 按四条判据重算 */
|
|
40
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
/** 本批首件与末件的到达时刻(队列空时为 null) */
|
|
42
|
+
private firstAt: number | null = null;
|
|
43
|
+
private lastAt = 0;
|
|
44
|
+
/** 挂起中的消费者(单消费者,最多一个) */
|
|
45
|
+
private waiter: ((batch: WakeItem[]) => void) | null = null;
|
|
46
|
+
/** 已到投递条件但没有消费者在等:置真,消费者一来就取走 */
|
|
47
|
+
private ready = false;
|
|
48
|
+
/** preempt 只报告机械时机;是否仍可安全取消由主循环裁决。 */
|
|
49
|
+
private preemptHandler: (() => void) | null = null;
|
|
50
|
+
|
|
51
|
+
private readonly log: Logger;
|
|
52
|
+
|
|
53
|
+
constructor(opts: WakeBusOptions, log: Logger = nullLogger()) {
|
|
54
|
+
this.opts = opts;
|
|
55
|
+
this.log = log;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setPreemptHandler(handler: () => void): void {
|
|
59
|
+
this.preemptHandler = handler;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
push(item: WakeItem, opts?: { trigger?: TriggerMode }): void {
|
|
63
|
+
const origin = originOf(item);
|
|
64
|
+
const trigger: TriggerMode =
|
|
65
|
+
opts?.trigger ?? (origin === 'internal' ? 'flush' : 'debounce');
|
|
66
|
+
const piggyback = trigger === 'piggyback';
|
|
67
|
+
this.queue.push({ item, piggyback });
|
|
68
|
+
const gate = this.gate;
|
|
69
|
+
if (gate) {
|
|
70
|
+
this.clearTimers();
|
|
71
|
+
// piggyback 不参与关键词或溢出判定。
|
|
72
|
+
if (piggyback) return;
|
|
73
|
+
// 延迟渲染项没有可用于关键词匹配的正文。
|
|
74
|
+
const gateText = textForGate(item);
|
|
75
|
+
if (origin === 'external' && gate.keyword !== undefined && gateText?.includes(gate.keyword)) {
|
|
76
|
+
gate.onKeyword();
|
|
77
|
+
const permitted = this.gate !== gate || this.bypassGateOnce;
|
|
78
|
+
this.log.emit('debug', '闸门关键词命中', { event: 'gate-keyword', data: { gate: gate.id, permitted } });
|
|
79
|
+
if (permitted) this.deliver(this.bypassGateOnce);
|
|
80
|
+
this.notifyPreempt(trigger, permitted);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// 已经有一批获准放行时,新到直接并入该批,不重复产生溢出通知。
|
|
84
|
+
if (this.bypassGateOnce) {
|
|
85
|
+
this.deliver(true);
|
|
86
|
+
this.notifyPreempt(trigger, true);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (origin === 'external' && this.pendingEventCount() > gate.overflowLimit) {
|
|
90
|
+
// 回调可能同步注入事件;先设置许可,使注入项仍属于当前批。
|
|
91
|
+
this.bypassGateOnce = true;
|
|
92
|
+
this.log.emit('debug', '闸门下积压越过溢出线,整批放行', { event: 'gate-overflow', data: { gate: gate.id, pending: this.pendingEventCount(), limit: gate.overflowLimit } });
|
|
93
|
+
gate.onOverflow();
|
|
94
|
+
this.deliver(true);
|
|
95
|
+
this.notifyPreempt(trigger, true);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (trigger === 'preempt') {
|
|
101
|
+
const permitted = !this.blocked();
|
|
102
|
+
this.deliver();
|
|
103
|
+
this.notifyPreempt(trigger, permitted);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (trigger === 'flush') {
|
|
107
|
+
this.deliver();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// piggyback 不启动或延后批次计时器。
|
|
111
|
+
if (piggyback) return;
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (this.firstAt === null) this.firstAt = now;
|
|
114
|
+
this.lastAt = now;
|
|
115
|
+
if (this.pendingEventCount() >= this.opts.maxBatchSize) {
|
|
116
|
+
this.deliver();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.arm();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 重新计算投递时间;配置修改在下一次 push 生效。 */
|
|
123
|
+
private arm(): void {
|
|
124
|
+
if (this.firstAt === null) return;
|
|
125
|
+
const { quietGapMs, minBatchAgeMs, maxBatchAgeMs } = this.opts;
|
|
126
|
+
const at = Math.min(
|
|
127
|
+
this.firstAt + maxBatchAgeMs,
|
|
128
|
+
Math.max(this.firstAt + minBatchAgeMs, this.lastAt + quietGapMs),
|
|
129
|
+
);
|
|
130
|
+
if (this.timer) clearTimeout(this.timer);
|
|
131
|
+
this.timer = setTimeout(() => this.deliver(), Math.max(0, at - Date.now()));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 暂停或闸门(未获准整批放行)期间不投递。 */
|
|
135
|
+
private blocked(): boolean {
|
|
136
|
+
return this.paused || (this.gate !== null && !this.bypassGateOnce);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 已获准投递的 preempt 才能取消当前模型轮;暂停与闸门继续拥有更高优先级。 */
|
|
140
|
+
private notifyPreempt(trigger: TriggerMode, permitted: boolean): void {
|
|
141
|
+
if (trigger === 'preempt' && permitted && !this.paused) {
|
|
142
|
+
this.log.emit('debug', '抢占:请求取消尚未外化的在途模型轮', { event: 'preempt' });
|
|
143
|
+
this.preemptHandler?.();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 人工暂停/继续(控制台);继续时积压一次性投递 */
|
|
148
|
+
setPaused(v: boolean): void {
|
|
149
|
+
if (v !== this.paused) this.log.emit('info', v ? '总线已暂停:事件照常落库,不投递' : '总线继续', { event: v ? 'paused' : 'resumed', data: { queued: this.queue.length } });
|
|
150
|
+
this.paused = v;
|
|
151
|
+
if (!this.paused && (this.ready || this.queue.length > 0)) {
|
|
152
|
+
this.deliver(this.bypassGateOnce);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
isPaused(): boolean {
|
|
157
|
+
return this.paused;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 安装或更新投递闸门。若此前没有闸门,安装前已经积压的内容获准放行一次;
|
|
162
|
+
* 闸门只约束安装之后到达的唤醒项。
|
|
163
|
+
*/
|
|
164
|
+
setDeliveryGate(gate: DeliveryGate): void {
|
|
165
|
+
const hadGate = this.gate !== null;
|
|
166
|
+
this.gate = gate;
|
|
167
|
+
this.clearTimers();
|
|
168
|
+
this.log.emit('debug', hadGate ? '投递闸门已更新' : '投递闸门已安装', { event: 'gate-set', data: { gate: gate.id, queued: this.queue.length } });
|
|
169
|
+
if (!hadGate && this.queue.length > 0) {
|
|
170
|
+
this.deliver(true);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 只解除匹配id的闸门。deliverQueued=false用于"先解闸、再把到期通知和积压
|
|
176
|
+
* 一起投递",避免拆成两个user回合。
|
|
177
|
+
*/
|
|
178
|
+
clearDeliveryGate(id: string, deliverQueued = true): boolean {
|
|
179
|
+
if (this.gate?.id !== id) return false;
|
|
180
|
+
this.gate = null;
|
|
181
|
+
this.log.emit('debug', '投递闸门已解除', { event: 'gate-cleared', data: { gate: id, queued: this.queue.length, deliverQueued } });
|
|
182
|
+
if (deliverQueued && this.queue.length > 0) {
|
|
183
|
+
this.deliver(this.bypassGateOnce);
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
isDeliveryBlocked(): boolean {
|
|
189
|
+
return this.gate !== null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 当前积压条数(控制台可见性;含尚未成文的投递成文项) */
|
|
193
|
+
pending(): number {
|
|
194
|
+
return this.queue.length;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 供空闲判定使用:不计延迟渲染项和 piggyback 项。 */
|
|
198
|
+
pendingImmediate(): number {
|
|
199
|
+
let n = 0;
|
|
200
|
+
for (const q of this.queue) {
|
|
201
|
+
if (!q.piggyback && (q.item.event !== undefined || q.item.candidate !== undefined)) n++;
|
|
202
|
+
}
|
|
203
|
+
return n;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 按原序取出匹配项,消费后不再投递。同步完成,不触发消费者或改变投递许可和计时器。 */
|
|
207
|
+
drainPending(pred: (item: WakeItem) => boolean): WakeItem[] {
|
|
208
|
+
const drained: WakeItem[] = [];
|
|
209
|
+
const kept: Queued[] = [];
|
|
210
|
+
for (const q of this.queue) {
|
|
211
|
+
if (pred(q.item)) drained.push(q.item);
|
|
212
|
+
else kept.push(q);
|
|
213
|
+
}
|
|
214
|
+
this.queue = kept;
|
|
215
|
+
// 仅剩 piggyback 项时清除 ready 和计时器,避免触发空批次。
|
|
216
|
+
if (!this.hasWakingItem()) {
|
|
217
|
+
this.ready = false;
|
|
218
|
+
this.bypassGateOnce = false;
|
|
219
|
+
this.firstAt = null;
|
|
220
|
+
this.clearTimers();
|
|
221
|
+
}
|
|
222
|
+
return drained;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 仅在满足投递条件时同步取走整批,否则返回 null。
|
|
227
|
+
* 供工具调用链在没有 nextBatch 消费者时接收已就绪的通知。
|
|
228
|
+
*/
|
|
229
|
+
takeIfReady(): WakeItem[] | null {
|
|
230
|
+
if (!this.ready || this.queue.length === 0 || this.blocked()) return null;
|
|
231
|
+
this.ready = false;
|
|
232
|
+
return this.take();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** 取走整个队列;尚不可投递时等待。只允许一个等待中的消费者。 */
|
|
236
|
+
nextBatch(): Promise<WakeItem[]> {
|
|
237
|
+
if (this.waiter) throw new Error('WakeBus只支持单消费者');
|
|
238
|
+
if (this.ready && this.queue.length > 0 && !this.blocked()) {
|
|
239
|
+
this.ready = false;
|
|
240
|
+
return Promise.resolve(this.take());
|
|
241
|
+
}
|
|
242
|
+
return new Promise<WakeItem[]>((resolve) => {
|
|
243
|
+
this.waiter = resolve;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private deliver(bypassGate = false): void {
|
|
248
|
+
this.clearTimers();
|
|
249
|
+
// piggyback 需要其他项触发投递。
|
|
250
|
+
if (this.queue.length > 0 && !this.hasWakingItem()) return;
|
|
251
|
+
if (bypassGate) this.bypassGateOnce = true;
|
|
252
|
+
if (this.blocked()) {
|
|
253
|
+
this.ready = true;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (this.queue.length === 0) {
|
|
257
|
+
// 许可仅用于当前批,不能沿用到下一批。
|
|
258
|
+
this.bypassGateOnce = false;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (this.waiter) {
|
|
262
|
+
const w = this.waiter;
|
|
263
|
+
this.waiter = null;
|
|
264
|
+
this.ready = false;
|
|
265
|
+
w(this.take());
|
|
266
|
+
} else {
|
|
267
|
+
this.ready = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private take(): WakeItem[] {
|
|
272
|
+
const batch = this.queue.map((q) => q.item);
|
|
273
|
+
this.log.emit('debug', '投递一批', { event: 'deliver', data: { count: batch.length, piggyback: this.queue.filter((q) => q.piggyback).length, ageMs: this.firstAt === null ? 0 : Date.now() - this.firstAt } });
|
|
274
|
+
this.queue = [];
|
|
275
|
+
this.bypassGateOnce = false;
|
|
276
|
+
this.firstAt = null; // 下一批的延迟下限和上限从首条事件重新计时。
|
|
277
|
+
return batch;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 供批次大小与闸门上限使用:只计外部即时事件和候选项,不计 piggyback。 */
|
|
281
|
+
private pendingEventCount(): number {
|
|
282
|
+
let count = 0;
|
|
283
|
+
for (const q of this.queue) {
|
|
284
|
+
if (
|
|
285
|
+
!q.piggyback &&
|
|
286
|
+
(q.item.event !== undefined || q.item.candidate !== undefined) &&
|
|
287
|
+
originOf(q.item) === 'external'
|
|
288
|
+
) count++;
|
|
289
|
+
}
|
|
290
|
+
return count;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private hasWakingItem(): boolean {
|
|
294
|
+
return this.queue.some((q) => !q.piggyback);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private clearTimers(): void {
|
|
298
|
+
if (this.timer) {
|
|
299
|
+
clearTimeout(this.timer);
|
|
300
|
+
this.timer = null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function originOf(item: WakeItem): EventOrigin {
|
|
306
|
+
return item.event?.origin ?? item.deferred?.origin ?? item.candidate!.origin;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function textForGate(item: WakeItem): string | null {
|
|
310
|
+
return item.event?.text ?? item.candidate?.gateText ?? null;
|
|
311
|
+
}
|