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,452 @@
1
+ /** 将原始 cmd 转换为事件、累加计数或最新读数;不处理的命令返回 null。计数与读数由 World 聚合后随批次投递。 */
2
+ import type { TriggerMode } from '../../core/types.ts';
3
+ import { giftFrameData } from './gift-frame.ts';
4
+
5
+ export interface LiveEvent {
6
+ kind: 'event';
7
+
8
+ type: string;
9
+ trigger: TriggerMode;
10
+ text: string;
11
+ senderKey?: string;
12
+ meta?: Record<string, unknown>;
13
+ coalesce?: LiveEventCoalescing;
14
+ }
15
+
16
+ type LiveEventCoalescing =
17
+ | { kind: 'danmaku'; key: string; body: string }
18
+ /** yuan 为 null 时金额未知,合并正文省略金额。 */
19
+ | { kind: 'gift'; key: string; gift: string; num: number; yuan: number | null };
20
+
21
+ /** 期间累加的次数 */
22
+ export type CountField = 'enter' | 'like' | 'freeGift';
23
+ /** 当前值,只留最新 */
24
+ export type GaugeField = 'watched' | 'online' | 'popularity' | 'fans' | 'likeTotal';
25
+
26
+ interface LiveCount {
27
+ kind: 'count';
28
+ field: CountField;
29
+ by: number;
30
+ }
31
+
32
+ interface LiveGauge {
33
+ kind: 'gauge';
34
+ field: GaugeField;
35
+ value: number;
36
+ }
37
+
38
+ export type Normalized = LiveEvent | LiveCount | LiveGauge;
39
+
40
+ /** 1 元 = 1000 金瓜子 */
41
+ const COIN_PER_YUAN = 1000;
42
+
43
+ const IGNORED = new Set([
44
+ 'COMBO_SEND',
45
+ 'COMBO_END',
46
+ 'NOTICE_MSG',
47
+ 'SYS_MSG',
48
+ 'WIDGET_BANNER',
49
+ 'RECOMMEND_CARD',
50
+ 'GOTO_BUY_FLOW',
51
+ 'STOP_LIVE_ROOM_LIST',
52
+ 'HOT_ROOM_NOTIFY',
53
+ 'POPULAR_RANK_CHANGED',
54
+ 'ONLINE_RANK_V3',
55
+ 'ONLINE_RANK_TOP3',
56
+ 'UNIVERSAL_ASR_TEXT',
57
+ 'DM_INTERACTION',
58
+ 'LOG_IN_NOTICE',
59
+ 'ENTRY_EFFECT_MUST_RECEIVE',
60
+ ]);
61
+
62
+ export interface NormalizeOptions {
63
+ /** 礼物提到 flush 的门槛(元) */
64
+ giftFlushYuan: number;
65
+ /** 解析或金额换算告警,由调用方记录日志。 */
66
+ warn?: (message: string, data?: Record<string, unknown>) => void;
67
+ }
68
+
69
+ /**
70
+ * GUARD_BUY 金额的合理区间(元)。data.price 的单位尚未实证,换算结果超出区间时省略金额;单位需以真实原帧和平台逐笔金额核对。
71
+ */
72
+ const GUARD_YUAN_MIN = 1;
73
+ const GUARD_YUAN_MAX = 30000;
74
+
75
+ export function normalize(msg: Record<string, unknown>, opts: NormalizeOptions): Normalized | null {
76
+
77
+ const cmd = String(msg.cmd ?? '').split(':')[0];
78
+ if (IGNORED.has(cmd)) return null;
79
+ const data = obj(msg.data);
80
+
81
+ switch (cmd) {
82
+ case 'DANMU_MSG':
83
+ case 'DANMU_MSG_MIRROR':
84
+ return danmaku(msg);
85
+
86
+ case 'SUPER_CHAT_MESSAGE': {
87
+ const user = obj(data.user_info);
88
+ const uid = audienceUid(data);
89
+ const uname = str(user.uname) || '某位观众';
90
+ const yuan = num(data.price);
91
+ return {
92
+ kind: 'event',
93
+ type: 'bilibili.superchat',
94
+ trigger: 'flush',
95
+ text: `[醒目留言 ¥${yuan}|${uname}] ${str(data.message)}`,
96
+ senderKey: senderKeyOf(uid),
97
+ meta: {
98
+ uid,
99
+ uname,
100
+ yuan,
101
+ messageId: data.id,
102
+ avatarUrl: imageUrl(user.face),
103
+ guardLevel: num(user.guard_level),
104
+ isAdmin: bool(user.manager),
105
+ },
106
+ };
107
+ }
108
+
109
+ case 'SUPER_CHAT_MESSAGE_DELETE':
110
+ return {
111
+ kind: 'event',
112
+ type: 'bilibili.superchat-del',
113
+ trigger: 'piggyback',
114
+ text: '[醒目留言被删除]',
115
+ meta: { ids: data.ids },
116
+ };
117
+
118
+ case 'GUARD_BUY': {
119
+ const uid = audienceUid(data);
120
+ const uname = str(data.username) || '某位观众';
121
+ const gift = str(data.gift_name) || '大航海';
122
+ const n = num(data.num) || 1;
123
+
124
+ const price = num(data.price);
125
+ const yuan = price / COIN_PER_YUAN;
126
+ const yuanKnown = price > 0 && yuan >= GUARD_YUAN_MIN && yuan <= GUARD_YUAN_MAX;
127
+ if (price > 0 && !yuanKnown) {
128
+ opts.warn?.('GUARD_BUY price 换算越界,金额不入账', { price, yuan });
129
+ }
130
+ const guardLevel = num(data.guard_level);
131
+ return {
132
+ kind: 'event',
133
+ type: 'bilibili.guard',
134
+ trigger: 'flush',
135
+ text: yuanKnown
136
+ ? `[上舰 ¥${trim(yuan)}|${uname}] 开通了 ${gift}×${n}`
137
+ : `[上舰|${uname}] 开通了 ${gift}×${n}`,
138
+ senderKey: senderKeyOf(uid),
139
+ meta: {
140
+ uid,
141
+ uname,
142
+ gift,
143
+ num: n,
144
+ ...(yuanKnown ? { yuan } : {}),
145
+ ...(guardLevel > 0 ? { guardLevel } : {}),
146
+ },
147
+ };
148
+ }
149
+
150
+ // 仅在帧明确表示续费时使用续费文案;缺失角色或等级时省略对应元数据。
151
+ case 'USER_TOAST_MSG': {
152
+ const sender = obj(data.sender_uinfo);
153
+ const base = obj(sender.base);
154
+ const uid = audienceUid(data);
155
+ const uname = str(data.username) || str(base.name) || '某位观众';
156
+ const role = str(data.role_name);
157
+ const guardLevel = num(data.guard_level) || num(sender.guard_level);
158
+ const renew = str(data.toast_msg).includes('续费');
159
+ return guardToast({ uid, uname, role, guardLevel, renew, face: base.face });
160
+ }
161
+
162
+ case 'USER_TOAST_MSG_V2': {
163
+
164
+ const sender = obj(data.sender_uinfo);
165
+ const base = obj(sender.base);
166
+ const guard = obj(data.guard_info);
167
+ const uid = audienceUid(data);
168
+ const uname = str(base.name) || '某位观众';
169
+ const role = str(guard.role_name);
170
+ const guardLevel = num(guard.guard_level);
171
+ const renew = str(data.toast_msg).includes('续费');
172
+ return guardToast({ uid, uname, role, guardLevel, renew, face: base.face });
173
+ }
174
+
175
+ case 'SEND_GIFT':
176
+ case 'SEND_GIFT_V2': {
177
+
178
+ const frame = giftFrameData(data, opts.warn);
179
+ const sender = obj(frame.sender_uinfo);
180
+ const base = obj(sender.base);
181
+ const uid = audienceUid(frame);
182
+ const uname = str(frame.uname) || str(base.name) || '某位观众';
183
+ const gift = str(frame.giftName) || str(frame.gift_name) || '礼物';
184
+ const n = num(frame.num) || 1;
185
+ const coinType = str(frame.coin_type);
186
+
187
+ if (coinType && coinType !== 'gold') return { kind: 'count', field: 'freeGift', by: n };
188
+ const totalCoin = num(frame.total_coin);
189
+
190
+ const yuanKnown = coinType === 'gold' && totalCoin > 0;
191
+ const yuan = totalCoin / COIN_PER_YUAN;
192
+ if (!yuanKnown) {
193
+ opts.warn?.('礼物帧读不出金额,按未知金额成事件', { cmd, gift, uname, coinType });
194
+ }
195
+ const guardLevel = num(frame.guard_level);
196
+ const tid = str(frame.tid);
197
+ return {
198
+ kind: 'event',
199
+ type: 'bilibili.gift',
200
+
201
+ trigger: yuanKnown && yuan >= opts.giftFlushYuan ? 'flush' : 'debounce',
202
+ text: yuanKnown
203
+ ? `[礼物 ¥${trim(yuan)}|${uname}] ${gift}×${n}`
204
+ : `[礼物|${uname}] ${gift}×${n}`,
205
+ senderKey: senderKeyOf(uid),
206
+ meta: {
207
+ uid,
208
+ uname,
209
+ gift,
210
+ num: n,
211
+ ...(yuanKnown ? { yuan } : {}),
212
+ avatarUrl: imageUrl(base.face),
213
+ guardLevel,
214
+ isAdmin: bool(frame.is_admin),
215
+ ...(tid ? { tid } : {}),
216
+ },
217
+ coalesce: {
218
+ kind: 'gift',
219
+ // 未知金额与已知金额分别归并。
220
+ key: JSON.stringify([gift, yuanKnown ? unitCoinKey(totalCoin, n) : '金额未知']),
221
+ gift,
222
+ num: n,
223
+ yuan: yuanKnown ? yuan : null,
224
+ },
225
+ };
226
+ }
227
+
228
+ case 'ENTRY_EFFECT': {
229
+ // 舰长进场特效。copy_writing 形如 "欢迎舰长 <%昵称%> 进入直播间"
230
+ const line = str(data.copy_writing).replace(/<%|%>/g, '').trim();
231
+ if (!line) return null;
232
+ return {
233
+ kind: 'event',
234
+ type: 'bilibili.enter-guard',
235
+ trigger: 'debounce',
236
+ text: `[进场] ${line}`,
237
+ senderKey: senderKeyOf(audienceUid(data)),
238
+ meta: { uid: audienceUid(data) },
239
+ };
240
+ }
241
+
242
+ case 'INTERACT_WORD':
243
+ case 'INTERACT_WORD_V2':
244
+ // V2 是 protobuf,分不出进场/关注/分享,一律按进场计数
245
+ return { kind: 'count', field: 'enter', by: 1 };
246
+
247
+ case 'LIKE_INFO_V3_CLICK':
248
+ return { kind: 'count', field: 'like', by: 1 };
249
+
250
+ case 'LIKE_INFO_V3_UPDATE':
251
+ return { kind: 'gauge', field: 'likeTotal', value: num(data.click_count) };
252
+
253
+ case 'WATCHED_CHANGE':
254
+ return { kind: 'gauge', field: 'watched', value: num(data.num) };
255
+
256
+ case 'ONLINE_RANK_COUNT':
257
+ return { kind: 'gauge', field: 'online', value: num(data.count) };
258
+
259
+ case 'POPULARITY_CHANGE':
260
+ return { kind: 'gauge', field: 'popularity', value: num(data.popularity) };
261
+
262
+ case 'ROOM_REAL_TIME_MESSAGE_UPDATE':
263
+ return { kind: 'gauge', field: 'fans', value: num(data.fans) };
264
+
265
+ case 'LIVE':
266
+ return {
267
+ kind: 'event',
268
+ type: 'bilibili.room',
269
+ trigger: 'flush',
270
+ text: '[直播间] 平台已推送开播指令:直播画面已对观众可见',
271
+ };
272
+
273
+ case 'PREPARING':
274
+ return {
275
+ kind: 'event',
276
+ type: 'bilibili.room',
277
+ trigger: 'flush',
278
+ text: '[直播间] 平台已推送下播指令:直播已结束,观众已经看不到直播画面;'
279
+ + '此后的弹幕来自仍留在房间页的人,不代表直播还在进行',
280
+ };
281
+
282
+ case 'ROOM_CHANGE':
283
+ return {
284
+ kind: 'event',
285
+ type: 'bilibili.room',
286
+ trigger: 'debounce',
287
+ text: `[直播间] 标题/分区变更为「${str(data.title)}」(${str(data.area_name)})`,
288
+ meta: { title: data.title, area: data.area_name },
289
+ };
290
+
291
+ case 'ROOM_SILENT_ON':
292
+ return { kind: 'event', type: 'bilibili.room', trigger: 'flush', text: '[直播间] 已开启全员禁言' };
293
+
294
+ case 'ROOM_SILENT_OFF':
295
+ return { kind: 'event', type: 'bilibili.room', trigger: 'flush', text: '[直播间] 已关闭全员禁言' };
296
+
297
+ case 'ROOM_BLOCK_MSG': {
298
+ const uname = str(data.uname) || str(msg.uname) || '某位观众';
299
+ return {
300
+ kind: 'event',
301
+ type: 'bilibili.block',
302
+ trigger: 'debounce',
303
+ text: `[房管] ${uname} 被禁言`,
304
+ meta: { uname },
305
+ };
306
+ }
307
+
308
+ case 'WARNING':
309
+ return {
310
+ kind: 'event',
311
+ type: 'bilibili.warning',
312
+ trigger: 'flush',
313
+ text: `[平台警告] ${str(msg.msg) || '直播间收到一条超管警告'}`,
314
+ };
315
+
316
+ case 'CUT_OFF':
317
+ return {
318
+ kind: 'event',
319
+ type: 'bilibili.warning',
320
+ trigger: 'flush',
321
+ text: `[直播被切断] ${str(msg.msg) || '直播已被平台切断'}`,
322
+ };
323
+
324
+ default:
325
+ return null;
326
+ }
327
+ }
328
+
329
+ /** 弹幕字段:正文 info[1],发言人 info[2][0..1],粉丝牌 info[3],大航海等级 info[7]。uid 为 0 时不提供稳定身份键。 */
330
+ function danmaku(msg: Record<string, unknown>): LiveEvent | null {
331
+ const info = Array.isArray(msg.info) ? (msg.info as unknown[]) : null;
332
+ if (!info) return null;
333
+ const text = str(info[1]).trim();
334
+ if (!text) return null;
335
+ const sender = Array.isArray(info[2]) ? (info[2] as unknown[]) : [];
336
+ const uid = num(sender[0]);
337
+ const uname = str(sender[1]) || '某位观众';
338
+ const medal = Array.isArray(info[3]) ? (info[3] as unknown[]) : [];
339
+ const userLevel = Array.isArray(info[4]) ? (info[4] as unknown[]) : [];
340
+ const rich = obj(Array.isArray(info[0]) ? (info[0] as unknown[])[15] : undefined);
341
+ const richUser = obj(rich.user);
342
+ const richBase = obj(richUser.base);
343
+ const guardLevel = num(info[7]);
344
+ const badge = guardLevel > 0 ? '·舰长' : '';
345
+ return {
346
+ kind: 'event',
347
+ type: 'bilibili.danmaku',
348
+ trigger: 'debounce',
349
+ text: `[弹幕|${uname}${badge}] ${text}`,
350
+ senderKey: senderKeyOf(uid),
351
+ meta: {
352
+ uid,
353
+ uname,
354
+ guardLevel,
355
+ isAdmin: bool(sender[2]),
356
+ vip: bool(sender[3]),
357
+ svip: bool(sender[4]),
358
+ rank: num(sender[5]),
359
+ nameColor: str(sender[7]),
360
+ userLevel: num(userLevel[0]),
361
+ avatarUrl: imageUrl(richBase.face),
362
+ medal: medal.length
363
+ ? {
364
+ level: num(medal[0]),
365
+ name: str(medal[1]),
366
+ anchorName: str(medal[2]),
367
+ roomId: num(medal[3]),
368
+ color: num(medal[4]),
369
+ }
370
+ : null,
371
+ body: text,
372
+ },
373
+ coalesce: { kind: 'danmaku', key: text, body: text },
374
+ };
375
+ }
376
+
377
+ function guardToast(fields: {
378
+ uid: number;
379
+ uname: string;
380
+ role: string;
381
+ guardLevel: number;
382
+ renew: boolean;
383
+ face: unknown;
384
+ }): LiveEvent {
385
+ const { uid, uname, role, guardLevel, renew } = fields;
386
+ const roleText = role || '大航海';
387
+ return {
388
+ kind: 'event',
389
+ type: 'bilibili.guard-renew',
390
+ trigger: 'debounce',
391
+ text: renew ? `[续费|${uname}] 续费了 ${roleText}` : `[上舰|${uname}] 上了 ${roleText}`,
392
+ senderKey: senderKeyOf(uid),
393
+ meta: {
394
+ uid,
395
+ uname,
396
+ ...(role ? { role } : {}),
397
+ ...(guardLevel > 0 ? { guardLevel } : {}),
398
+ ...(renew ? { renew: true } : {}),
399
+ avatarUrl: imageUrl(fields.face),
400
+ },
401
+ };
402
+ }
403
+
404
+ function senderKeyOf(uid: number): string | undefined {
405
+ return uid > 0 ? String(uid) : undefined;
406
+ }
407
+
408
+ /** 付费事件的 uid 在不同 cmd 版本中所处层级不同。 */
409
+ function audienceUid(data: Record<string, unknown>): number {
410
+ const values = [
411
+ num(data.uid),
412
+ num(obj(data.user_info).uid),
413
+ num(obj(data.sender_uinfo).uid),
414
+ ];
415
+ return values.find((value) => value > 0) ?? 0;
416
+ }
417
+
418
+ function obj(v: unknown): Record<string, unknown> {
419
+ return v !== null && typeof v === 'object' ? (v as Record<string, unknown>) : {};
420
+ }
421
+
422
+ function str(v: unknown): string {
423
+ return typeof v === 'string' ? v : '';
424
+ }
425
+
426
+ function num(v: unknown): number {
427
+ return typeof v === 'number' && Number.isFinite(v) ? v : 0;
428
+ }
429
+
430
+ function bool(v: unknown): boolean {
431
+ return v === true || v === 1;
432
+ }
433
+
434
+ function imageUrl(v: unknown): string {
435
+ const raw = str(v);
436
+ if (raw.startsWith('//')) return `https:${raw}`;
437
+ return /^https?:\/\//i.test(raw) ? raw : '';
438
+ }
439
+
440
+ function unitCoinKey(totalCoin: number, count: number): string {
441
+ let a = Math.abs(Math.round(totalCoin));
442
+ let b = Math.abs(Math.round(count));
443
+ const numerator = Math.round(totalCoin);
444
+ const denominator = Math.round(count);
445
+ while (b !== 0) [a, b] = [b, a % b];
446
+ const divisor = a || 1;
447
+ return `${numerator / divisor}/${denominator / divisor}`;
448
+ }
449
+
450
+ function trim(yuan: number): string {
451
+ return Number.isInteger(yuan) ? String(yuan) : yuan.toFixed(2);
452
+ }
@@ -0,0 +1,76 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { dirname } from 'node:path';
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import type { AgentAnnouncementState } from './types.ts';
5
+
6
+ const EMPTY_STATE: AgentAnnouncementState = {
7
+ schemaVersion: 1,
8
+ text: '',
9
+ revision: 0,
10
+ updatedAt: null,
11
+ };
12
+
13
+ export class AgentAnnouncementStore {
14
+ private state: AgentAnnouncementState = { ...EMPTY_STATE };
15
+
16
+ constructor(private readonly file?: string) {
17
+ this.load();
18
+ }
19
+
20
+ get current(): AgentAnnouncementState {
21
+ return { ...this.state };
22
+ }
23
+
24
+ set(text: string, maxChars: number): AgentAnnouncementState {
25
+ const normalized = text.replace(/\r\n?/g, '\n');
26
+ const count = Array.from(normalized).length;
27
+ if (count > maxChars) throw new Error(`公告最多 ${maxChars} 字,当前 ${count} 字`);
28
+ const next: AgentAnnouncementState = {
29
+ schemaVersion: 1,
30
+ text: normalized,
31
+ revision: this.state.revision + 1,
32
+ updatedAt: new Date().toISOString(),
33
+ };
34
+ this.persist(next);
35
+ this.state = next;
36
+ return this.current;
37
+ }
38
+
39
+ private load(): void {
40
+ if (!this.file || !existsSync(this.file)) return;
41
+ try {
42
+ const raw = JSON.parse(readFileSync(this.file, 'utf8')) as Partial<AgentAnnouncementState>;
43
+ if (raw.schemaVersion !== 1 || typeof raw.text !== 'string') return;
44
+ this.state = {
45
+ schemaVersion: 1,
46
+ text: raw.text.replace(/\r\n?/g, '\n'),
47
+ revision: Number.isInteger(raw.revision) && Number(raw.revision) >= 0 ? Number(raw.revision) : 0,
48
+ updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : null,
49
+ };
50
+ } catch {
51
+ this.state = { ...EMPTY_STATE };
52
+ }
53
+ }
54
+
55
+ private persist(next: AgentAnnouncementState): void {
56
+ if (!this.file) return;
57
+ const dir = dirname(this.file);
58
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
59
+ const suffix = `${process.pid}-${randomUUID()}`;
60
+ const temporary = `${this.file}.tmp-${suffix}`;
61
+ const backup = `${this.file}.bak-${suffix}`;
62
+ writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
63
+ if (!existsSync(this.file)) {
64
+ renameSync(temporary, this.file);
65
+ return;
66
+ }
67
+ renameSync(this.file, backup);
68
+ try {
69
+ renameSync(temporary, this.file);
70
+ } catch (error) {
71
+ renameSync(backup, this.file);
72
+ throw error;
73
+ }
74
+ rmSync(backup);
75
+ }
76
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import type { OverlayAssetInfo } from './types.ts';
5
+
6
+ const MAX_ASSET_BYTES = 8 * 1024 * 1024;
7
+ const ASSET_ID_RE = /^[a-f0-9]{64}\.(?:png|jpg|webp|gif)$/;
8
+
9
+ const MIME_BY_EXT: Record<string, string> = {
10
+ png: 'image/png',
11
+ jpg: 'image/jpeg',
12
+ webp: 'image/webp',
13
+ gif: 'image/gif',
14
+ };
15
+
16
+ export class OverlayAssetStore {
17
+ constructor(readonly dir: string) {}
18
+
19
+ list(baseUrl: string | null): OverlayAssetInfo[] {
20
+ if (!existsSync(this.dir)) return [];
21
+ return readdirSync(this.dir, { withFileTypes: true })
22
+ .filter((entry) => entry.isFile() && ASSET_ID_RE.test(entry.name))
23
+ .map((entry) => {
24
+ const ext = entry.name.split('.').pop() ?? '';
25
+ return {
26
+ id: entry.name,
27
+ mime: MIME_BY_EXT[ext] ?? 'application/octet-stream',
28
+ size: statSync(join(this.dir, entry.name)).size,
29
+ url: baseUrl ? `${baseUrl}/assets/${entry.name}` : '',
30
+ };
31
+ })
32
+ .sort((a, b) => a.id.localeCompare(b.id));
33
+ }
34
+
35
+ import(base64: string): { id: string; mime: string; size: number } {
36
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64) || base64.length % 4 !== 0) {
37
+ throw new Error('素材不是有效的 base64');
38
+ }
39
+ const bytes = Buffer.from(base64, 'base64');
40
+ if (!bytes.length) throw new Error('素材为空');
41
+ if (bytes.length > MAX_ASSET_BYTES) throw new Error('素材不能超过 8 MiB');
42
+ const detected = detectImage(bytes);
43
+ if (!detected) throw new Error('只支持 PNG、JPEG、WebP 和 GIF 图片');
44
+ const id = `${createHash('sha256').update(bytes).digest('hex')}.${detected.ext}`;
45
+ if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
46
+ const path = join(this.dir, id);
47
+ if (!existsSync(path)) writeFileSync(path, bytes);
48
+ return { id, mime: detected.mime, size: bytes.length };
49
+ }
50
+
51
+ delete(id: string): boolean {
52
+ if (!ASSET_ID_RE.test(id)) throw new Error('素材 id 无效');
53
+ const path = join(this.dir, id);
54
+ if (!existsSync(path)) return false;
55
+ unlinkSync(path);
56
+ return true;
57
+ }
58
+
59
+ path(id: string): { path: string; mime: string } | null {
60
+ if (!ASSET_ID_RE.test(id)) return null;
61
+ const path = join(this.dir, id);
62
+ if (!existsSync(path)) return null;
63
+ const ext = id.split('.').pop() ?? '';
64
+ return { path, mime: MIME_BY_EXT[ext] ?? 'application/octet-stream' };
65
+ }
66
+ }
67
+
68
+ function detectImage(bytes: Buffer): { ext: string; mime: string } | null {
69
+ if (bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
70
+ return { ext: 'png', mime: 'image/png' };
71
+ }
72
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
73
+ return { ext: 'jpg', mime: 'image/jpeg' };
74
+ }
75
+ if (bytes.length >= 12 && bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP') {
76
+ return { ext: 'webp', mime: 'image/webp' };
77
+ }
78
+ if (bytes.length >= 6 && (bytes.toString('ascii', 0, 6) === 'GIF87a' || bytes.toString('ascii', 0, 6) === 'GIF89a')) {
79
+ return { ext: 'gif', mime: 'image/gif' };
80
+ }
81
+ return null;
82
+ }