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.
Files changed (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +43 -0
  4. package/src/boot.ts +46 -0
  5. package/src/bot.ts +1342 -0
  6. package/src/config-file.ts +41 -0
  7. package/src/core/README.md +92 -0
  8. package/src/core/billing.ts +26 -0
  9. package/src/core/blobs.ts +106 -0
  10. package/src/core/bus.ts +311 -0
  11. package/src/core/config-schema.ts +201 -0
  12. package/src/core/config.ts +262 -0
  13. package/src/core/core.ts +699 -0
  14. package/src/core/cost.ts +244 -0
  15. package/src/core/event-store.ts +304 -0
  16. package/src/core/fork.ts +113 -0
  17. package/src/core/generation.ts +118 -0
  18. package/src/core/instance-lock.ts +215 -0
  19. package/src/core/ipc-logger.ts +105 -0
  20. package/src/core/language.ts +62 -0
  21. package/src/core/log-context.ts +38 -0
  22. package/src/core/loop.ts +1858 -0
  23. package/src/core/markers.ts +48 -0
  24. package/src/core/prefix.ts +121 -0
  25. package/src/core/run.ts +95 -0
  26. package/src/core/secrets.ts +18 -0
  27. package/src/core/session.ts +56 -0
  28. package/src/core/sessions.ts +261 -0
  29. package/src/core/state.ts +104 -0
  30. package/src/core/template.ts +72 -0
  31. package/src/core/timers.ts +129 -0
  32. package/src/core/tool-log.ts +135 -0
  33. package/src/core/transcript.ts +92 -0
  34. package/src/core/truncate.ts +121 -0
  35. package/src/core/types.ts +1140 -0
  36. package/src/core/usage-log.ts +79 -0
  37. package/src/core/util.ts +386 -0
  38. package/src/deploy-listing.ts +160 -0
  39. package/src/deploy.ts +162 -0
  40. package/src/extensions/README.md +72 -0
  41. package/src/extensions/dry-mount.ts +390 -0
  42. package/src/extensions/manifest.ts +153 -0
  43. package/src/extensions/runtime.ts +42 -0
  44. package/src/extensions.ts +519 -0
  45. package/src/launcher.ts +285 -0
  46. package/src/paths.ts +155 -0
  47. package/src/protocol/open-responses/LICENSE +201 -0
  48. package/src/protocol/open-responses/README.md +14 -0
  49. package/src/protocol/open-responses/context-helpers.ts +27 -0
  50. package/src/protocol/open-responses/context-log.ts +59 -0
  51. package/src/protocol/open-responses/context.ts +89 -0
  52. package/src/protocol/open-responses/generated.ts +115 -0
  53. package/src/protocol/open-responses/index.ts +33 -0
  54. package/src/protocol/open-responses/openapi.json +4230 -0
  55. package/src/protocol/open-responses/stream.ts +146 -0
  56. package/src/protocol/open-responses/tokens.ts +31 -0
  57. package/src/providers/README.md +86 -0
  58. package/src/providers/base.ts +111 -0
  59. package/src/providers/configuration.ts +97 -0
  60. package/src/providers/console/config.ts +21 -0
  61. package/src/providers/console/settings.ts +443 -0
  62. package/src/providers/console/strings.ts +74 -0
  63. package/src/providers/console/types.ts +11 -0
  64. package/src/providers/llamacpp/archive.ts +148 -0
  65. package/src/providers/llamacpp/catalog.ts +247 -0
  66. package/src/providers/llamacpp/config.ts +36 -0
  67. package/src/providers/llamacpp/console/client.ts +6 -0
  68. package/src/providers/llamacpp/console/models-panel.ts +120 -0
  69. package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
  70. package/src/providers/llamacpp/console/server.ts +153 -0
  71. package/src/providers/llamacpp/index.ts +95 -0
  72. package/src/providers/llamacpp/native.ts +51 -0
  73. package/src/providers/llamacpp/options.ts +117 -0
  74. package/src/providers/llamacpp/runtime-store.ts +139 -0
  75. package/src/providers/llamacpp/runtime.ts +214 -0
  76. package/src/providers/llamacpp/server.ts +286 -0
  77. package/src/providers/llamacpp/strings.ts +230 -0
  78. package/src/providers/openai-responses-compat/config.ts +20 -0
  79. package/src/providers/openai-responses-compat/index.ts +89 -0
  80. package/src/providers/openai-responses-compat/native.ts +127 -0
  81. package/src/providers/openai-responses-compat/strings.ts +19 -0
  82. package/src/providers/pricebook.ts +75 -0
  83. package/src/providers/registry.ts +137 -0
  84. package/src/providers/strings.ts +71 -0
  85. package/src/providers/transport/chat.ts +156 -0
  86. package/src/providers/transport/errors.ts +48 -0
  87. package/src/providers/transport/history.ts +68 -0
  88. package/src/providers/transport/native-input.ts +83 -0
  89. package/src/providers/transport/native-types.ts +13 -0
  90. package/src/providers/transport/response-assembly.ts +191 -0
  91. package/src/providers/transport/response-http.ts +196 -0
  92. package/src/providers/transport/response-meters.ts +27 -0
  93. package/src/providers/transport/responses-input.ts +56 -0
  94. package/src/web/README.md +84 -0
  95. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
  96. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
  97. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
  98. package/src/web/client/console-pages/builtins.ts +10 -0
  99. package/src/web/client/console-pages/context.ts +135 -0
  100. package/src/web/client/console-pages/host.ts +633 -0
  101. package/src/web/client/console-pages/loader.ts +132 -0
  102. package/src/web/client/console-pages/strings.ts +90 -0
  103. package/src/web/client/console-pages/tools/strings.ts +49 -0
  104. package/src/web/client/console-pages/tools/view.ts +215 -0
  105. package/src/web/client/core/api.ts +200 -0
  106. package/src/web/client/core/language.ts +56 -0
  107. package/src/web/client/core/lifecycle.ts +132 -0
  108. package/src/web/client/core/router.ts +199 -0
  109. package/src/web/client/core/stream.ts +157 -0
  110. package/src/web/client/core/websocket.ts +82 -0
  111. package/src/web/client/features/appearance/index.ts +333 -0
  112. package/src/web/client/features/appearance/strings.ts +87 -0
  113. package/src/web/client/features/config/index.ts +11 -0
  114. package/src/web/client/features/config/strings.ts +41 -0
  115. package/src/web/client/features/config/view.ts +428 -0
  116. package/src/web/client/features/core/events.ts +197 -0
  117. package/src/web/client/features/core/index.ts +254 -0
  118. package/src/web/client/features/core/run.ts +56 -0
  119. package/src/web/client/features/core/runlog.ts +214 -0
  120. package/src/web/client/features/core/sessions.ts +53 -0
  121. package/src/web/client/features/core/strings.ts +157 -0
  122. package/src/web/client/features/extensions/index.ts +422 -0
  123. package/src/web/client/features/extensions/strings.ts +164 -0
  124. package/src/web/client/features/feature.ts +93 -0
  125. package/src/web/client/features/live/context.ts +280 -0
  126. package/src/web/client/features/live/fork.ts +156 -0
  127. package/src/web/client/features/live/index.ts +392 -0
  128. package/src/web/client/features/live/onboarding.ts +113 -0
  129. package/src/web/client/features/live/protocol.ts +122 -0
  130. package/src/web/client/features/live/sessions.ts +71 -0
  131. package/src/web/client/features/live/status.ts +68 -0
  132. package/src/web/client/features/live/strings.ts +252 -0
  133. package/src/web/client/features/live/timeline.ts +590 -0
  134. package/src/web/client/features/prompts/editor.ts +292 -0
  135. package/src/web/client/features/prompts/index.ts +346 -0
  136. package/src/web/client/features/prompts/strings.ts +136 -0
  137. package/src/web/client/features/prompts/view.ts +256 -0
  138. package/src/web/client/features/providers/index.ts +100 -0
  139. package/src/web/client/features/providers/strings.ts +23 -0
  140. package/src/web/client/features/settings/general.ts +28 -0
  141. package/src/web/client/features/settings/index.ts +82 -0
  142. package/src/web/client/features/settings/strings.ts +33 -0
  143. package/src/web/client/features/storage/index.ts +1 -0
  144. package/src/web/client/features/storage/strings.ts +45 -0
  145. package/src/web/client/features/storage/view.ts +154 -0
  146. package/src/web/client/features/usage/chart.ts +486 -0
  147. package/src/web/client/features/usage/index.ts +407 -0
  148. package/src/web/client/features/usage/labels.ts +46 -0
  149. package/src/web/client/features/usage/range.ts +36 -0
  150. package/src/web/client/features/usage/state.ts +37 -0
  151. package/src/web/client/features/usage/strings.ts +191 -0
  152. package/src/web/client/features/usage/tooltip.ts +86 -0
  153. package/src/web/client/features/usage/types.ts +96 -0
  154. package/src/web/client/features/worlds/index.ts +424 -0
  155. package/src/web/client/features/worlds/strings.ts +121 -0
  156. package/src/web/client/main.ts +273 -0
  157. package/src/web/client/shell/avatar.ts +233 -0
  158. package/src/web/client/shell/index.ts +549 -0
  159. package/src/web/client/shell/strings.ts +89 -0
  160. package/src/web/client/theme/handoff.ts +43 -0
  161. package/src/web/client/theme/palette.ts +109 -0
  162. package/src/web/client/theme/registry.ts +130 -0
  163. package/src/web/client/theme/storage.ts +72 -0
  164. package/src/web/client/theme/strings.ts +123 -0
  165. package/src/web/client/theme/studio.ts +366 -0
  166. package/src/web/client/ui/actions.ts +109 -0
  167. package/src/web/client/ui/data.ts +185 -0
  168. package/src/web/client/ui/dom.ts +23 -0
  169. package/src/web/client/ui/fields.ts +191 -0
  170. package/src/web/client/ui/format.ts +75 -0
  171. package/src/web/client/ui/icons.ts +183 -0
  172. package/src/web/client/ui/images.ts +91 -0
  173. package/src/web/client/ui/index.ts +89 -0
  174. package/src/web/client/ui/lamp.ts +125 -0
  175. package/src/web/client/ui/log.ts +98 -0
  176. package/src/web/client/ui/overlay.ts +184 -0
  177. package/src/web/client/ui/page.ts +8 -0
  178. package/src/web/client/ui/prompt-input.tsx +311 -0
  179. package/src/web/client/ui/sheet.ts +131 -0
  180. package/src/web/client/ui/strings.ts +71 -0
  181. package/src/web/console-pages.ts +367 -0
  182. package/src/web/files.ts +107 -0
  183. package/src/web/path-picker.ts +296 -0
  184. package/src/web/public/index.html +17 -0
  185. package/src/web/public/styles.css +1715 -0
  186. package/src/web/server.ts +1883 -0
  187. package/src/web/shared/client-panel.ts +603 -0
  188. package/src/web/shared/console-protocol.ts +560 -0
  189. package/src/web/shared/css.d.ts +1 -0
  190. package/src/web/shared/path-picker.ts +23 -0
  191. package/src/web/shared/theme.ts +256 -0
  192. package/src/web/theme-store.ts +37 -0
  193. package/src/world.ts +449 -0
  194. package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
  195. package/src/worlds/bilibili/README.md +176 -0
  196. package/src/worlds/bilibili/audience-admission.ts +995 -0
  197. package/src/worlds/bilibili/client.ts +528 -0
  198. package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
  199. package/src/worlds/bilibili/config.ts +185 -0
  200. package/src/worlds/bilibili/console/client.ts +202 -0
  201. package/src/worlds/bilibili/definition.ts +35 -0
  202. package/src/worlds/bilibili/gift-frame.ts +131 -0
  203. package/src/worlds/bilibili/normalize.ts +452 -0
  204. package/src/worlds/bilibili/overlay/announcement.ts +76 -0
  205. package/src/worlds/bilibili/overlay/assets.ts +82 -0
  206. package/src/worlds/bilibili/overlay/model.ts +494 -0
  207. package/src/worlds/bilibili/overlay/project.ts +202 -0
  208. package/src/worlds/bilibili/overlay/server.ts +286 -0
  209. package/src/worlds/bilibili/overlay/types.ts +217 -0
  210. package/src/worlds/bilibili/overlay/web/app.js +690 -0
  211. package/src/worlds/bilibili/overlay/web/editor.css +297 -0
  212. package/src/worlds/bilibili/overlay/web/editor.html +102 -0
  213. package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
  214. package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
  215. package/src/worlds/bilibili/overlay/web/styles.css +258 -0
  216. package/src/worlds/bilibili/protobuf.ts +123 -0
  217. package/src/worlds/bilibili/wire.ts +94 -0
  218. package/src/worlds/bilibili/world.ts +1389 -0
  219. package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
  220. package/src/worlds/console-fixture/console/client.ts +72 -0
  221. package/src/worlds/console-fixture/world.ts +73 -0
  222. package/src/worlds/index.ts +15 -0
  223. package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
  224. package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
  225. package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
  226. package/src/worlds/minecraft/README.md +917 -0
  227. package/src/worlds/minecraft/blueprint-plan.ts +915 -0
  228. package/src/worlds/minecraft/blueprint-registry.ts +566 -0
  229. package/src/worlds/minecraft/blueprint-repair.ts +236 -0
  230. package/src/worlds/minecraft/blueprint-resource.ts +272 -0
  231. package/src/worlds/minecraft/blueprint.ts +492 -0
  232. package/src/worlds/minecraft/body-lease.ts +340 -0
  233. package/src/worlds/minecraft/bridge.ts +749 -0
  234. package/src/worlds/minecraft/check.ts +651 -0
  235. package/src/worlds/minecraft/chests.ts +292 -0
  236. package/src/worlds/minecraft/client-launch.ts +272 -0
  237. package/src/worlds/minecraft/client-options.ts +85 -0
  238. package/src/worlds/minecraft/client-skins.ts +164 -0
  239. package/src/worlds/minecraft/client-window.ps1 +91 -0
  240. package/src/worlds/minecraft/client.ts +423 -0
  241. package/src/worlds/minecraft/combat-context.ts +109 -0
  242. package/src/worlds/minecraft/combat.ts +1552 -0
  243. package/src/worlds/minecraft/config.ts +499 -0
  244. package/src/worlds/minecraft/console/access.ts +218 -0
  245. package/src/worlds/minecraft/console/client.ts +244 -0
  246. package/src/worlds/minecraft/console/log.ts +110 -0
  247. package/src/worlds/minecraft/console/mount.ts +420 -0
  248. package/src/worlds/minecraft/console/skin.ts +257 -0
  249. package/src/worlds/minecraft/console/style.css +154 -0
  250. package/src/worlds/minecraft/console/world.ts +449 -0
  251. package/src/worlds/minecraft/deaths.ts +135 -0
  252. package/src/worlds/minecraft/definition.ts +18 -0
  253. package/src/worlds/minecraft/engine-child.ts +274 -0
  254. package/src/worlds/minecraft/engine-ipc.ts +98 -0
  255. package/src/worlds/minecraft/entity-facts.ts +132 -0
  256. package/src/worlds/minecraft/escape.ts +407 -0
  257. package/src/worlds/minecraft/executor.ts +14494 -0
  258. package/src/worlds/minecraft/explored.ts +156 -0
  259. package/src/worlds/minecraft/geometry.ts +205 -0
  260. package/src/worlds/minecraft/goal-plan.ts +647 -0
  261. package/src/worlds/minecraft/item-break.ts +126 -0
  262. package/src/worlds/minecraft/item-facts.ts +146 -0
  263. package/src/worlds/minecraft/item-pick.ts +67 -0
  264. package/src/worlds/minecraft/level-dat.ts +176 -0
  265. package/src/worlds/minecraft/log.ts +132 -0
  266. package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
  267. package/src/worlds/minecraft/names.ts +377 -0
  268. package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
  269. package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
  270. package/src/worlds/minecraft/piglin.ts +49 -0
  271. package/src/worlds/minecraft/policy.ts +547 -0
  272. package/src/worlds/minecraft/precheck.ts +655 -0
  273. package/src/worlds/minecraft/proxy.ts +445 -0
  274. package/src/worlds/minecraft/ranged.ts +635 -0
  275. package/src/worlds/minecraft/readouts.ts +102 -0
  276. package/src/worlds/minecraft/round.ts +26 -0
  277. package/src/worlds/minecraft/search-observation.ts +92 -0
  278. package/src/worlds/minecraft/server-config.ts +467 -0
  279. package/src/worlds/minecraft/server.ts +658 -0
  280. package/src/worlds/minecraft/show.ts +84 -0
  281. package/src/worlds/minecraft/skills.ts +1763 -0
  282. package/src/worlds/minecraft/terrain.ts +1735 -0
  283. package/src/worlds/minecraft/window.ts +96 -0
  284. package/src/worlds/minecraft/works.ts +179 -0
  285. package/src/worlds/minecraft/world.ts +6438 -0
  286. package/src/worlds/qq/ENV_PROMPT.md +5 -0
  287. package/src/worlds/qq/config.ts +108 -0
  288. package/src/worlds/qq/console/client.ts +126 -0
  289. package/src/worlds/qq/console/events.ts +155 -0
  290. package/src/worlds/qq/console/gate.ts +232 -0
  291. package/src/worlds/qq/console/roster.ts +203 -0
  292. package/src/worlds/qq/console/style.css +65 -0
  293. package/src/worlds/qq/conversation.ts +28 -0
  294. package/src/worlds/qq/definition.ts +62 -0
  295. package/src/worlds/qq/driver.ts +375 -0
  296. package/src/worlds/qq/history-tools.ts +209 -0
  297. package/src/worlds/qq/normalize.ts +323 -0
  298. package/src/worlds/qq/qface-map.ts +282 -0
  299. package/src/worlds/qq/vision-prompt.ts +27 -0
  300. package/src/worlds/qq/vision.ts +642 -0
  301. package/src/worlds/qq/vlm.ts +142 -0
  302. package/src/worlds/qq/world.ts +1503 -0
  303. package/src/worlds/terminal/ENV_PROMPT.md +11 -0
  304. package/src/worlds/terminal/config.ts +9 -0
  305. package/src/worlds/terminal/definition.ts +14 -0
  306. package/src/worlds/terminal/world.ts +700 -0
  307. package/src/worlds/websearch/ENV_PROMPT.md +1 -0
  308. package/src/worlds/websearch/brave-client.ts +261 -0
  309. package/src/worlds/websearch/config.ts +75 -0
  310. package/src/worlds/websearch/definition.ts +20 -0
  311. 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
+ }