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,375 @@
1
+ /**
2
+ * OneBotDriver — 薄驱动层。协议扩展动作失败时隔离为局部错误。
3
+ *
4
+ * core作WS客户端连协议端(NapCat等)的正向WS;事件推送与API调用
5
+ * 走同一条双工连接。只依赖OneBot v11标准动作;扩展动作(表情回应等)
6
+ * 走callExtension单独隔离——失败优雅降级返回错误,不抛崩。
7
+ * 协议端切换=改一行WS地址,协议端是消耗品。
8
+ */
9
+ import WebSocket from 'ws';
10
+ import type { Logger } from '../../core/types.ts';
11
+ import { nullLogger } from '../../core/util.ts';
12
+
13
+ /** OneBot事件的最小形状(驱动层不解释语义,原样交给 World) */
14
+ export interface OneBotEvent {
15
+ post_type?: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ /** 连接后探测到的自身身份信息(envPrompt的数据源) */
20
+ interface DriverIdentity {
21
+ selfId: number;
22
+ nickname: string;
23
+ /** 每个监听群:群名 + 自己在该群的群昵称(card||nickname) */
24
+ groups: Map<number, { groupName: string; card: string }>;
25
+ }
26
+
27
+ interface OneBotDriverOptions {
28
+ wsUrl: string;
29
+ /** 监听的群号集合(身份初始化时逐群查群名/自己的群昵称) */
30
+ groups: number[];
31
+ /** OneBot access token,空串=不带 */
32
+ token?: string;
33
+ log?: Logger;
34
+ /** 重连退避起点,默认1000ms */
35
+ reconnectBaseMs?: number;
36
+ /** 重连退避封顶,默认30000ms */
37
+ reconnectMaxMs?: number;
38
+ /** API调用默认超时,默认10000ms */
39
+ apiTimeoutMs?: number;
40
+ }
41
+
42
+ interface PendingCall {
43
+ action: string;
44
+ resolve: (data: unknown) => void;
45
+ reject: (err: Error) => void;
46
+ timer: ReturnType<typeof setTimeout>;
47
+ }
48
+
49
+ /** 扩展动作的结果:不抛错,调用方按ok分支降级 */
50
+ type ExtensionResult =
51
+ | { ok: true; data: unknown }
52
+ | { ok: false; error: string };
53
+
54
+ export class OneBotDriver {
55
+ private readonly opts: OneBotDriverOptions;
56
+ private readonly log: Logger;
57
+ /** 监听群号(可热改;身份初始化按此逐群查) */
58
+ private groups: number[];
59
+ private ws?: WebSocket;
60
+ private stopped = true;
61
+ private echoSeq = 0;
62
+ private pending = new Map<string, PendingCall>();
63
+ private backoffMs: number;
64
+ private reconnectTimer?: ReturnType<typeof setTimeout>;
65
+ private eventCbs: Array<(ev: OneBotEvent) => void> = [];
66
+ private readyCbs: Array<() => void> = [];
67
+
68
+ /** 连接并初始化身份后可用;断线期间保留上次值 */
69
+ identity?: DriverIdentity;
70
+ connected = false;
71
+
72
+ constructor(opts: OneBotDriverOptions) {
73
+ this.opts = opts;
74
+ this.log = opts.log ?? nullLogger();
75
+ this.backoffMs = opts.reconnectBaseMs ?? 1000;
76
+ this.groups = [...opts.groups];
77
+ }
78
+
79
+ /** 热改监听群号:更新列表,已连接则后台刷新身份(新群补群名/群昵称) */
80
+ setGroups(groups: number[]): void {
81
+ this.groups = [...groups];
82
+ if (this.connected) {
83
+ void this.initIdentity().catch((err) => {
84
+ this.log.warn('热改后身份刷新失败', {
85
+ error: err instanceof Error ? err.message : String(err),
86
+ });
87
+ });
88
+ }
89
+ }
90
+
91
+ /** 注册原始事件回调(不带echo的服务端推送) */
92
+ onEvent(cb: (ev: OneBotEvent) => void): void {
93
+ this.eventCbs.push(cb);
94
+ }
95
+
96
+ /** 每次(重)连成功并完成身份初始化后回调 */
97
+ onReady(cb: () => void): void {
98
+ this.readyCbs.push(cb);
99
+ }
100
+
101
+ /**
102
+ * 启动连接。首次连接尝试结束(成功就绪或失败)后resolve;
103
+ * 失败时后台按指数退避(1s起倍增,30s封顶)持续重连,不reject。
104
+ */
105
+ start(): Promise<void> {
106
+ this.stopped = false;
107
+ this.backoffMs = this.opts.reconnectBaseMs ?? 1000;
108
+ return new Promise((resolve) => {
109
+ this.connectAttempt(resolve);
110
+ });
111
+ }
112
+
113
+ /** 等待连接就绪(已就绪则立即返回);超时reject。测试/联调辅助。 */
114
+ waitReady(timeoutMs = 10000): Promise<void> {
115
+ if (this.connected && this.identity) return Promise.resolve();
116
+ return new Promise((resolve, reject) => {
117
+ const timer = setTimeout(() => {
118
+ remove();
119
+ reject(new Error(`等待WS就绪超时(${timeoutMs}ms)`));
120
+ }, timeoutMs);
121
+ const cb = () => {
122
+ clearTimeout(timer);
123
+ remove();
124
+ resolve();
125
+ };
126
+ const remove = () => {
127
+ const i = this.readyCbs.indexOf(cb);
128
+ if (i >= 0) this.readyCbs.splice(i, 1);
129
+ };
130
+ this.readyCbs.push(cb);
131
+ });
132
+ }
133
+
134
+ async stop(): Promise<void> {
135
+ this.stopped = true;
136
+ if (this.reconnectTimer) {
137
+ clearTimeout(this.reconnectTimer);
138
+ this.reconnectTimer = undefined;
139
+ }
140
+ this.rejectAllPending(new Error('驱动已停止'));
141
+ const ws = this.ws;
142
+ this.ws = undefined;
143
+ this.connected = false;
144
+ if (ws) {
145
+ ws.terminate();
146
+ }
147
+ }
148
+
149
+ /**
150
+ * OneBot标准API调用:echo关联响应,超时reject。
151
+ * retcode非0或status为failed → reject。
152
+ */
153
+ callApi(
154
+ action: string,
155
+ params: Record<string, unknown> = {},
156
+ timeoutMs?: number,
157
+ ): Promise<unknown> {
158
+ const timeout = timeoutMs ?? this.opts.apiTimeoutMs ?? 10000;
159
+ return new Promise((resolve, reject) => {
160
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
161
+ reject(new Error(`${action}: WS未连接`));
162
+ return;
163
+ }
164
+ const echo = `ob-${++this.echoSeq}`;
165
+ const timer = setTimeout(() => {
166
+ this.pending.delete(echo);
167
+ reject(new Error(`${action}: 调用超时(${timeout}ms)`));
168
+ }, timeout);
169
+ this.pending.set(echo, { action, resolve, reject, timer });
170
+ try {
171
+ this.ws.send(JSON.stringify({ action, params, echo }));
172
+ } catch (err) {
173
+ clearTimeout(timer);
174
+ this.pending.delete(echo);
175
+ reject(err instanceof Error ? err : new Error(String(err)));
176
+ }
177
+ });
178
+ }
179
+
180
+ /**
181
+ * 扩展动作隔离通道:set_msg_emoji_like等非OneBot v11标准动作
182
+ * 从这里走。任何失败(不支持/超时/断线)都不抛,返回{ok:false,error}
183
+ * 由 World 优雅降级成文本。
184
+ */
185
+ async callExtension(
186
+ action: string,
187
+ params: Record<string, unknown> = {},
188
+ timeoutMs?: number,
189
+ ): Promise<ExtensionResult> {
190
+ try {
191
+ const data = await this.callApi(action, params, timeoutMs);
192
+ return { ok: true, data };
193
+ } catch (err) {
194
+ const message = err instanceof Error ? err.message : String(err);
195
+ this.log.warn(`扩展动作失败: ${action}`, { error: message });
196
+ return { ok: false, error: message };
197
+ }
198
+ }
199
+
200
+
201
+ private connectAttempt(onSettled?: () => void): void {
202
+ if (this.stopped) {
203
+ onSettled?.();
204
+ return;
205
+ }
206
+ let settled = false;
207
+ const settle = () => {
208
+ if (!settled) {
209
+ settled = true;
210
+ onSettled?.();
211
+ }
212
+ };
213
+
214
+ const token = this.opts.token;
215
+ const ws = new WebSocket(this.opts.wsUrl, {
216
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined,
217
+ });
218
+ this.ws = ws;
219
+
220
+ ws.on('open', () => {
221
+ if (this.stopped || this.ws !== ws) {
222
+ ws.terminate();
223
+ settle();
224
+ return;
225
+ }
226
+ this.connected = true;
227
+ this.backoffMs = this.opts.reconnectBaseMs ?? 1000;
228
+ this.log.info('WS已连接', { wsUrl: this.opts.wsUrl });
229
+ void (async () => {
230
+ try {
231
+ await this.initIdentity();
232
+ } catch (err) {
233
+ this.log.warn('身份初始化失败(连接仍可用)', {
234
+ error: err instanceof Error ? err.message : String(err),
235
+ });
236
+ }
237
+ if (this.stopped || this.ws !== ws) {
238
+ settle();
239
+ return;
240
+ }
241
+ for (const cb of [...this.readyCbs]) cb();
242
+ settle();
243
+ })();
244
+ });
245
+
246
+ ws.on('message', (data) => {
247
+ if (!this.stopped && this.ws === ws) this.handleMessage(String(data));
248
+ });
249
+
250
+ ws.on('error', (err) => {
251
+ this.log.warn('WS错误', { error: err.message });
252
+ });
253
+
254
+ ws.on('close', () => {
255
+ const isCurrent = this.ws === ws;
256
+ const wasConnected = this.connected;
257
+ if (isCurrent) {
258
+ this.ws = undefined;
259
+ this.connected = false;
260
+ this.rejectAllPending(new Error('WS连接已断开'));
261
+ if (wasConnected) this.log.info('WS已断开');
262
+ }
263
+ settle();
264
+ if (isCurrent && !this.stopped) this.scheduleReconnect();
265
+ });
266
+ }
267
+
268
+ private scheduleReconnect(): void {
269
+ if (this.reconnectTimer || this.stopped) return;
270
+ const delay = this.backoffMs;
271
+ this.backoffMs = Math.min(
272
+ this.backoffMs * 2,
273
+ this.opts.reconnectMaxMs ?? 30000,
274
+ );
275
+ this.log.info(`${delay}ms后重连`);
276
+ this.reconnectTimer = setTimeout(() => {
277
+ this.reconnectTimer = undefined;
278
+ if (!this.stopped) this.connectAttempt();
279
+ }, delay);
280
+ }
281
+
282
+ /** 连上后查 自身账号→逐个监听群的群名/自己的群昵称,填identity */
283
+ private async initIdentity(): Promise<void> {
284
+ const login = (await this.callApi('get_login_info')) as {
285
+ user_id?: number;
286
+ nickname?: string;
287
+ };
288
+ const selfId = Number(login?.user_id);
289
+ const nickname = String(login?.nickname ?? '');
290
+
291
+ const groups = new Map<number, { groupName: string; card: string }>();
292
+ for (const gid of this.groups) {
293
+ let groupName = this.identity?.groups.get(gid)?.groupName ?? String(gid);
294
+ try {
295
+ const gi = (await this.callApi('get_group_info', { group_id: gid })) as {
296
+ group_name?: string;
297
+ };
298
+ if (gi?.group_name) groupName = String(gi.group_name);
299
+ } catch (err) {
300
+ this.log.warn('get_group_info失败', {
301
+ gid,
302
+ error: err instanceof Error ? err.message : String(err),
303
+ });
304
+ }
305
+
306
+ let card = nickname;
307
+ try {
308
+ const mi = (await this.callApi('get_group_member_info', {
309
+ group_id: gid,
310
+ user_id: selfId,
311
+ })) as { card?: string; nickname?: string };
312
+ card = String(mi?.card || mi?.nickname || nickname);
313
+ } catch (err) {
314
+ this.log.warn('get_group_member_info失败', {
315
+ gid,
316
+ error: err instanceof Error ? err.message : String(err),
317
+ });
318
+ }
319
+ groups.set(gid, { groupName, card });
320
+ }
321
+
322
+ this.identity = { selfId, nickname, groups };
323
+ }
324
+
325
+ private handleMessage(raw: string): void {
326
+ let msg: Record<string, unknown>;
327
+ try {
328
+ msg = JSON.parse(raw) as Record<string, unknown>;
329
+ } catch {
330
+ this.log.warn('收到非JSON消息,已忽略');
331
+ return;
332
+ }
333
+
334
+ // API响应:带echo
335
+ if (msg.echo !== undefined && msg.echo !== null) {
336
+ const call = this.pending.get(String(msg.echo));
337
+ if (!call) return; // 已超时/已清理
338
+ this.pending.delete(String(msg.echo));
339
+ clearTimeout(call.timer);
340
+ const retcode = Number(msg.retcode ?? -1);
341
+ const status = String(msg.status ?? '');
342
+ if (retcode === 0 || status === 'ok' || status === 'async') {
343
+ call.resolve(msg.data);
344
+ } else {
345
+ call.reject(
346
+ new Error(
347
+ `${call.action}: retcode=${retcode}${msg.message ? ` ${String(msg.message)}` : ''}`,
348
+ ),
349
+ );
350
+ }
351
+ return;
352
+ }
353
+
354
+ // 事件推送
355
+ if (msg.post_type !== undefined) {
356
+ for (const cb of this.eventCbs) {
357
+ try {
358
+ cb(msg as OneBotEvent);
359
+ } catch (err) {
360
+ this.log.error('事件回调抛错', {
361
+ error: err instanceof Error ? err.message : String(err),
362
+ });
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ private rejectAllPending(err: Error): void {
369
+ for (const call of this.pending.values()) {
370
+ clearTimeout(call.timer);
371
+ call.reject(new Error(`${call.action}: ${err.message}`));
372
+ }
373
+ this.pending.clear();
374
+ }
375
+ }
@@ -0,0 +1,209 @@
1
+ import type {
2
+ EventEnvelope,
3
+ EventGrepQuery,
4
+ EventRangeQuery,
5
+ WorldHost,
6
+ ToolDef,
7
+ } from '../../core/types.ts';
8
+ import { renderEventLines } from '../../core/util.ts';
9
+ import {
10
+ eventInConversation,
11
+ parseConversationAddress,
12
+ type Conv,
13
+ } from './conversation.ts';
14
+
15
+ interface HistoryToolDeps {
16
+ source: string;
17
+ host: () => WorldHost | undefined;
18
+ }
19
+
20
+ const NOT_STARTED = '[tool failed] QQ module not started';
21
+ const TARGET_FORMAT = '"group:<id>" or "private:<id>"';
22
+
23
+ function parseConversationFilter(raw: unknown): Conv | { error: string } | null {
24
+ if (typeof raw !== 'string' || !raw.trim()) return null;
25
+ const conversation = parseConversationAddress(raw);
26
+ return conversation ?? {
27
+ error: `"${raw.trim()}" is not a valid conversation; use ${TARGET_FORMAT} (numeric QQ group/user id, not a name)`,
28
+ };
29
+ }
30
+
31
+ function matchesReadFilters(
32
+ event: EventEnvelope,
33
+ source: string,
34
+ args: Record<string, unknown>,
35
+ conversation: Conv | null,
36
+ ): boolean {
37
+ if (event.source !== source) return false;
38
+ if (event.origin !== 'external') return false;
39
+ if (typeof args.sender === 'string' && event.senderKey !== args.sender) return false;
40
+ if (typeof args.from_time === 'string' && event.ts < args.from_time) return false;
41
+ if (typeof args.to_time === 'string' && event.ts > args.to_time) return false;
42
+ return !conversation || eventInConversation(event, conversation);
43
+ }
44
+
45
+ /**
46
+ * 邻域读取的两个入口:
47
+ * - around = 平台 message_id(消息行首的 `#<id>`),World 自己定位
48
+ * - around_time = ISO 时间,取该时刻之后的第一条为中心(没有号可用时的入口)
49
+ * 两者都不涉及 core 事件游标——游标是存储位置,不进 agent 的词表。
50
+ */
51
+ function readAround(
52
+ host: WorldHost,
53
+ source: string,
54
+ args: Record<string, unknown>,
55
+ conversation: Conv | null,
56
+ ): EventEnvelope[] {
57
+ const candidates = host.store
58
+ .range({ source, origin: 'external' })
59
+ .filter((event) => matchesReadFilters(event, source, args, conversation));
60
+ let center = -1;
61
+ if (args.around !== undefined && args.around !== null && String(args.around) !== '') {
62
+ const mid = String(args.around).trim().replace(/^#/, '');
63
+ center = candidates.findIndex((event) => String(event.meta?.message_id ?? '') === mid);
64
+ } else if (typeof args.around_time === 'string' && args.around_time) {
65
+ center = candidates.findIndex((event) => event.ts >= (args.around_time as string));
66
+ // 给的时刻晚于全部记录时,以最后一条为中心
67
+ if (center < 0 && candidates.length > 0) center = candidates.length - 1;
68
+ }
69
+ if (center < 0) return [];
70
+ const before = args.before !== undefined ? Math.max(0, Number(args.before)) : 20;
71
+ const after = args.after !== undefined ? Math.max(0, Number(args.after)) : 20;
72
+ return candidates.slice(Math.max(0, center - before), center + after + 1);
73
+ }
74
+
75
+ function createReadHistoryTool(deps: HistoryToolDeps): ToolDef {
76
+ return {
77
+ name: 'qq_read_history',
78
+ description:
79
+ 'Read QQ history: the neighborhood of one message (around / around_time), or a time range. Optional conversation and sender filters.',
80
+ tags: ['read'],
81
+ parameters: {
82
+ type: 'object',
83
+ properties: {
84
+ around: {
85
+ type: 'string',
86
+ description: 'A QQ message id — the `#<id>` at the start of a message line. Reads the messages around it.',
87
+ },
88
+ around_time: {
89
+ type: 'string',
90
+ description: 'ISO 8601 time: reads around the first message at or after it. Use when you have no message id.',
91
+ },
92
+ before: { type: 'number', description: 'With around/around_time: how many earlier messages (default 20).' },
93
+ after: { type: 'number', description: 'With around/around_time: how many later messages (default 20).' },
94
+ from_time: { type: 'string', description: 'Start time, ISO 8601.' },
95
+ to_time: { type: 'string', description: 'End time, ISO 8601.' },
96
+ sender: { type: 'string', description: 'Filter by QQ number.' },
97
+ conversation: {
98
+ type: 'string',
99
+ description: `Filter by conversation: ${TARGET_FORMAT} (numeric QQ group/user id, not a name).`,
100
+ },
101
+ limit: { type: 'number', description: 'Max results, default 50.' },
102
+ },
103
+ required: [],
104
+ },
105
+ handler: async (args) => {
106
+ const host = deps.host();
107
+ if (!host) return NOT_STARTED;
108
+ const filter = parseConversationFilter(args.conversation);
109
+ if (filter && 'error' in filter) return `[bad input] ${filter.error}`;
110
+ const conversation = filter || null;
111
+ const limit = args.limit !== undefined ? Number(args.limit) : 50;
112
+
113
+ const hasAround =
114
+ (args.around !== undefined && args.around !== null && String(args.around) !== '') ||
115
+ (typeof args.around_time === 'string' && args.around_time !== '');
116
+ let events: EventEnvelope[];
117
+ if (hasAround) {
118
+ events = readAround(host, deps.source, args, conversation);
119
+ } else {
120
+ const query: EventRangeQuery = {
121
+ source: deps.source,
122
+ origin: 'external',
123
+ limit: conversation ? undefined : limit,
124
+ };
125
+ if (typeof args.from_time === 'string') query.fromTs = args.from_time;
126
+ if (typeof args.to_time === 'string') query.toTs = args.to_time;
127
+ if (typeof args.sender === 'string') query.senderKey = args.sender;
128
+ events = host.store.range(query);
129
+ if (conversation) {
130
+ events = events.filter((event) => eventInConversation(event, conversation));
131
+ if (events.length > limit) events = events.slice(events.length - limit);
132
+ }
133
+ }
134
+
135
+ return events.length ? renderEventLines(events) : '(no matching messages)';
136
+ },
137
+ };
138
+ }
139
+
140
+ function createGrepHistoryTool(deps: HistoryToolDeps): ToolDef {
141
+ return {
142
+ name: 'qq_grep_history',
143
+ description:
144
+ 'Keyword search over QQ history; each hit includes 3 messages of context on each side. Optional conversation and sender filters.',
145
+ tags: ['read'],
146
+ parameters: {
147
+ type: 'object',
148
+ properties: {
149
+ keyword: { type: 'string', description: 'Keyword (plain substring match).' },
150
+ sender: { type: 'string', description: 'Filter by QQ number.' },
151
+ conversation: {
152
+ type: 'string',
153
+ description: `Filter by conversation: ${TARGET_FORMAT} (numeric QQ group/user id, not a name).`,
154
+ },
155
+ from_time: { type: 'string', description: 'Start time, ISO 8601.' },
156
+ to_time: { type: 'string', description: 'End time, ISO 8601.' },
157
+ limit: { type: 'number', description: 'Max hit groups, default 5.' },
158
+ },
159
+ required: ['keyword'],
160
+ },
161
+ handler: async (args) => {
162
+ const host = deps.host();
163
+ if (!host) return NOT_STARTED;
164
+ const keyword = typeof args.keyword === 'string' ? args.keyword : '';
165
+ if (!keyword) return '[bad input] keyword must not be empty';
166
+ const filter = parseConversationFilter(args.conversation);
167
+ if (filter && 'error' in filter) return `[bad input] ${filter.error}`;
168
+ const conversation = filter || null;
169
+ const limit = args.limit !== undefined ? Number(args.limit) : 5;
170
+
171
+ const query: EventGrepQuery = {
172
+ keyword,
173
+ context: 3,
174
+ source: deps.source,
175
+ origin: 'external',
176
+ limit: conversation ? undefined : limit,
177
+ };
178
+ if (typeof args.sender === 'string') query.senderKey = args.sender;
179
+ if (typeof args.from_time === 'string') query.fromTs = args.from_time;
180
+ if (typeof args.to_time === 'string') query.toTs = args.to_time;
181
+
182
+ let hits = host.store.grep(query);
183
+ if (conversation) {
184
+ hits = hits.filter((hit) => {
185
+ const event = hit.events.find((item) => item.cursor === hit.hitCursor);
186
+ return !!event && eventInConversation(event, conversation);
187
+ });
188
+ if (hits.length > limit) hits = hits.slice(0, limit);
189
+ }
190
+ hits = hits.map((hit) => ({
191
+ ...hit,
192
+ events: hit.events.filter(
193
+ (event) =>
194
+ event.source === deps.source &&
195
+ event.origin === 'external' &&
196
+ (!conversation || eventInConversation(event, conversation)),
197
+ ),
198
+ }));
199
+
200
+ return hits.length
201
+ ? hits.map((hit) => renderEventLines(hit.events)).join('\n---\n')
202
+ : `(no messages containing "${keyword}")`;
203
+ },
204
+ };
205
+ }
206
+
207
+ export function createHistoryTools(deps: HistoryToolDeps): ToolDef[] {
208
+ return [createReadHistoryTool(deps), createGrepHistoryTool(deps)];
209
+ }