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
package/src/world.ts
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按 WorldDefinition 构造实例,依据 worlds.<id>.enabled 挂载,并提供运行时激活、停用和重启。
|
|
3
|
+
* 挂载状态与启动状态分开,实际启动由 Core 管理。定义实例在停用和重启时重新构造;
|
|
4
|
+
* 预建实例重启时复用原对象。名称、标签与缺失原因来自定义或 bot 声明。
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import type { CoreConfig, World, WorldLifecycleEvent } from './core/types.ts';
|
|
9
|
+
import type { LoadedConfig } from './core/config.ts';
|
|
10
|
+
import type { BotDefinition } from './bot.ts';
|
|
11
|
+
import { updateJsonObject } from './config-file.ts';
|
|
12
|
+
import { pick, type Language } from './core/language.ts';
|
|
13
|
+
|
|
14
|
+
/** 装配层给控制台的回执与拒绝理由,按发起请求的界面语言取。 */
|
|
15
|
+
const ASSEMBLY_TEXT = {
|
|
16
|
+
zh: {
|
|
17
|
+
constructFailed: (detail: string) => `构造失败: ${detail}`,
|
|
18
|
+
notImplemented: '本地没有找到这个 World 的实现。',
|
|
19
|
+
unknownWorld: (id: string) => `未知 World: ${id}`,
|
|
20
|
+
alreadyRunning: (label: string) => `${label} 已启用`,
|
|
21
|
+
prebuilt: (label: string) => `${label} 是预建实例,不经装配层激活`,
|
|
22
|
+
activated: (label: string, id: string) => `${label}(${id})已启用`,
|
|
23
|
+
deactivated: (label: string, id: string) => `${label}(${id})已停用`,
|
|
24
|
+
notActive: (label: string) => `${label} 未激活,没有可重启的实例`,
|
|
25
|
+
restarted: (label: string) => `${label} 已重启`,
|
|
26
|
+
toolClash: (other: string, names: string[]) => `工具名与 ${other} 撞名,拒绝挂载: ${names.join(', ')}`,
|
|
27
|
+
toolReserved: (names: string[]) => `工具名已被 Core 或 Persona 占用,拒绝挂载: ${names.join(', ')}`,
|
|
28
|
+
unbound: '装配层尚未绑定 core',
|
|
29
|
+
},
|
|
30
|
+
en: {
|
|
31
|
+
constructFailed: (detail: string) => `Construction failed: ${detail}`,
|
|
32
|
+
notImplemented: 'No implementation of this World was found locally.',
|
|
33
|
+
unknownWorld: (id: string) => `Unknown World: ${id}`,
|
|
34
|
+
alreadyRunning: (label: string) => `${label} is already enabled`,
|
|
35
|
+
prebuilt: (label: string) => `${label} is a prebuilt instance and is not activated through assembly`,
|
|
36
|
+
activated: (label: string, id: string) => `${label} (${id}) enabled`,
|
|
37
|
+
deactivated: (label: string, id: string) => `${label} (${id}) disabled`,
|
|
38
|
+
notActive: (label: string) => `${label} is not active, so there is no instance to restart`,
|
|
39
|
+
restarted: (label: string) => `${label} restarted`,
|
|
40
|
+
toolClash: (other: string, names: string[]) => `Tool names clash with ${other}, refusing to mount: ${names.join(', ')}`,
|
|
41
|
+
toolReserved: (names: string[]) => `Tool names are taken by Core or the Persona, refusing to mount: ${names.join(', ')}`,
|
|
42
|
+
unbound: 'The assembly layer is not bound to a core yet',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const text = (language: Language) => pick(language, ASSEMBLY_TEXT);
|
|
46
|
+
|
|
47
|
+
/** 每个 World 配置段的最小形状。 */
|
|
48
|
+
export interface WorldSection {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type DeepPartial<T> = T extends readonly unknown[]
|
|
53
|
+
? T
|
|
54
|
+
: T extends object
|
|
55
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
56
|
+
: T;
|
|
57
|
+
|
|
58
|
+
/** 装配层提供给 World 构造函数的上下文。 */
|
|
59
|
+
export interface WorldContext<S extends WorldSection = WorldSection> {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
/** worlds.<id> 的共享引用;每次读取的配置立即生效,构造时保存的值需重启实例。 */
|
|
62
|
+
readonly cfg: S;
|
|
63
|
+
readonly timezone: string;
|
|
64
|
+
readonly botName: string;
|
|
65
|
+
/** 部署目录,保存本机配置、密钥和部署资产覆盖。 */
|
|
66
|
+
readonly botDir: string;
|
|
67
|
+
/** bot 代码包目录,供包内模板与资产使用,可由多个部署共享。 */
|
|
68
|
+
readonly packageDir: string;
|
|
69
|
+
readonly dataDir: string;
|
|
70
|
+
readonly repoRoot: string;
|
|
71
|
+
secret(name: string): string;
|
|
72
|
+
/** 将密钥写入部署 .env,并更新进程环境供 secret() 立即读取。 */
|
|
73
|
+
storeSecret(name: string, value: string): void;
|
|
74
|
+
/** 深合并进 `worlds.<id>` 段:活对象与 config.json 同步。数组整体替换。 */
|
|
75
|
+
persist(patch: DeepPartial<S>): void;
|
|
76
|
+
/** 按 cfg.enabled 同步状态:启用时激活或重启,禁用时停止。 */
|
|
77
|
+
restart(): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface WorldDefinition<S extends WorldSection = WorldSection> {
|
|
81
|
+
id: string;
|
|
82
|
+
label: string;
|
|
83
|
+
/** World 配置默认值,每次返回新对象;enabled=false,由 bot 声明或部署配置启用。 */
|
|
84
|
+
defaults(): S;
|
|
85
|
+
/** 激活前置检查。抛错 = 不能激活,错误信息原样给操作者。 */
|
|
86
|
+
preflight?(ctx: WorldContext<S>): void;
|
|
87
|
+
/**
|
|
88
|
+
* 返回 x-options 的完整选项表,包含按请求语言生成的固定项。
|
|
89
|
+
* 未知 kind 返回空数组,框架继续查询其他 World;当前值不在表中时由控制台补入。
|
|
90
|
+
* Persona 的选项由 Bot ConsoleContribution 提供。
|
|
91
|
+
*/
|
|
92
|
+
configOptions?(kind: string, language: Language): Array<{ value: string; label: string }>;
|
|
93
|
+
create(ctx: WorldContext<S>): World;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* bot 默认使用的 World 声明。字符串是 World id;对象还可提供标签与缺失原因。
|
|
98
|
+
* 两种声明均允许本地缺少实现。
|
|
99
|
+
*/
|
|
100
|
+
export type WorldDeclaration = string | { id: string; label: string; reason?: string };
|
|
101
|
+
|
|
102
|
+
/** 各 World 的默认配置段;已声明的 id 默认启用。 */
|
|
103
|
+
export function worldDefaults(
|
|
104
|
+
definitions: readonly WorldDefinition<WorldSection>[],
|
|
105
|
+
declares: readonly WorldDeclaration[],
|
|
106
|
+
overrides: Record<string, Record<string, unknown>> = {},
|
|
107
|
+
): Record<string, WorldSection> {
|
|
108
|
+
const declared = new Set(declares.map((d) => (typeof d === 'string' ? d : d.id)));
|
|
109
|
+
const worlds: Record<string, WorldSection> = {};
|
|
110
|
+
for (const def of definitions) {
|
|
111
|
+
worlds[def.id] = { ...def.defaults(), enabled: declared.has(def.id), ...(overrides[def.id] ?? {}) };
|
|
112
|
+
}
|
|
113
|
+
return worlds;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 将可用 World 定义加入 bot。对 defaults() 中缺失的 World 段补充默认值,
|
|
118
|
+
* 已声明的 id 默认启用,其余默认关闭;已有配置段保持原样。
|
|
119
|
+
*/
|
|
120
|
+
export function withWorlds<C extends CoreConfig>(
|
|
121
|
+
definition: BotDefinition<C>,
|
|
122
|
+
definitions: readonly WorldDefinition<WorldSection>[],
|
|
123
|
+
): BotDefinition<C> {
|
|
124
|
+
const declared = new Set((definition.declares ?? []).map((d) => (typeof d === 'string' ? d : d.id)));
|
|
125
|
+
return {
|
|
126
|
+
...definition,
|
|
127
|
+
worlds: definitions,
|
|
128
|
+
defaults: () => {
|
|
129
|
+
const config = definition.defaults();
|
|
130
|
+
const worlds = ((config as unknown as { worlds?: Record<string, WorldSection> }).worlds ??= {});
|
|
131
|
+
for (const def of definitions) worlds[def.id] ??= { ...def.defaults(), enabled: declared.has(def.id) };
|
|
132
|
+
return config;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface WorldSlot {
|
|
138
|
+
readonly id: string;
|
|
139
|
+
readonly label: string;
|
|
140
|
+
/** bot 声明过的渠道;false = 部署侧选配。 */
|
|
141
|
+
readonly declared: boolean;
|
|
142
|
+
/** null = 预建实例(不能重建,只能停/起)。 */
|
|
143
|
+
readonly definition: WorldDefinition<WorldSection> | null;
|
|
144
|
+
instance: World;
|
|
145
|
+
mounted: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface MissingWorld {
|
|
149
|
+
readonly id: string;
|
|
150
|
+
readonly label: string;
|
|
151
|
+
/** 缺失的原因,按界面语言给;bot 声明里自带的理由不翻译。 */
|
|
152
|
+
readonly reason: (language: Language) => string;
|
|
153
|
+
/** Persona声明过的渠道。缺省 true:声明了却没实现的那类。 */
|
|
154
|
+
readonly declared?: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface PrebuiltOptions {
|
|
158
|
+
/** 控制台显示名;缺省用 id。 */
|
|
159
|
+
labels?: Record<string, string>;
|
|
160
|
+
/** 按 bot 声明的渠道算(缺省)还是按部署侧选配算。 */
|
|
161
|
+
declared?: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Core 构造后绑定挂载与卸载方法。 */
|
|
165
|
+
export interface WorldMountHost {
|
|
166
|
+
mount(mod: World): Promise<void>;
|
|
167
|
+
unmount(id: string): Promise<void>;
|
|
168
|
+
/** 装配层改了一个槽位的挂载状态(激活 / 停用 / 重启)。启动期的初始挂载不报。 */
|
|
169
|
+
lifecycle?(event: WorldLifecycleEvent): void;
|
|
170
|
+
/**
|
|
171
|
+
* Core 保留帧与 Persona 自有工具的名称。绑定时检查已挂载实例,
|
|
172
|
+
* 冲突实例移入 missing;后续激活、重启和添加预建实例同样检查。
|
|
173
|
+
*/
|
|
174
|
+
reservedToolNames?(): readonly string[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
178
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
179
|
+
|
|
180
|
+
/** 就地深合并:叶子替换,沿途对象身份保持,数组整体替换。 */
|
|
181
|
+
function assignDeep(target: Record<string, unknown>, patch: Record<string, unknown>): void {
|
|
182
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
183
|
+
if (value === undefined) continue;
|
|
184
|
+
const current = target[key];
|
|
185
|
+
if (isPlainObject(value) && isPlainObject(current)) assignDeep(current, value);
|
|
186
|
+
else target[key] = isPlainObject(value) ? structuredClone(value) : value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function writeEnvLine(envPath: string, name: string, value: string): void {
|
|
191
|
+
const prev = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
|
|
192
|
+
const line = `${name}=${value}`;
|
|
193
|
+
const pattern = new RegExp(`^[ \\t]*${name}\\s*=.*$`, 'm');
|
|
194
|
+
const next = pattern.test(prev)
|
|
195
|
+
? prev.replace(pattern, line)
|
|
196
|
+
: (prev.trimEnd() ? `${prev.trimEnd()}\n${line}\n` : `${line}\n`);
|
|
197
|
+
writeFileSync(envPath, next, 'utf8');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export class WorldAssembly {
|
|
201
|
+
/** 挂载表。与 core 共用同一个数组,激活/停用就地增删。 */
|
|
202
|
+
readonly mounted: World[] = [];
|
|
203
|
+
readonly slots: WorldSlot[] = [];
|
|
204
|
+
readonly missing: MissingWorld[] = [];
|
|
205
|
+
private host: WorldMountHost | null = null;
|
|
206
|
+
private readonly cfgPath: string;
|
|
207
|
+
|
|
208
|
+
constructor(
|
|
209
|
+
private readonly loaded: LoadedConfig<CoreConfig>,
|
|
210
|
+
worlds: readonly WorldDefinition<WorldSection>[],
|
|
211
|
+
declares: readonly WorldDeclaration[],
|
|
212
|
+
) {
|
|
213
|
+
this.cfgPath = join(loaded.rootDir, 'config.json');
|
|
214
|
+
const declared = new Set<string>();
|
|
215
|
+
for (const decl of declares) declared.add(typeof decl === 'string' ? decl : decl.id);
|
|
216
|
+
const defined = new Set(worlds.map((m) => m.id));
|
|
217
|
+
for (const def of worlds) {
|
|
218
|
+
const section = this.section(def);
|
|
219
|
+
let instance: World;
|
|
220
|
+
try {
|
|
221
|
+
instance = def.create(this.context(def));
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// 单个定义构造失败记入 missing,继续构造其他 World。
|
|
224
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
225
|
+
this.missing.push({
|
|
226
|
+
id: def.id,
|
|
227
|
+
label: def.label,
|
|
228
|
+
declared: declared.has(def.id),
|
|
229
|
+
reason: (language) => text(language).constructFailed(detail),
|
|
230
|
+
});
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const clash = section.enabled ? this.toolClash(instance, this.mounted) : null;
|
|
234
|
+
if (clash) {
|
|
235
|
+
this.missing.push({ id: def.id, label: def.label, declared: declared.has(def.id), reason: clash });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const slot: WorldSlot = {
|
|
239
|
+
id: def.id,
|
|
240
|
+
label: def.label,
|
|
241
|
+
declared: declared.has(def.id),
|
|
242
|
+
definition: def,
|
|
243
|
+
instance,
|
|
244
|
+
mounted: false,
|
|
245
|
+
};
|
|
246
|
+
this.slots.push(slot);
|
|
247
|
+
if (section.enabled) {
|
|
248
|
+
slot.mounted = true;
|
|
249
|
+
this.mounted.push(slot.instance);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const decl of declares) {
|
|
253
|
+
if (typeof decl === 'string') {
|
|
254
|
+
if (!defined.has(decl)) this.missing.push({ id: decl, label: decl, reason: (language) => text(language).notImplemented });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (defined.has(decl.id)) continue;
|
|
258
|
+
const given = decl.reason;
|
|
259
|
+
this.missing.push({ id: decl.id, label: decl.label, reason: given ? () => given : (language) => text(language).notImplemented });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 只有预建实例的槽位表(测试与开发态用):不读配置,不写 config.json。 */
|
|
264
|
+
static ofInstances(instances: readonly World[], opts: PrebuiltOptions = {}): WorldAssembly {
|
|
265
|
+
const loaded = {
|
|
266
|
+
config: { worlds: {} } as unknown as CoreConfig,
|
|
267
|
+
secret: () => '',
|
|
268
|
+
rootDir: '',
|
|
269
|
+
memoryDir: '',
|
|
270
|
+
dataDir: '',
|
|
271
|
+
} satisfies LoadedConfig<CoreConfig>;
|
|
272
|
+
const assembly = new WorldAssembly(loaded, [], []);
|
|
273
|
+
assembly.addPrebuilt(instances, opts);
|
|
274
|
+
return assembly;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 预建实例初始挂载,重启复用原对象;同 id 时替换原定义实例。 */
|
|
278
|
+
addPrebuilt(instances: readonly World[], opts: PrebuiltOptions = {}): void {
|
|
279
|
+
for (const mod of instances) {
|
|
280
|
+
const existing = this.slots.findIndex((s) => s.id === mod.id);
|
|
281
|
+
if (existing >= 0) {
|
|
282
|
+
const old = this.slots[existing];
|
|
283
|
+
if (old.mounted) this.mounted.splice(this.mounted.indexOf(old.instance), 1);
|
|
284
|
+
this.slots.splice(existing, 1);
|
|
285
|
+
}
|
|
286
|
+
const clash = this.toolClash(mod, this.mounted);
|
|
287
|
+
if (clash) throw new Error(clash('zh'));
|
|
288
|
+
this.slots.push({
|
|
289
|
+
id: mod.id,
|
|
290
|
+
label: opts.labels?.[mod.id] ?? mod.id,
|
|
291
|
+
declared: opts.declared ?? true,
|
|
292
|
+
definition: null,
|
|
293
|
+
instance: mod,
|
|
294
|
+
mounted: true,
|
|
295
|
+
});
|
|
296
|
+
this.mounted.push(mod);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 绑定 Core 后检查保留名称,冲突的已挂载实例移入 missing。 */
|
|
301
|
+
bind(host: WorldMountHost): void {
|
|
302
|
+
this.host = host;
|
|
303
|
+
for (const slot of [...this.slots]) {
|
|
304
|
+
if (!slot.mounted) continue;
|
|
305
|
+
const clash = this.toolClash(slot.instance, []);
|
|
306
|
+
if (!clash) continue;
|
|
307
|
+
this.mounted.splice(this.mounted.indexOf(slot.instance), 1);
|
|
308
|
+
this.slots.splice(this.slots.indexOf(slot), 1);
|
|
309
|
+
this.missing.push({ id: slot.id, label: slot.label, declared: slot.declared, reason: clash });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
slot(id: string, language: Language = 'zh'): WorldSlot {
|
|
314
|
+
const slot = this.slots.find((s) => s.id === id);
|
|
315
|
+
if (!slot) throw new Error(text(language).unknownWorld(id));
|
|
316
|
+
return slot;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 所有槽位的实例(含未挂载的),给控制台拼面板、提示词文档与配置组用。 */
|
|
320
|
+
instances(): World[] {
|
|
321
|
+
return this.slots.map((s) => s.instance);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
labelOf(id: string): string | undefined {
|
|
325
|
+
return this.slots.find((s) => s.id === id)?.label;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** 三个动作的回执与拒绝理由按 `language` 给;省略 = 中文。 */
|
|
329
|
+
async activate(id: string, language: Language = 'zh'): Promise<string> {
|
|
330
|
+
const t = text(language);
|
|
331
|
+
const slot = this.slot(id, language);
|
|
332
|
+
if (slot.mounted) return t.alreadyRunning(slot.label);
|
|
333
|
+
const def = slot.definition;
|
|
334
|
+
if (!def) throw new Error(t.prebuilt(slot.label));
|
|
335
|
+
const ctx = this.context(def);
|
|
336
|
+
def.preflight?.(ctx);
|
|
337
|
+
const clash = this.toolClash(slot.instance, this.mounted);
|
|
338
|
+
if (clash) throw new Error(clash(language));
|
|
339
|
+
this.persist(id, { enabled: true });
|
|
340
|
+
try {
|
|
341
|
+
await this.mountHost().mount(slot.instance);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
this.persist(id, { enabled: false });
|
|
344
|
+
slot.instance = def.create(ctx);
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
slot.mounted = true;
|
|
348
|
+
this.host?.lifecycle?.({ kind: 'mounted', id, label: slot.label });
|
|
349
|
+
return t.activated(slot.label, id);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async deactivate(id: string, language: Language = 'zh'): Promise<string> {
|
|
353
|
+
const slot = this.slot(id, language);
|
|
354
|
+
const wasMounted = slot.mounted;
|
|
355
|
+
if (wasMounted) await this.stopSlot(slot);
|
|
356
|
+
this.persist(id, { enabled: false });
|
|
357
|
+
if (wasMounted) this.host?.lifecycle?.({ kind: 'unmounted', id, label: slot.label });
|
|
358
|
+
return text(language).deactivated(slot.label, id);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async restart(id: string, language: Language = 'zh'): Promise<string> {
|
|
362
|
+
const slot = this.slot(id, language);
|
|
363
|
+
if (!slot.mounted) throw new Error(text(language).notActive(slot.label));
|
|
364
|
+
await this.stopSlot(slot);
|
|
365
|
+
const clash = this.toolClash(slot.instance, this.mounted);
|
|
366
|
+
if (clash) throw new Error(clash(language));
|
|
367
|
+
await this.mountHost().mount(slot.instance);
|
|
368
|
+
slot.mounted = true;
|
|
369
|
+
this.host?.lifecycle?.({ kind: 'restarted', id, label: slot.label });
|
|
370
|
+
return text(language).restarted(slot.label);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** 按 worlds.<id>.enabled 同步状态,供 WorldContext.restart 调用。 */
|
|
374
|
+
async sync(id: string): Promise<void> {
|
|
375
|
+
const slot = this.slot(id);
|
|
376
|
+
const enabled = (slot.definition ? this.section(slot.definition) : { enabled: true }).enabled;
|
|
377
|
+
if (enabled) {
|
|
378
|
+
if (slot.mounted) await this.restart(id);
|
|
379
|
+
else await this.activate(id);
|
|
380
|
+
} else if (slot.mounted) {
|
|
381
|
+
await this.stopSlot(slot);
|
|
382
|
+
this.host?.lifecycle?.({ kind: 'unmounted', id, label: slot.label });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private async stopSlot(slot: WorldSlot): Promise<void> {
|
|
387
|
+
await this.mountHost().unmount(slot.id);
|
|
388
|
+
slot.mounted = false;
|
|
389
|
+
if (slot.definition) slot.instance = slot.definition.create(this.context(slot.definition));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** 工具名在 bot 内唯一。与保留名称或已挂载 World 工具冲突时拒绝挂载。 */
|
|
393
|
+
private toolClash(mod: World, mounted: readonly World[]): ((language: Language) => string) | null {
|
|
394
|
+
const names = new Set(mod.tools().map((t) => t.name));
|
|
395
|
+
const reserved = (this.host?.reservedToolNames?.() ?? []).filter((n) => names.has(n));
|
|
396
|
+
if (reserved.length) return (language) => text(language).toolReserved(reserved);
|
|
397
|
+
for (const other of mounted) {
|
|
398
|
+
const shared = other.tools().map((t) => t.name).filter((n) => names.has(n));
|
|
399
|
+
if (shared.length) {
|
|
400
|
+
const label = this.labelOf(other.id) ?? other.id;
|
|
401
|
+
return (language) => text(language).toolClash(label, shared);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private mountHost(): WorldMountHost {
|
|
408
|
+
if (!this.host) throw new Error(text('zh').unbound);
|
|
409
|
+
return this.host;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** `worlds.<id>` 活引用;缺段时按定义默认值补一段。 */
|
|
413
|
+
private section(def: WorldDefinition<WorldSection>): WorldSection {
|
|
414
|
+
const worlds = ((this.loaded.config as unknown as { worlds?: Record<string, WorldSection> }).worlds ??= {});
|
|
415
|
+
return (worlds[def.id] ??= def.defaults());
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private persist(id: string, patch: Record<string, unknown>): void {
|
|
419
|
+
const worlds = (this.loaded.config as unknown as { worlds: Record<string, Record<string, unknown>> }).worlds;
|
|
420
|
+
assignDeep(worlds[id], patch);
|
|
421
|
+
updateJsonObject(this.cfgPath, (raw) => {
|
|
422
|
+
const rawIo = (raw.worlds ??= {}) as Record<string, Record<string, unknown>>;
|
|
423
|
+
const section = (rawIo[id] ??= {});
|
|
424
|
+
assignDeep(section, patch);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private context<S extends WorldSection>(def: WorldDefinition<S>): WorldContext<S> {
|
|
429
|
+
const { loaded } = this;
|
|
430
|
+
const cfg = loaded.config;
|
|
431
|
+
return {
|
|
432
|
+
id: def.id,
|
|
433
|
+
cfg: this.section(def as unknown as WorldDefinition<WorldSection>) as S,
|
|
434
|
+
timezone: cfg.timezone,
|
|
435
|
+
botName: cfg.displayName,
|
|
436
|
+
botDir: loaded.rootDir,
|
|
437
|
+
packageDir: loaded.packageDir ?? loaded.rootDir,
|
|
438
|
+
dataDir: loaded.dataDir,
|
|
439
|
+
repoRoot: loaded.repoRoot ?? loaded.rootDir,
|
|
440
|
+
secret: (name) => loaded.secret(name),
|
|
441
|
+
storeSecret: (name, value) => {
|
|
442
|
+
writeEnvLine(join(loaded.rootDir, '.env'), name, value);
|
|
443
|
+
process.env[name] = value;
|
|
444
|
+
},
|
|
445
|
+
persist: (patch) => this.persist(def.id, patch as Record<string, unknown>),
|
|
446
|
+
restart: () => this.sync(def.id),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<!-- Owner: src/worlds/bilibili/definition.ts -->
|
|
2
|
+
|
|
3
|
+
# worlds/bilibili
|
|
4
|
+
|
|
5
|
+
B 站直播间协议只读接入。观众的弹幕、礼物、醒目留言、上舰、进场与人流读数从这里进事件流;
|
|
6
|
+
同一 World 提供本机透明 Overlay。唯一工具 `bilibili_set_announcement` 只写本机 Agent 公告栏,
|
|
7
|
+
不会向 B 站账号发送弹幕或执行平台操作。
|
|
8
|
+
|
|
9
|
+
“重要观众”指符合 UID 资格、在限流时优先分配上下文预算的观众。资格、拥挤状态、事件分组与人物档案召回由 [`audience-admission.ts`](audience-admission.ts) 管理;阈值和预算在 [`config.ts`](config.ts)。资格变化不删除 Memory 中的人物档案。具体条件见「重要观众与实时限流」。
|
|
10
|
+
|
|
11
|
+
## 分层
|
|
12
|
+
|
|
13
|
+
| 文件 | 职责 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| [`wire.ts`](wire.ts) | 16 字节帧头的编解码;`op=5` 且 protover 2/3 时包体是压缩过的另一串完整包,解析是递归的 |
|
|
16
|
+
| [`client.ts`](client.ts) | 握手四步 + wss 长连 + 30s 心跳 + 退避重连 |
|
|
17
|
+
| [`normalize.ts`](normalize.ts) | 原始 cmd → 归一化产物(事件 / 计数 / 读数 / 不推)。纯函数 |
|
|
18
|
+
| [`protobuf.ts`](protobuf.ts) | 按字段号与 wire type 读取 protobuf;无法解析时返回 null |
|
|
19
|
+
| [`gift-frame.ts`](gift-frame.ts) | `SEND_GIFT_V2` 的 pb blob → V1 字段形状。两种帧共用下游 |
|
|
20
|
+
| [`coalescing-buffer.ts`](coalescing-buffer.ts) | 相同弹幕与常规礼物进入事件总线前的固定窗归并 |
|
|
21
|
+
| [`audience-admission.ts`](audience-admission.ts) | 按 UID 记录优先资格,以高能榜和批次预算决定是否筛选,并分配三类事件的上下文额度 |
|
|
22
|
+
| [`world.ts`](world.ts) | 投递分档、人流聚合、公告工具、控制台数据面与 Overlay 接线 |
|
|
23
|
+
| [`overlay/`](overlay/) | 设计模型、用户组匹配、原始事件显示、素材、公告持久化、loopback HTTP/SSE、运行页与独立编辑器 |
|
|
24
|
+
| [`console/`](console/) | 直播间入站诊断 `log`;Overlay 编辑入口由 World 链接提供 |
|
|
25
|
+
|
|
26
|
+
帧压缩使用 `node:zlib`,WBI 签名使用 `node:crypto` 的 md5,WebSocket 使用 `ws`。protobuf 读取器按已核对的字段编号解析;本项目没有官方 `.proto` schema。
|
|
27
|
+
|
|
28
|
+
## 接入协议
|
|
29
|
+
|
|
30
|
+
本 World 使用 B 站 web 直播协议,按返回的 uid 识别观众。该接口非官方开放平台接口,字段可能变化,接入也可能受风控限制。
|
|
31
|
+
|
|
32
|
+
## 送礼帧:V1 与 V2 并存
|
|
33
|
+
|
|
34
|
+
`SEND_GIFT` 使用 V1 JSON 字段;`SEND_GIFT_V2` 的 `data.pb` 是 base64 编码的 protobuf,`data` 还可含 `dmscore`。[`gift-frame.ts`](gift-frame.ts) 将 V2 解码为 V1 字段结构,包括 `giftName`、`num`、`coin_type`、`total_coin`、`uname` 与 `sender_uinfo`。帧中已有 V1 字段时沿用 V1 路径。
|
|
35
|
+
|
|
36
|
+
字段编号没有官方 schema,映射来自 `bilibili-raw-samples.jsonl` 的实际接收帧。回归夹具位于 [`tests/worlds/bilibili/gift-v2-frames.ts`](../../../tests/worlds/bilibili/gift-v2-frames.ts),保留原始布局并替换身份字段。V1 接收帧和同结构夹具也用于回放与测试。
|
|
37
|
+
|
|
38
|
+
**读取失败不能降级为计数。** 只有明确读出 `coin_type` 且不是 gold 时才计为免费礼物;读取失败仍生成事件,正文省略金额并告警。
|
|
39
|
+
|
|
40
|
+
握手四步(全在 `client.ts`):
|
|
41
|
+
|
|
42
|
+
1. `x/frontend/finger/spi` 取非空 `buvid3`
|
|
43
|
+
2. `x/web-interface/nav` 取 WBI 密钥与登录 uid
|
|
44
|
+
3. `Room/get_info` 将短房间号转换为认证包使用的真实房间号
|
|
45
|
+
4. `getDanmuInfo`(WBI 签名)拿弹幕服务器列表与 token
|
|
46
|
+
|
|
47
|
+
## 登录凭证与观众身份
|
|
48
|
+
|
|
49
|
+
`worlds.bilibili.sessdata` 保存浏览器登录后的 SESSDATA cookie,位于部署的 `config.json`,不进入版本控制。匿名连接仍可接收弹幕;已观测到匿名响应将观众 uid 置为 0,并对昵称脱敏。`uid=0` 时不生成 `senderKey`,昵称不作为稳定身份键。
|
|
50
|
+
|
|
51
|
+
已有弹幕且最近至多 20 条均缺少有效 uid 时,控制台显示「近期弹幕缺少观众 uid」。
|
|
52
|
+
|
|
53
|
+
登录态以服务端返回的 `LiveStatus.selfUid` 为准,0 表示匿名。已配置 `sessdata` 但仍为匿名时,每次接入至多记录一次 error。
|
|
54
|
+
|
|
55
|
+
## 接入日志
|
|
56
|
+
|
|
57
|
+
- **归并统计**:`info 直播间归并折叠` 按分钟汇总 `folds`、`sourceItems` 和 `windowMs`,停机时记录剩余不足一分钟的统计。
|
|
58
|
+
- **筛选统计**:`warn 直播间准入筛除` 逐批记录筛除量、候选量、限流状态与本场累计量。
|
|
59
|
+
- **存储失败**:`warn B站观众准入账本落盘失败` 由 `onPersistError` 报告。
|
|
60
|
+
|
|
61
|
+
## 投递分档
|
|
62
|
+
|
|
63
|
+
事件按 `trigger` 决定是否唤醒以及是否参与合批。人流读数在投递时生成正文。
|
|
64
|
+
|
|
65
|
+
| 事件 | 唤醒 | 说明 |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `bilibili.superchat` | flush | 醒目留言 |
|
|
68
|
+
| `bilibili.guard` | flush | 上舰 |
|
|
69
|
+
| `bilibili.gift` | flush / debounce | 金额 ≥ `worlds.bilibili.giftFlushYuan` 走 flush,低于它先过短窗归并再排常规合批 |
|
|
70
|
+
| `bilibili.guard-renew` | debounce | 续费 |
|
|
71
|
+
| `bilibili.danmaku` | debounce | 弹幕 |
|
|
72
|
+
| `bilibili.enter-guard` | debounce | 舰长进场(普通进场只计数) |
|
|
73
|
+
| `bilibili.block` | debounce | 观众被禁言 |
|
|
74
|
+
| `bilibili.room` | flush / debounce | 开播、下播、全员禁言走 flush;标题变更 debounce |
|
|
75
|
+
| `bilibili.feed` | flush / debounce | 弹幕接入中断超过 60 秒成文一次(flush),恢复时补一条带中断时长(debounce);60 秒内的抖动不推 |
|
|
76
|
+
| `bilibili.warning` | flush | 超管警告与切断;World 仅投递事件 |
|
|
77
|
+
| `bilibili.superchat-del` | piggyback | |
|
|
78
|
+
| `bilibili.audience` | piggyback + **投递成文** | 人流读数 |
|
|
79
|
+
|
|
80
|
+
进场、点赞和免费礼物累计次数;看过、在线、人气与粉丝数记录接收时的读数。它们在 World 内累积,经 `pushDeferred` 随下一批事件投递,并在渲染正文后清零。待投递期间不重复登记;超过 5 分钟未渲染时允许重新登记。
|
|
81
|
+
|
|
82
|
+
`normalize.ts` 的 `IGNORED` 列出不投递的 cmd,包括运营挂件、连麦玩法、全站广播及自身语音转写。未识别的 cmd 仍按原名称计数,并显示在控制台。
|
|
83
|
+
|
|
84
|
+
## 相同消息归并
|
|
85
|
+
|
|
86
|
+
弹幕与低于插队门槛的付费礼物在写事件库和总线前经过固定窗。窗口默认 300ms,从池内首件
|
|
87
|
+
起算且不因后续消息续期;累计到 64 条立即冲刷。任何不能归并的事件与 `flush` 都是顺序屏障:
|
|
88
|
+
先按各组首件的到达顺序冲刷旧池,再立即写入该事件。停机同样先冲刷并取消定时器。
|
|
89
|
+
|
|
90
|
+
弹幕以「稳定 UID + 去掉首尾空白后的正文」逐字匹配,大小写、内部空白和 Unicode 形式均不改写;**不同观众发同一句话不合并**。礼物只合并礼物流中连续出现的同一稳定 UID、同一礼物名和单价,累计笔数、件数与金额;另一位送礼人或另一种礼物结束该连续段,普通弹幕不打断礼物段。无稳定 UID 的礼物逐笔投递,不凭昵称猜身份。匿名接入时弹幕没有稳定身份键,按正文合并。
|
|
91
|
+
单件仍使用原正文、昵称和 `senderKey`;两件以上写成 `[弹幕×N|昵称] 正文`;礼物写成 `[礼物×N笔 ¥合计|昵称] 礼物×总件数`,保留送礼人姓名与组级 `senderKey`。昵称必须进入归并正文,因为私有 `meta` 不渲染给 agent。
|
|
92
|
+
参与者的稳定 `senderKey`、昵称与原始条数保留在组级私有 `meta`,供人物档案逐人召回。Overlay 与控制台日志在原始消息到达时逐条更新,不等待归并窗。
|
|
93
|
+
|
|
94
|
+
控制台 `log.state.coalescing` 报告输入量、输出量、已合并量、冲刷次数、容量冲刷次数与当前池内条数。`worlds.bilibili.coalesceWindowMs` 和 `worlds.bilibili.coalesceMaxItems` 支持热改;窗口设为 0 时关闭等待,逐条输出。
|
|
95
|
+
|
|
96
|
+
## 重要观众与实时限流
|
|
97
|
+
|
|
98
|
+
优先资格只使用稳定 UID,满足任一条件即可获得:舰队身份的正向记录保留 35 天;单条 SC 不低于 ¥30,或 30 天内累计不低于 ¥50;30 天内至少一场直播有 25 个去重活跃分钟。互动所需分钟数与场次数可配置。SC 与互动资格自命中起保留 30 天。
|
|
99
|
+
|
|
100
|
+
账本位于 `data/bilibili-audience/ledger.json`,记录 UID、时间、金额、活跃分钟和用于跨批分配的等待计数(`fairDebt`);不存昵称或消息正文。记录在归并和筛选前更新。脏账本每 30 秒至多保存一次,停机强制保存;运行期保存失败时报警,保留未保存状态并在后续更新时重试。
|
|
101
|
+
|
|
102
|
+
高能榜用于判断拥挤状态,不等同于在线人数。默认达到 200 时进入拥挤态,连续 120 秒不高于 170 时退出;信号超过 300 秒未更新后不再维持拥挤态,下播或切换直播代次时清零。只有拥挤态且同批候选超过 114 行或 1,061 个估算 token 时才筛选,否则全部进入上下文。
|
|
103
|
+
|
|
104
|
+
筛选时分为三组:
|
|
105
|
+
|
|
106
|
+
- `critical`:平台警告、开关播、SC、上舰/续费与达到即时投递金额门槛的礼物全部保留,可超过批次预算。
|
|
107
|
+
- `important`:任一参与者具有优先资格的合并组整体归入此组。按跨批等待计数分配,最多使用所配置行数和 token 预算的 50%,同时受全批剩余额度限制。
|
|
108
|
+
- `ordinary`:其余事件按持久保存的私有盐确定性抽样,使用剩余额度。
|
|
109
|
+
|
|
110
|
+
原始事件均以 `archive-only` 保存,供历史查询;筛选结果进入 Agent 上下文。控制台报告高能榜、拥挤与限流状态、重要观众数和累计筛除量。
|
|
111
|
+
|
|
112
|
+
## Overlay
|
|
113
|
+
|
|
114
|
+
World 在 `127.0.0.1` 启动独立页面:`GET /overlay` 是 OBS browser source,`GET /editor`
|
|
115
|
+
是编辑器,`GET /stream` 是同源 SSE 数据面,`GET /assets/<id>` 提供上传素材。端口默认 7795,
|
|
116
|
+
被占用时顺延,含配置端口最多尝试 5 个端口,并记录 warn。
|
|
117
|
+
编辑接口只接受浏览器携带的同源 `Origin` 与 JSON 请求,并限制请求体大小;
|
|
118
|
+
服务不发送 wildcard CORS。设计和公告分别使用修订号做冲突检测,素材、设计与公告写入共用串行队列,
|
|
119
|
+
避免保存过程中删除素材或后完成的旧请求覆盖新状态。
|
|
120
|
+
|
|
121
|
+
组件类型:
|
|
122
|
+
|
|
123
|
+
- 横向或纵向弹幕机,准入可选弹幕、礼物、弹幕+礼物;用户名与内容可分别限制显示字数,礼物包含免费礼物、付费礼物、SC 与舰队事件。
|
|
124
|
+
- 横向或纵向滚动公告、固定公告、Agent 公告;多行滚动公告可设置每行停留时间和行间切换时长,滚动内容在运动方向的两端渐隐。
|
|
125
|
+
- HTTPS/HTTP 外部图片或上传图片,支持 contain/cover/fill。
|
|
126
|
+
|
|
127
|
+
组件可带独立标题。标题占据上、右、下或左侧边带,支持起点、居中、终点对齐,并拥有独立的
|
|
128
|
+
字体、字号、字重、RGBA 颜色与描边。Agent 公告更新时按 Unicode 字符逐字显示并保留输入光标;
|
|
129
|
+
弹幕机 Mock 会持续混合注入弹幕和礼物,组件工作区的 Mock 测试只写入预览运行器,不改设计、
|
|
130
|
+
真实公告或直播事件流。
|
|
131
|
+
|
|
132
|
+
内置样式为天蓝色、海军蓝、白色与极简,全部默认尖角。自定义样式可分别配置用户名和正文的
|
|
133
|
+
字体、字号、字重、RGBA 颜色与描边。Nine-slice 将源图四条切线与输出边框宽度分开保存;
|
|
134
|
+
编辑器可在源图上拖动切线,并用可缩放的实际组件预览验证拉伸、平铺和中心填充。
|
|
135
|
+
|
|
136
|
+
用户组规则是递归 `all`/`any` 组合。可匹配 uid、舰队等级、粉丝牌、房管、VIP/SVIP、
|
|
137
|
+
用户等级以及实测弹幕结构中的排名和颜色字段;最高优先级的匹配组覆写用户名与正文样式。
|
|
138
|
+
头像优先使用消息携带的 `sender_uinfo`/富用户信息,经典弹幕没有头像时显示昵称首字占位。
|
|
139
|
+
|
|
140
|
+
设计保存在部署配置 `worlds.bilibili.overlay.design`,由装配层的 `onOverlayConfig` 原子写回。
|
|
141
|
+
上传素材位于 `data/bilibili-overlay/assets/`,文件名是内容哈希;只接受经魔数确认的 PNG、JPEG、
|
|
142
|
+
WebP 与 GIF,单文件上限 8 MiB。Agent 公告单独原子保存到
|
|
143
|
+
`data/bilibili-overlay/agent-notice.json`,工具写入后以独立 SSE 事件更新公告节点,不会清空
|
|
144
|
+
正在滚动的弹幕。公告内容在下一次主会话前缀重建时通过 `ENV_PROMPT.md` 占位符同步; World 不会
|
|
145
|
+
为了每次公告写入强制重建前缀。
|
|
146
|
+
|
|
147
|
+
## 控制台
|
|
148
|
+
|
|
149
|
+
控制台提供直播间事件诊断和独立 Overlay 编辑器入口。样式、组件与布局编辑共享草稿、撤销历史和保存操作。草稿预览复用 OBS 渲染器,支持拖动、八向缩放、网格、画布与组件吸附、层级、可见性和锁定;编辑控件与选择框不进入 `/overlay` 输出。
|
|
150
|
+
|
|
151
|
+
从主 Web 页面打开编辑器时,语义调色板通过 URL fragment 传递。编辑器内置浅色和深色默认调色板,主 Web 停止后仍可通过独立 URL 打开。画布背景的浅深设置只保存在本机,不写入设计数据。
|
|
152
|
+
|
|
153
|
+
配置项包括直播间号、登录凭证、礼物即时投递门槛、相同消息归并窗与容量上限,以及 Overlay
|
|
154
|
+
启用、端口和 Agent 公告字数上限。
|
|
155
|
+
|
|
156
|
+
日志面板每 2 秒轮询 `log.state`。`total` 是累计记录数,包括已超出 `RECENT_CAP` 而从缓冲移除的记录;客户端按差值追加新行,保留滚动位置。
|
|
157
|
+
|
|
158
|
+
目录名 `worlds/bilibili` 的末段必须与 World id `bilibili` 对得上:浏览器产物的 asset key 由目录名推导
|
|
159
|
+
(`src/worlds/<x>/console/client.ts` → `world:<x>`),而控制台按 provider id `world:bilibili` 去取它。
|
|
160
|
+
两边对不上时页面上出现的是"声明了面板,但没有构建出浏览器扩展"。
|
|
161
|
+
|
|
162
|
+
## 字段待校
|
|
163
|
+
|
|
164
|
+
以下 cmd 尚未用实际接收帧验证,字段形状来自社区文档:
|
|
165
|
+
`ROOM_BLOCK_MSG`(禁言)、`ROOM_SILENT_ON/OFF`、`WARNING`、`CUT_OFF`。
|
|
166
|
+
|
|
167
|
+
已用接收帧核对的 cmd 包括 `DANMU_MSG`、`USER_TOAST_MSG_V2`、`INTERACT_WORD_V2`、`LIKE_INFO_V3_*`、`WATCHED_CHANGE`、
|
|
168
|
+
`ONLINE_RANK_COUNT`、`ROOM_REAL_TIME_MESSAGE_UPDATE`、`STOP_LIVE_ROOM_LIST`。
|
|
169
|
+
|
|
170
|
+
`INTERACT_WORD_V2` 使用 protobuf;当前解析未区分进场、关注和分享,均计为进场。
|
|
171
|
+
|
|
172
|
+
## 接入限制
|
|
173
|
+
|
|
174
|
+
- 握手失败后按指数退避重连,等待从 2 秒起加倍,最多 60 秒。状态轮询遇到 HTTP 412 时将下一次轮询延后 10 分钟。412 / -352 作为风控错误报告。
|
|
175
|
+
- 连接看门狗在平台开播时检查 `connecting`,以及没有重试定时器的 `retrying` 状态;持续 60 秒无结果后重新连接。它不检查 `connected` 状态下长期收不到业务消息的情况。
|
|
176
|
+
- Web 协议字段可能变化;`IGNORED` 之外的新 cmd 会显示在控制台计数中。
|