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,494 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OVERLAY_SCHEMA_VERSION,
|
|
3
|
+
type AudienceCompare,
|
|
4
|
+
type AudienceField,
|
|
5
|
+
type AudienceGroup,
|
|
6
|
+
type AudienceRule,
|
|
7
|
+
type BilibiliOverlayConfig,
|
|
8
|
+
type BilibiliOverlayDesign,
|
|
9
|
+
type OverlayAudienceFacts,
|
|
10
|
+
type OverlayComponent,
|
|
11
|
+
type OverlayComponentTitle,
|
|
12
|
+
type OverlayPlacement,
|
|
13
|
+
type OverlayStyle,
|
|
14
|
+
type OverlayTextStyle,
|
|
15
|
+
type OverlayTextStylePatch,
|
|
16
|
+
} from './types.ts';
|
|
17
|
+
|
|
18
|
+
const ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,63}$/;
|
|
19
|
+
const COLOR_RE = /^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/;
|
|
20
|
+
const URL_RE = /^https?:\/\//i;
|
|
21
|
+
const AUDIENCE_FIELDS = new Set<AudienceField>([
|
|
22
|
+
'uid',
|
|
23
|
+
'guardLevel',
|
|
24
|
+
'medalLevel',
|
|
25
|
+
'medalName',
|
|
26
|
+
'medalAnchorName',
|
|
27
|
+
'medalRoomId',
|
|
28
|
+
'medalColor',
|
|
29
|
+
'isAdmin',
|
|
30
|
+
'vip',
|
|
31
|
+
'svip',
|
|
32
|
+
'rank',
|
|
33
|
+
'nameColor',
|
|
34
|
+
'userLevel',
|
|
35
|
+
'eventKind',
|
|
36
|
+
]);
|
|
37
|
+
const AUDIENCE_COMPARES = new Set<AudienceCompare>(['eq', 'gte', 'lte', 'exists', 'contains']);
|
|
38
|
+
|
|
39
|
+
const BASE_USERNAME: OverlayTextStyle = {
|
|
40
|
+
fontFamily: 'Microsoft YaHei, sans-serif',
|
|
41
|
+
fontSize: 30,
|
|
42
|
+
fontWeight: 700,
|
|
43
|
+
color: '#ffffffff',
|
|
44
|
+
strokeColor: '#17324dff',
|
|
45
|
+
strokeWidth: 1,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const BASE_BODY: OverlayTextStyle = {
|
|
49
|
+
fontFamily: 'Microsoft YaHei, sans-serif',
|
|
50
|
+
fontSize: 32,
|
|
51
|
+
fontWeight: 500,
|
|
52
|
+
color: '#ffffffff',
|
|
53
|
+
strokeColor: '#17324dff',
|
|
54
|
+
strokeWidth: 1,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const BASE_TITLE: OverlayTextStyle = {
|
|
58
|
+
fontFamily: 'Microsoft YaHei, sans-serif',
|
|
59
|
+
fontSize: 26,
|
|
60
|
+
fontWeight: 700,
|
|
61
|
+
color: '#ffffffff',
|
|
62
|
+
strokeColor: '#17324dff',
|
|
63
|
+
strokeWidth: 0,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const BUILTIN_OVERLAY_STYLES: readonly OverlayStyle[] = [
|
|
67
|
+
{
|
|
68
|
+
id: 'builtin:sky',
|
|
69
|
+
name: '天蓝色',
|
|
70
|
+
background: '#58b8e8dd',
|
|
71
|
+
borderColor: '#d8f5ffff',
|
|
72
|
+
borderWidth: 2,
|
|
73
|
+
radius: 0,
|
|
74
|
+
padding: 16,
|
|
75
|
+
username: { ...BASE_USERNAME, color: '#ffffffff', strokeColor: '#17628bff' },
|
|
76
|
+
body: { ...BASE_BODY, color: '#ffffffff', strokeColor: '#17628bff' },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'builtin:navy',
|
|
80
|
+
name: '海军蓝',
|
|
81
|
+
background: '#0c2340e8',
|
|
82
|
+
borderColor: '#4f83b8ff',
|
|
83
|
+
borderWidth: 2,
|
|
84
|
+
radius: 0,
|
|
85
|
+
padding: 16,
|
|
86
|
+
username: { ...BASE_USERNAME, color: '#7dd3fcff', strokeColor: '#020617ff' },
|
|
87
|
+
body: { ...BASE_BODY, color: '#f8fafcff', strokeColor: '#020617ff' },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'builtin:white',
|
|
91
|
+
name: '白色',
|
|
92
|
+
background: '#fffffff0',
|
|
93
|
+
borderColor: '#d9e2ecff',
|
|
94
|
+
borderWidth: 2,
|
|
95
|
+
radius: 0,
|
|
96
|
+
padding: 16,
|
|
97
|
+
username: { ...BASE_USERNAME, color: '#176b9cff', strokeColor: '#ffffffff', strokeWidth: 0 },
|
|
98
|
+
body: { ...BASE_BODY, color: '#172033ff', strokeColor: '#ffffffff', strokeWidth: 0 },
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'builtin:minimal',
|
|
102
|
+
name: '极简',
|
|
103
|
+
background: '#000000ff',
|
|
104
|
+
borderColor: '#00000000',
|
|
105
|
+
borderWidth: 0,
|
|
106
|
+
radius: 0,
|
|
107
|
+
padding: 0,
|
|
108
|
+
username: { ...BASE_USERNAME, strokeWidth: 0 },
|
|
109
|
+
body: { ...BASE_BODY, strokeWidth: 0 },
|
|
110
|
+
},
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
export const BILIBILI_OVERLAY_DEFAULTS: BilibiliOverlayConfig = {
|
|
114
|
+
enabled: true,
|
|
115
|
+
port: 7795,
|
|
116
|
+
agentNoticeMaxChars: 200,
|
|
117
|
+
design: {
|
|
118
|
+
schemaVersion: OVERLAY_SCHEMA_VERSION,
|
|
119
|
+
canvas: { width: 1920, height: 1080 },
|
|
120
|
+
styles: [],
|
|
121
|
+
groups: [],
|
|
122
|
+
components: [
|
|
123
|
+
{
|
|
124
|
+
id: 'danmaku-main',
|
|
125
|
+
name: '主弹幕机',
|
|
126
|
+
kind: 'danmaku',
|
|
127
|
+
styleId: 'builtin:sky',
|
|
128
|
+
axis: 'vertical',
|
|
129
|
+
admission: 'all',
|
|
130
|
+
showAvatar: true,
|
|
131
|
+
speed: 90,
|
|
132
|
+
gap: 12,
|
|
133
|
+
maxItems: 12,
|
|
134
|
+
usernameMaxChars: 24,
|
|
135
|
+
bodyMaxChars: 80,
|
|
136
|
+
edgeFadePx: 32,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'agent-notice',
|
|
140
|
+
name: 'Agent 公告',
|
|
141
|
+
kind: 'agent-notice',
|
|
142
|
+
styleId: 'builtin:navy',
|
|
143
|
+
emptyText: '公告栏待更新',
|
|
144
|
+
hideWhenEmpty: false,
|
|
145
|
+
typingMs: 42,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
placements: [
|
|
149
|
+
{
|
|
150
|
+
id: 'placement-danmaku-main',
|
|
151
|
+
componentId: 'danmaku-main',
|
|
152
|
+
x: 1240,
|
|
153
|
+
y: 120,
|
|
154
|
+
width: 600,
|
|
155
|
+
height: 760,
|
|
156
|
+
z: 10,
|
|
157
|
+
visible: true,
|
|
158
|
+
locked: false,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'placement-agent-notice',
|
|
162
|
+
componentId: 'agent-notice',
|
|
163
|
+
x: 120,
|
|
164
|
+
y: 60,
|
|
165
|
+
width: 960,
|
|
166
|
+
height: 120,
|
|
167
|
+
z: 20,
|
|
168
|
+
visible: true,
|
|
169
|
+
locked: false,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export function cloneOverlayConfig(value: BilibiliOverlayConfig = BILIBILI_OVERLAY_DEFAULTS): BilibiliOverlayConfig {
|
|
176
|
+
return structuredClone(value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function normalizeOverlayConfig(value: unknown): BilibiliOverlayConfig {
|
|
180
|
+
const raw = object(value);
|
|
181
|
+
return {
|
|
182
|
+
enabled: bool(raw.enabled, BILIBILI_OVERLAY_DEFAULTS.enabled),
|
|
183
|
+
port: integer(raw.port, 0, 65535, BILIBILI_OVERLAY_DEFAULTS.port),
|
|
184
|
+
agentNoticeMaxChars: integer(
|
|
185
|
+
raw.agentNoticeMaxChars,
|
|
186
|
+
1,
|
|
187
|
+
5000,
|
|
188
|
+
BILIBILI_OVERLAY_DEFAULTS.agentNoticeMaxChars,
|
|
189
|
+
),
|
|
190
|
+
design: normalizeOverlayDesign(raw.design),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function normalizeOverlayDesign(value: unknown): BilibiliOverlayDesign {
|
|
195
|
+
if (value === undefined || value === null) return structuredClone(BILIBILI_OVERLAY_DEFAULTS.design);
|
|
196
|
+
const raw = object(value);
|
|
197
|
+
if (Object.hasOwn(raw, 'schemaVersion')) {
|
|
198
|
+
const version = raw.schemaVersion;
|
|
199
|
+
if (typeof version !== 'number' || !Number.isInteger(version) || version < 1 || version > OVERLAY_SCHEMA_VERSION) {
|
|
200
|
+
throw new Error(`Overlay 设计 schemaVersion 无效或不受支持: ${String(version)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const canvas = object(raw.canvas);
|
|
204
|
+
const styles = array(raw.styles).slice(0, 100).map((item, index) => normalizeStyle(item, index));
|
|
205
|
+
uniqueIds(styles, '样式');
|
|
206
|
+
const groups = array(raw.groups).slice(0, 100).map((item, index) => normalizeGroup(item, index));
|
|
207
|
+
uniqueIds(groups, '用户组');
|
|
208
|
+
const components = array(raw.components)
|
|
209
|
+
.slice(0, 200)
|
|
210
|
+
.map((item, index) => normalizeComponent(item, index));
|
|
211
|
+
uniqueIds(components, '组件');
|
|
212
|
+
const placements = array(raw.placements).slice(0, 400).map((item, index) => normalizePlacement(item, index));
|
|
213
|
+
uniqueIds(placements, '布局项');
|
|
214
|
+
|
|
215
|
+
const styleIds = new Set([...BUILTIN_OVERLAY_STYLES.map((item) => item.id), ...styles.map((item) => item.id)]);
|
|
216
|
+
for (const component of components) {
|
|
217
|
+
if (!styleIds.has(component.styleId)) throw new Error(`组件「${component.name}」引用了不存在的样式`);
|
|
218
|
+
}
|
|
219
|
+
const componentIds = new Set(components.map((item) => item.id));
|
|
220
|
+
for (const placement of placements) {
|
|
221
|
+
if (!componentIds.has(placement.componentId)) throw new Error(`布局项引用了不存在的组件 ${placement.componentId}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
schemaVersion: OVERLAY_SCHEMA_VERSION,
|
|
226
|
+
canvas: {
|
|
227
|
+
width: integer(canvas.width, 320, 7680, 1920),
|
|
228
|
+
height: integer(canvas.height, 180, 4320, 1080),
|
|
229
|
+
},
|
|
230
|
+
styles,
|
|
231
|
+
groups,
|
|
232
|
+
components,
|
|
233
|
+
placements,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function matchAudienceGroup(
|
|
238
|
+
groups: readonly AudienceGroup[],
|
|
239
|
+
facts: OverlayAudienceFacts,
|
|
240
|
+
): AudienceGroup | undefined {
|
|
241
|
+
return [...groups]
|
|
242
|
+
.filter((group) => group.enabled && testAudienceRule(group.rule, facts))
|
|
243
|
+
.sort((a, b) => b.priority - a.priority || a.id.localeCompare(b.id))[0];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function testAudienceRule(rule: AudienceRule, facts: OverlayAudienceFacts): boolean {
|
|
247
|
+
if (rule.op === 'all') return rule.rules.every((child) => testAudienceRule(child, facts));
|
|
248
|
+
if (rule.op === 'any') return rule.rules.some((child) => testAudienceRule(child, facts));
|
|
249
|
+
const actual = facts[rule.field];
|
|
250
|
+
if (rule.compare === 'exists') return actual !== undefined && actual !== null && actual !== '';
|
|
251
|
+
if (actual === undefined || actual === null) return false;
|
|
252
|
+
if (rule.compare === 'contains') return String(actual).includes(String(rule.value ?? ''));
|
|
253
|
+
if (rule.compare === 'gte') return Number(actual) >= Number(rule.value);
|
|
254
|
+
if (rule.compare === 'lte') return Number(actual) <= Number(rule.value);
|
|
255
|
+
return actual === rule.value || String(actual) === String(rule.value);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function normalizeStyle(value: unknown, index: number): OverlayStyle {
|
|
259
|
+
const raw = object(value);
|
|
260
|
+
const nine = object(raw.nineSlice);
|
|
261
|
+
const sourceSlice = object(nine.slice);
|
|
262
|
+
const destinationWidth = object(nine.width);
|
|
263
|
+
const assetId = text(nine.assetId, '', 128);
|
|
264
|
+
return {
|
|
265
|
+
id: id(raw.id, `style-${index + 1}`),
|
|
266
|
+
name: text(raw.name, `样式 ${index + 1}`, 80),
|
|
267
|
+
background: color(raw.background, '#58b8e8dd'),
|
|
268
|
+
borderColor: color(raw.borderColor, '#d8f5ffff'),
|
|
269
|
+
borderWidth: number(raw.borderWidth, 0, 64, 2),
|
|
270
|
+
radius: number(raw.radius, 0, 200, 0),
|
|
271
|
+
padding: number(raw.padding, 0, 200, 16),
|
|
272
|
+
username: normalizeTextStyle(raw.username, BASE_USERNAME),
|
|
273
|
+
body: normalizeTextStyle(raw.body, BASE_BODY),
|
|
274
|
+
...(assetId
|
|
275
|
+
? {
|
|
276
|
+
nineSlice: {
|
|
277
|
+
assetId,
|
|
278
|
+
slice: {
|
|
279
|
+
top: integer(sourceSlice.top ?? nine.top, 0, 65535, 20),
|
|
280
|
+
right: integer(sourceSlice.right ?? nine.right, 0, 65535, 20),
|
|
281
|
+
bottom: integer(sourceSlice.bottom ?? nine.bottom, 0, 65535, 20),
|
|
282
|
+
left: integer(sourceSlice.left ?? nine.left, 0, 65535, 20),
|
|
283
|
+
},
|
|
284
|
+
width: {
|
|
285
|
+
top: number(destinationWidth.top ?? nine.top, 0, 512, 20),
|
|
286
|
+
right: number(destinationWidth.right ?? nine.right, 0, 512, 20),
|
|
287
|
+
bottom: number(destinationWidth.bottom ?? nine.bottom, 0, 512, 20),
|
|
288
|
+
left: number(destinationWidth.left ?? nine.left, 0, 512, 20),
|
|
289
|
+
},
|
|
290
|
+
fill: bool(nine.fill, true),
|
|
291
|
+
repeat: nine.repeat === 'repeat' || nine.repeat === 'round' ? nine.repeat : 'stretch',
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
: {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function normalizeTextStyle(value: unknown, fallback: OverlayTextStyle): OverlayTextStyle {
|
|
299
|
+
const raw = object(value);
|
|
300
|
+
return {
|
|
301
|
+
fontFamily: text(raw.fontFamily, fallback.fontFamily, 200),
|
|
302
|
+
fontSize: number(raw.fontSize, 8, 240, fallback.fontSize),
|
|
303
|
+
fontWeight: integer(raw.fontWeight, 100, 900, fallback.fontWeight),
|
|
304
|
+
color: color(raw.color, fallback.color),
|
|
305
|
+
strokeColor: color(raw.strokeColor, fallback.strokeColor),
|
|
306
|
+
strokeWidth: number(raw.strokeWidth, 0, 12, fallback.strokeWidth),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeTextPatch(value: unknown): OverlayTextStylePatch {
|
|
311
|
+
const raw = object(value);
|
|
312
|
+
return {
|
|
313
|
+
...(typeof raw.fontFamily === 'string' ? { fontFamily: text(raw.fontFamily, '', 200) } : {}),
|
|
314
|
+
...(raw.fontSize !== undefined ? { fontSize: number(raw.fontSize, 8, 240, 32) } : {}),
|
|
315
|
+
...(raw.fontWeight !== undefined ? { fontWeight: integer(raw.fontWeight, 100, 900, 500) } : {}),
|
|
316
|
+
...(typeof raw.color === 'string' ? { color: color(raw.color, '#ffffffff') } : {}),
|
|
317
|
+
...(typeof raw.strokeColor === 'string' ? { strokeColor: color(raw.strokeColor, '#000000ff') } : {}),
|
|
318
|
+
...(raw.strokeWidth !== undefined ? { strokeWidth: number(raw.strokeWidth, 0, 12, 0) } : {}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function normalizeGroup(value: unknown, index: number): AudienceGroup {
|
|
323
|
+
const raw = object(value);
|
|
324
|
+
return {
|
|
325
|
+
id: id(raw.id, `group-${index + 1}`),
|
|
326
|
+
name: text(raw.name, `用户组 ${index + 1}`, 80),
|
|
327
|
+
enabled: bool(raw.enabled, true),
|
|
328
|
+
priority: integer(raw.priority, -10000, 10000, 0),
|
|
329
|
+
rule: normalizeRule(raw.rule, 0),
|
|
330
|
+
username: normalizeTextPatch(raw.username),
|
|
331
|
+
body: normalizeTextPatch(raw.body),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function normalizeRule(value: unknown, depth: number): AudienceRule {
|
|
336
|
+
if (depth > 6) throw new Error('用户组规则嵌套不能超过 6 层');
|
|
337
|
+
const raw = object(value);
|
|
338
|
+
if (raw.op === 'all' || raw.op === 'any') {
|
|
339
|
+
const rules = array(raw.rules).slice(0, 32).map((child) => normalizeRule(child, depth + 1));
|
|
340
|
+
if (!rules.length) throw new Error('组合规则至少需要一条子规则');
|
|
341
|
+
return { op: raw.op, rules };
|
|
342
|
+
}
|
|
343
|
+
const field = String(raw.field ?? '') as AudienceField;
|
|
344
|
+
const compare = String(raw.compare ?? '') as AudienceCompare;
|
|
345
|
+
if (!AUDIENCE_FIELDS.has(field) || !AUDIENCE_COMPARES.has(compare)) throw new Error('用户组规则字段或比较方式无效');
|
|
346
|
+
const compareValue = raw.value;
|
|
347
|
+
if (compare !== 'exists' && !['string', 'number', 'boolean'].includes(typeof compareValue)) {
|
|
348
|
+
throw new Error('用户组规则缺少比较值');
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
op: 'leaf',
|
|
352
|
+
field,
|
|
353
|
+
compare,
|
|
354
|
+
...(compare === 'exists' ? {} : { value: compareValue as string | number | boolean }),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function normalizeComponent(value: unknown, index: number): OverlayComponent {
|
|
359
|
+
const raw = object(value);
|
|
360
|
+
const title = normalizeComponentTitle(raw.title);
|
|
361
|
+
const base = {
|
|
362
|
+
id: id(raw.id, `component-${index + 1}`),
|
|
363
|
+
name: text(raw.name, `组件 ${index + 1}`, 80),
|
|
364
|
+
styleId: id(raw.styleId, 'builtin:sky'),
|
|
365
|
+
...(title ? { title } : {}),
|
|
366
|
+
};
|
|
367
|
+
switch (raw.kind) {
|
|
368
|
+
case 'scroll-notice':
|
|
369
|
+
return {
|
|
370
|
+
...base,
|
|
371
|
+
kind: 'scroll-notice',
|
|
372
|
+
axis: raw.axis === 'vertical' ? 'vertical' : 'horizontal',
|
|
373
|
+
text: text(raw.text, '滚动公告', 5000),
|
|
374
|
+
speed: number(raw.speed, 10, 500, 70),
|
|
375
|
+
gap: number(raw.gap, 0, 500, 60),
|
|
376
|
+
lineHoldMs: integer(raw.lineHoldMs, 0, 60000, 1600),
|
|
377
|
+
lineTransitionMs: integer(raw.lineTransitionMs, 0, 10000, 420),
|
|
378
|
+
edgeFadePx: number(raw.edgeFadePx, 0, 512, 32),
|
|
379
|
+
};
|
|
380
|
+
case 'fixed-notice':
|
|
381
|
+
return { ...base, kind: 'fixed-notice', text: text(raw.text, '固定公告', 5000) };
|
|
382
|
+
case 'agent-notice':
|
|
383
|
+
return {
|
|
384
|
+
...base,
|
|
385
|
+
kind: 'agent-notice',
|
|
386
|
+
emptyText: text(raw.emptyText, '公告栏待更新', 500),
|
|
387
|
+
hideWhenEmpty: bool(raw.hideWhenEmpty, false),
|
|
388
|
+
typingMs: integer(raw.typingMs, 0, 2000, 42),
|
|
389
|
+
};
|
|
390
|
+
case 'image':
|
|
391
|
+
return {
|
|
392
|
+
...base,
|
|
393
|
+
kind: 'image',
|
|
394
|
+
source: raw.source === 'upload' ? 'upload' : 'external',
|
|
395
|
+
url: URL_RE.test(String(raw.url ?? '')) ? text(raw.url, '', 2048) : '',
|
|
396
|
+
assetId: text(raw.assetId, '', 128),
|
|
397
|
+
fit: raw.fit === 'cover' || raw.fit === 'fill' ? raw.fit : 'contain',
|
|
398
|
+
opacity: number(raw.opacity, 0, 1, 1),
|
|
399
|
+
};
|
|
400
|
+
case 'danmaku':
|
|
401
|
+
return {
|
|
402
|
+
...base,
|
|
403
|
+
kind: 'danmaku',
|
|
404
|
+
axis: raw.axis === 'horizontal' ? 'horizontal' : 'vertical',
|
|
405
|
+
admission: raw.admission === 'danmaku' || raw.admission === 'gift' ? raw.admission : 'all',
|
|
406
|
+
showAvatar: bool(raw.showAvatar, true),
|
|
407
|
+
speed: number(raw.speed, 10, 500, 90),
|
|
408
|
+
gap: number(raw.gap, 0, 500, 12),
|
|
409
|
+
maxItems: integer(raw.maxItems, 1, 100, 12),
|
|
410
|
+
usernameMaxChars: integer(raw.usernameMaxChars, 0, 5000, 24),
|
|
411
|
+
bodyMaxChars: integer(raw.bodyMaxChars, 0, 5000, 80),
|
|
412
|
+
edgeFadePx: number(raw.edgeFadePx, 0, 512, 32),
|
|
413
|
+
};
|
|
414
|
+
default:
|
|
415
|
+
throw new Error(`组件「${base.name}」类型无效: ${String(raw.kind)}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function normalizeComponentTitle(value: unknown): OverlayComponentTitle | undefined {
|
|
420
|
+
const raw = object(value);
|
|
421
|
+
const titleText = text(raw.text, '', 500);
|
|
422
|
+
if (!titleText) return undefined;
|
|
423
|
+
return {
|
|
424
|
+
text: titleText,
|
|
425
|
+
position: raw.position === 'right' || raw.position === 'bottom' || raw.position === 'left'
|
|
426
|
+
? raw.position
|
|
427
|
+
: 'top',
|
|
428
|
+
align: raw.align === 'center' || raw.align === 'right' ? raw.align : 'left',
|
|
429
|
+
style: normalizeTextStyle(raw.style, BASE_TITLE),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function normalizePlacement(value: unknown, index: number): OverlayPlacement {
|
|
434
|
+
const raw = object(value);
|
|
435
|
+
return {
|
|
436
|
+
id: id(raw.id, `placement-${index + 1}`),
|
|
437
|
+
componentId: id(raw.componentId, ''),
|
|
438
|
+
x: number(raw.x, -7680, 7680, 0),
|
|
439
|
+
y: number(raw.y, -4320, 4320, 0),
|
|
440
|
+
width: number(raw.width, 20, 7680, 600),
|
|
441
|
+
height: number(raw.height, 20, 4320, 200),
|
|
442
|
+
z: integer(raw.z, -10000, 10000, index),
|
|
443
|
+
visible: bool(raw.visible, true),
|
|
444
|
+
locked: bool(raw.locked, false),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function uniqueIds(items: ReadonlyArray<{ id: string }>, label: string): void {
|
|
449
|
+
const ids = new Set<string>();
|
|
450
|
+
for (const item of items) {
|
|
451
|
+
if (ids.has(item.id)) throw new Error(`${label} id 重复: ${item.id}`);
|
|
452
|
+
ids.add(item.id);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function object(value: unknown): Record<string, unknown> {
|
|
457
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
458
|
+
? value as Record<string, unknown>
|
|
459
|
+
: {};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function array(value: unknown): unknown[] {
|
|
463
|
+
return Array.isArray(value) ? value : [];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function id(value: unknown, fallback: string): string {
|
|
467
|
+
const out = typeof value === 'string' ? value.trim() : '';
|
|
468
|
+
if (out && ID_RE.test(out)) return out;
|
|
469
|
+
if (fallback && ID_RE.test(fallback)) return fallback;
|
|
470
|
+
throw new Error('id 只能包含字母、数字、点、冒号、下划线和连字符');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function text(value: unknown, fallback: string, max: number): string {
|
|
474
|
+
return (typeof value === 'string' ? value : fallback).slice(0, max);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function bool(value: unknown, fallback: boolean): boolean {
|
|
478
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function number(value: unknown, min: number, max: number, fallback: number): number {
|
|
482
|
+
const n = typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
483
|
+
return Math.min(max, Math.max(min, n));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function integer(value: unknown, min: number, max: number, fallback: number): number {
|
|
487
|
+
return Math.round(number(value, min, max, fallback));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function color(value: unknown, fallback: string): string {
|
|
491
|
+
if (typeof value !== 'string' || !COLOR_RE.test(value)) return fallback;
|
|
492
|
+
const normalized = value.toLowerCase();
|
|
493
|
+
return normalized.length === 7 ? `${normalized}ff` : normalized;
|
|
494
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { giftFrameData } from '../gift-frame.ts';
|
|
2
|
+
import type { Normalized } from '../normalize.ts';
|
|
3
|
+
import type { OverlayAudienceEvent, OverlayAudienceFacts } from './types.ts';
|
|
4
|
+
|
|
5
|
+
export function projectOverlayEvent(
|
|
6
|
+
msg: Record<string, unknown>,
|
|
7
|
+
normalized: Normalized | null,
|
|
8
|
+
): OverlayAudienceEvent | null {
|
|
9
|
+
const cmd = String(msg.cmd ?? '').split(':')[0];
|
|
10
|
+
if (cmd === 'DANMU_MSG' || cmd === 'DANMU_MSG_MIRROR') return projectDanmaku(msg);
|
|
11
|
+
if (!isGiftCommand(cmd)) return null;
|
|
12
|
+
return projectGift(msg, cmd, normalized);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function projectDanmaku(msg: Record<string, unknown>): OverlayAudienceEvent | null {
|
|
16
|
+
const info = Array.isArray(msg.info) ? msg.info as unknown[] : [];
|
|
17
|
+
const body = string(info[1]).trim();
|
|
18
|
+
if (!body) return null;
|
|
19
|
+
const sender = array(info[2]);
|
|
20
|
+
const medal = array(info[3]);
|
|
21
|
+
const userLevel = array(info[4]);
|
|
22
|
+
const rich = firstObject(
|
|
23
|
+
object(array(info[0])[15]).user,
|
|
24
|
+
object(array(info[0])[15]).user_info,
|
|
25
|
+
object(array(info[0])[15]).sender_uinfo,
|
|
26
|
+
msg.user_info,
|
|
27
|
+
msg.sender_uinfo,
|
|
28
|
+
);
|
|
29
|
+
const base = firstObject(rich.base, rich);
|
|
30
|
+
const uid = positiveNumber(sender[0]) || positiveNumber(object(rich).uid);
|
|
31
|
+
const username = string(sender[1]) || string(object(base).name) || string(object(base).uname) || '某位观众';
|
|
32
|
+
const guardLevel = optionalNumber(info[7]);
|
|
33
|
+
const medalLevel = optionalNumber(medal[0]);
|
|
34
|
+
const userLevelValue = optionalNumber(userLevel[0]);
|
|
35
|
+
const rank = optionalNumber(sender[5]);
|
|
36
|
+
const nameColor = typeof sender[7] === 'string' ? string(sender[7]) : undefined;
|
|
37
|
+
const isAdmin = optionalBoolean(sender[2]);
|
|
38
|
+
const vip = optionalBoolean(sender[3]);
|
|
39
|
+
const svip = optionalBoolean(sender[4]);
|
|
40
|
+
const facts: OverlayAudienceFacts = {
|
|
41
|
+
...(uid ? { uid: String(uid) } : {}),
|
|
42
|
+
...(guardLevel !== undefined ? { guardLevel } : {}),
|
|
43
|
+
...(medalLevel !== undefined ? { medalLevel } : {}),
|
|
44
|
+
...(typeof medal[1] === 'string' ? { medalName: string(medal[1]) } : {}),
|
|
45
|
+
...(typeof medal[2] === 'string' ? { medalAnchorName: string(medal[2]) } : {}),
|
|
46
|
+
...(optionalNumber(medal[3]) !== undefined ? { medalRoomId: optionalNumber(medal[3]) } : {}),
|
|
47
|
+
...(optionalNumber(medal[4]) !== undefined ? { medalColor: optionalNumber(medal[4]) } : {}),
|
|
48
|
+
...(isAdmin !== undefined ? { isAdmin } : {}),
|
|
49
|
+
...(vip !== undefined ? { vip } : {}),
|
|
50
|
+
...(svip !== undefined ? { svip } : {}),
|
|
51
|
+
...(rank !== undefined ? { rank } : {}),
|
|
52
|
+
...(nameColor !== undefined ? { nameColor } : {}),
|
|
53
|
+
...(userLevelValue !== undefined ? { userLevel: userLevelValue } : {}),
|
|
54
|
+
eventKind: 'danmaku',
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
eventKind: 'danmaku',
|
|
58
|
+
username,
|
|
59
|
+
body,
|
|
60
|
+
avatarUrl: faceUrl(rich, base),
|
|
61
|
+
facts,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function projectGift(
|
|
66
|
+
msg: Record<string, unknown>,
|
|
67
|
+
cmd: string,
|
|
68
|
+
normalized: Normalized | null,
|
|
69
|
+
): OverlayAudienceEvent | null {
|
|
70
|
+
// 解析告警由 normalize 负责。
|
|
71
|
+
const data = giftFrameData(object(msg.data));
|
|
72
|
+
const sender = firstObject(data.sender_uinfo, data.user_info);
|
|
73
|
+
const base = firstObject(sender.base, sender);
|
|
74
|
+
const uid = positiveNumber(data.uid) || positiveNumber(object(data.user_info).uid) || positiveNumber(object(sender).uid);
|
|
75
|
+
const username = string(data.uname)
|
|
76
|
+
|| string(data.username)
|
|
77
|
+
|| string(object(data.user_info).uname)
|
|
78
|
+
|| string(object(base).name)
|
|
79
|
+
|| string(object(base).uname)
|
|
80
|
+
|| '某位观众';
|
|
81
|
+
const guardLevel = firstNumber(data.guard_level, sender.guard_level);
|
|
82
|
+
const medalLevel = optionalNumber(object(sender.medal).level);
|
|
83
|
+
const medalName = typeof object(sender.medal).name === 'string' ? string(object(sender.medal).name) : undefined;
|
|
84
|
+
const isAdmin = firstBoolean(sender.is_admin, object(data.user_info).manager);
|
|
85
|
+
const vip = optionalBoolean(sender.vip);
|
|
86
|
+
const svip = optionalBoolean(sender.svip);
|
|
87
|
+
const userLevel = optionalNumber(sender.user_level);
|
|
88
|
+
const facts: OverlayAudienceFacts = {
|
|
89
|
+
...(uid ? { uid: String(uid) } : {}),
|
|
90
|
+
...(guardLevel !== undefined ? { guardLevel } : {}),
|
|
91
|
+
...(medalLevel !== undefined ? { medalLevel } : {}),
|
|
92
|
+
...(medalName !== undefined ? { medalName } : {}),
|
|
93
|
+
...(isAdmin !== undefined ? { isAdmin } : {}),
|
|
94
|
+
...(vip !== undefined ? { vip } : {}),
|
|
95
|
+
...(svip !== undefined ? { svip } : {}),
|
|
96
|
+
...(userLevel !== undefined ? { userLevel } : {}),
|
|
97
|
+
eventKind: 'gift',
|
|
98
|
+
};
|
|
99
|
+
const body = giftBody(cmd, data, normalized);
|
|
100
|
+
if (!body) return null;
|
|
101
|
+
return {
|
|
102
|
+
eventKind: 'gift',
|
|
103
|
+
username,
|
|
104
|
+
body,
|
|
105
|
+
avatarUrl: faceUrl(sender, base, data.user_info),
|
|
106
|
+
facts,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function giftBody(cmd: string, data: Record<string, unknown>, normalized: Normalized | null): string {
|
|
111
|
+
if (cmd === 'SUPER_CHAT_MESSAGE') return `醒目留言 ¥${nonNegativeNumber(data.price)}:${string(data.message)}`;
|
|
112
|
+
if (cmd === 'GUARD_BUY') return `开通 ${string(data.gift_name) || '大航海'} ×${positiveNumber(data.num) || 1}`;
|
|
113
|
+
if (cmd === 'USER_TOAST_MSG' || cmd === 'USER_TOAST_MSG_V2') {
|
|
114
|
+
// toast_msg 明确写明续费时才显示“续费”,否则使用中性措辞。V2 把角色名挪进了 guard_info。
|
|
115
|
+
const rawRole = cmd === 'USER_TOAST_MSG_V2' ? object(data.guard_info).role_name : data.role_name;
|
|
116
|
+
const role = string(rawRole) || '大航海';
|
|
117
|
+
return `${string(data.toast_msg).includes('续费') ? '续费' : '上舰'} ${role}`;
|
|
118
|
+
}
|
|
119
|
+
if (cmd === 'SEND_GIFT' || cmd === 'SEND_GIFT_V2') {
|
|
120
|
+
const gift = string(data.giftName) || string(data.gift_name) || '礼物';
|
|
121
|
+
return `${gift} ×${positiveNumber(data.num) || 1}`;
|
|
122
|
+
}
|
|
123
|
+
return normalized?.kind === 'event' ? normalized.text : '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isGiftCommand(cmd: string): boolean {
|
|
127
|
+
return cmd === 'SUPER_CHAT_MESSAGE'
|
|
128
|
+
|| cmd === 'GUARD_BUY'
|
|
129
|
+
|| cmd === 'USER_TOAST_MSG'
|
|
130
|
+
|| cmd === 'USER_TOAST_MSG_V2'
|
|
131
|
+
|| cmd === 'SEND_GIFT'
|
|
132
|
+
|| cmd === 'SEND_GIFT_V2';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function faceUrl(...values: unknown[]): string {
|
|
136
|
+
for (const value of values) {
|
|
137
|
+
const objValue = object(value);
|
|
138
|
+
for (const candidate of [objValue.face, object(objValue.base).face]) {
|
|
139
|
+
const raw = string(candidate);
|
|
140
|
+
if (raw.startsWith('//')) return `https:${raw}`;
|
|
141
|
+
if (/^https?:\/\//i.test(raw)) return raw;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function firstObject(...values: unknown[]): Record<string, unknown> {
|
|
148
|
+
for (const value of values) {
|
|
149
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
150
|
+
return value as Record<string, unknown>;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function object(value: unknown): Record<string, unknown> {
|
|
157
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
158
|
+
? value as Record<string, unknown>
|
|
159
|
+
: {};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function array(value: unknown): unknown[] {
|
|
163
|
+
return Array.isArray(value) ? value : [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function string(value: unknown): string {
|
|
167
|
+
return typeof value === 'string' ? value : '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function positiveNumber(value: unknown): number {
|
|
171
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function nonNegativeNumber(value: unknown): number {
|
|
175
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function optionalNumber(value: unknown): number | undefined {
|
|
179
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function firstNumber(...values: unknown[]): number | undefined {
|
|
183
|
+
for (const value of values) {
|
|
184
|
+
const number = optionalNumber(value);
|
|
185
|
+
if (number !== undefined) return number;
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function optionalBoolean(value: unknown): boolean | undefined {
|
|
191
|
+
if (typeof value === 'boolean') return value;
|
|
192
|
+
if (value === 0 || value === 1) return value === 1;
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function firstBoolean(...values: unknown[]): boolean | undefined {
|
|
197
|
+
for (const value of values) {
|
|
198
|
+
const boolean = optionalBoolean(value);
|
|
199
|
+
if (boolean !== undefined) return boolean;
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|