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,56 @@
1
+ /**
2
+ * The console's language on the browser side. The server stamps its default on `<html lang>`;
3
+ * a preference saved in this browser overrides it. Strings are selected at module load time,
4
+ * so changing the preference reloads the page. Every request and every WebSocket handshake
5
+ * carries the value back (`languageHeaders` / `withLanguage`), and the server renders its own
6
+ * console text in it.
7
+ */
8
+ import type { Language } from '../../../core/language.ts';
9
+ import { CONSOLE_LANGUAGE_HEADER, CONSOLE_LANGUAGE_QUERY } from '../../shared/console-protocol.ts';
10
+
11
+ export type { Language };
12
+
13
+ const STORAGE_KEY = 'cortico.console.language';
14
+
15
+ export function saveLanguage(language: Language, storage: Pick<Storage, 'setItem'>): void {
16
+ storage.setItem(STORAGE_KEY, language);
17
+ }
18
+
19
+ export function readLanguage(doc: Document): Language {
20
+ let preference: string | null = null;
21
+ try {
22
+ preference = doc.defaultView?.localStorage.getItem(STORAGE_KEY) ?? null;
23
+ } catch {
24
+ // Storage may be unavailable in private browsing; retain the server default.
25
+ }
26
+ const language = preference === 'zh' || preference === 'en'
27
+ ? preference
28
+ : doc.documentElement.lang.toLowerCase().startsWith('en') ? 'en' : 'zh';
29
+ doc.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
30
+ return language;
31
+ }
32
+
33
+ function readStamp(): Language {
34
+ try {
35
+ return readLanguage(document);
36
+ } catch {
37
+ return 'zh';
38
+ }
39
+ }
40
+
41
+ export const LANGUAGE: Language = readStamp();
42
+
43
+ /** Select one language's table; declare tables as `zh` plus `en: typeof zh`. */
44
+ export function pick<T>(table: { readonly zh: T; readonly en: T }): T {
45
+ return LANGUAGE === 'en' ? table.en : table.zh;
46
+ }
47
+
48
+ /** The header every HTTP request carries. */
49
+ export function languageHeaders(): Record<string, string> {
50
+ return { [CONSOLE_LANGUAGE_HEADER]: LANGUAGE };
51
+ }
52
+
53
+ /** A WebSocket path with the language attached as a query parameter (the handshake cannot carry headers). */
54
+ export function withLanguage(path: string): string {
55
+ return `${path}${path.includes('?') ? '&' : '?'}${CONSOLE_LANGUAGE_QUERY}=${LANGUAGE}`;
56
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * 每次控制台挂载的资源管理器。卸载时先 abort,再逐项 dispose。
3
+ * 监听和 fetch 使用 signal;不接受 signal 的定时器、RAF、observer、AudioContext 与 ObjectURL 经 own、interval 或 frame 登记。
4
+ */
5
+
6
+ import { toDisposable, type Disposable } from '../../shared/client-panel.ts';
7
+
8
+ export class Lifecycle {
9
+ private readonly controller = new AbortController();
10
+ /** 按后进先出顺序释放资源。 */
11
+ private readonly owned: Disposable[] = [];
12
+ private closed = false;
13
+ private readonly onError: (err: unknown) => void;
14
+
15
+ /** onError 接收释放错误;单项失败不阻止后续释放。 */
16
+ constructor(onError: (err: unknown) => void = () => {}) {
17
+ this.onError = onError;
18
+ }
19
+
20
+ /** unmount 时 abort。传给 `addEventListener` / `fetch` 即可自动清理。 */
21
+ get signal(): AbortSignal {
22
+ return this.controller.signal;
23
+ }
24
+
25
+ get disposed(): boolean {
26
+ return this.closed;
27
+ }
28
+
29
+ /** 登记资源;已 dispose 时立即释放并原样返回。 */
30
+ own<T extends Disposable>(d: T): T {
31
+ if (this.closed) {
32
+ this.safely(() => d.dispose());
33
+ return d;
34
+ }
35
+ this.owned.push(d);
36
+ return d;
37
+ }
38
+
39
+ /** 包一个清理函数并登记。 */
40
+ add(cleanup: () => void): Disposable {
41
+ return this.own(toDisposable(cleanup));
42
+ }
43
+
44
+ /** 轮询。dispose 时自动停。 */
45
+ interval(fn: () => void, ms: number): Disposable {
46
+ if (this.closed) return toDisposable(() => {});
47
+ const id = setInterval(() => {
48
+ try {
49
+ fn();
50
+ } catch (err) {
51
+ this.onError(err);
52
+ }
53
+ }, ms);
54
+ return this.own(toDisposable(() => clearInterval(id)));
55
+ }
56
+
57
+ /** 一次性延时;触发后移除登记,dispose 时取消。 */
58
+ timeout(fn: () => void, ms: number): Disposable {
59
+ if (this.closed) return toDisposable(() => {});
60
+ let handle: Disposable | undefined;
61
+ const id = setTimeout(() => {
62
+ if (handle) this.forget(handle);
63
+ try {
64
+ fn();
65
+ } catch (err) {
66
+ this.onError(err);
67
+ }
68
+ }, ms);
69
+ handle = this.own(toDisposable(() => clearTimeout(id)));
70
+ return handle;
71
+ }
72
+
73
+ private forget(d: Disposable): void {
74
+ const i = this.owned.indexOf(d);
75
+ if (i >= 0) this.owned.splice(i, 1);
76
+ }
77
+
78
+ /**
79
+ * RAF 循环。`fn` 返回 `false` 即自行结束;dispose 时自动停。
80
+ * `dtMs` 是距上一帧的毫秒数,首帧为 0。
81
+ */
82
+ frame(fn: (dtMs: number) => void | false): Disposable {
83
+ if (this.closed) return toDisposable(() => {});
84
+ let raf = 0;
85
+ let last = 0;
86
+ let stopped = false;
87
+ const step = (now: number): void => {
88
+ if (stopped) return;
89
+ const dt = last === 0 ? 0 : now - last;
90
+ last = now;
91
+ let keep: void | false;
92
+ try {
93
+ keep = fn(dt);
94
+ } catch (err) {
95
+ this.onError(err);
96
+ keep = false;
97
+ }
98
+ if (keep === false) {
99
+ stopped = true;
100
+ return;
101
+ }
102
+ // 回调可能已 dispose 当前生命周期,此时不再安排下一帧。
103
+ if (stopped) return;
104
+ raf = requestAnimationFrame(step);
105
+ };
106
+ raf = requestAnimationFrame(step);
107
+ return this.own(toDisposable(() => {
108
+ stopped = true;
109
+ cancelAnimationFrame(raf);
110
+ }));
111
+ }
112
+
113
+ /** 幂等释放:先 abort 取消请求与监听,再按后进先出顺序 dispose。 */
114
+ dispose(): void {
115
+ if (this.closed) return;
116
+ this.closed = true;
117
+ this.safely(() => this.controller.abort());
118
+ for (let i = this.owned.length - 1; i >= 0; i--) {
119
+ const d = this.owned[i]!;
120
+ this.safely(() => d.dispose());
121
+ }
122
+ this.owned.length = 0;
123
+ }
124
+
125
+ private safely(fn: () => void): void {
126
+ try {
127
+ fn();
128
+ } catch (err) {
129
+ this.onError(err);
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Hash 路由集中读写 location.hash,提供解析、变更通知与离开拦截。路由只解析路径段;各段的业务含义由 host 解释。
3
+ */
4
+
5
+ import { toDisposable, type Disposable } from '../../shared/client-panel.ts';
6
+
7
+ export interface Route {
8
+ /** 路径段,已 decode。`#/provider/world%3Achat/gate` → `['provider','world:chat','gate']` */
9
+ segments: string[];
10
+ /** 查询参数(`?a=1`),已 decode */
11
+ query: Record<string, string>;
12
+ /** 原始 hash(不含前导 `#`),用于回滚 */
13
+ raw: string;
14
+ }
15
+
16
+ /** 返回一句话 = 拦下并让用户确认;返回 null = 放行。 */
17
+ export type LeaveGuard = () => string | null;
18
+
19
+ export interface RouterDeps {
20
+ win: Window;
21
+ /** 拦截时问用户。返回 true 表示确认离开。 */
22
+ confirmLeave(message: string): Promise<boolean>;
23
+ onError?(err: unknown): void;
24
+ }
25
+
26
+ export function parseHash(raw: string): Route {
27
+ const hash = raw.startsWith('#') ? raw.slice(1) : raw;
28
+ const qIdx = hash.indexOf('?');
29
+ const path = qIdx >= 0 ? hash.slice(0, qIdx) : hash;
30
+ const query: Record<string, string> = {};
31
+ if (qIdx >= 0) {
32
+ for (const pair of hash.slice(qIdx + 1).split('&')) {
33
+ if (!pair) continue;
34
+ const eq = pair.indexOf('=');
35
+ const k = eq >= 0 ? pair.slice(0, eq) : pair;
36
+ const v = eq >= 0 ? pair.slice(eq + 1) : '';
37
+ try {
38
+ query[decodeURIComponent(k)] = decodeURIComponent(v);
39
+ } catch {
40
+ query[k] = v; // 坏编码不该让整页打不开
41
+ }
42
+ }
43
+ }
44
+ const segments = path.split('/').filter((s) => s !== '').map((s) => {
45
+ try {
46
+ return decodeURIComponent(s);
47
+ } catch {
48
+ return s;
49
+ }
50
+ });
51
+ return { segments, query, raw: hash };
52
+ }
53
+
54
+ /** 比较两个 hash 是否指向同一处,忽略前导 `#`(`''` / `'#'` 都算空)。 */
55
+ function sameHash(a: string, b: string): boolean {
56
+ const norm = (h: string): string => (h.startsWith('#') ? h.slice(1) : h);
57
+ return norm(a) === norm(b);
58
+ }
59
+
60
+ export function buildHash(segments: readonly string[], query?: Record<string, string>): string {
61
+ const path = segments.map((s) => encodeURIComponent(s)).join('/');
62
+ const entries = Object.entries(query ?? {});
63
+ const qs = entries
64
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
65
+ .join('&');
66
+ return `#/${path}${qs ? `?${qs}` : ''}`;
67
+ }
68
+
69
+ export class Router {
70
+ private readonly deps: RouterDeps;
71
+ private readonly listeners = new Set<(route: Route) => void>();
72
+ private readonly guards = new Set<LeaveGuard>();
73
+ private current: Route;
74
+ /** 待处理的回拨 hash;写入相同 hash 不触发 hashchange,因此按目标值识别回拨。 */
75
+ private pendingRevert: string | null = null;
76
+ /** 确认框是否已经开着。开着时再来的变更直接拨回,不叠第二个框。 */
77
+ private confirming = false;
78
+ private started = false;
79
+
80
+ constructor(deps: RouterDeps) {
81
+ this.deps = deps;
82
+ this.current = parseHash(deps.win.location.hash);
83
+ }
84
+
85
+ get route(): Route {
86
+ return this.current;
87
+ }
88
+
89
+ start(): Disposable {
90
+ if (this.started) throw new Error('Router 已经启动');
91
+ this.started = true;
92
+ // start 时重新读取地址,构造后的地址可能已变化。
93
+ this.current = parseHash(this.deps.win.location.hash);
94
+ const handler = (): void => { void this.onHashChange(); };
95
+ this.deps.win.addEventListener('hashchange', handler);
96
+ this.emit();
97
+ return toDisposable(() => this.deps.win.removeEventListener('hashchange', handler));
98
+ }
99
+
100
+ onChange(cb: (route: Route) => void): Disposable {
101
+ this.listeners.add(cb);
102
+ return toDisposable(() => this.listeners.delete(cb));
103
+ }
104
+
105
+ /** 登记离开拦截;返回的 Disposable 必须随面板卸载释放。 */
106
+ addLeaveGuard(guard: LeaveGuard): Disposable {
107
+ this.guards.add(guard);
108
+ return toDisposable(() => this.guards.delete(guard));
109
+ }
110
+
111
+ navigate(segments: readonly string[], query?: Record<string, string>): void {
112
+ this.go(buildHash(segments, query), (hash) => { this.deps.win.location.hash = hash; });
113
+ }
114
+
115
+ /** 替换当前历史条目,不新增条目。 */
116
+ replace(segments: readonly string[], query?: Record<string, string>): void {
117
+ this.go(buildHash(segments, query), (hash) => this.deps.win.location.replace(hash));
118
+ }
119
+
120
+ private go(next: string, write: (hash: string) => void): void {
121
+ // 按地址栏判重;写入相同 hash 不触发 hashchange。
122
+ if (sameHash(this.deps.win.location.hash, next)) {
123
+ if (!sameHash(this.current.raw, next)) {
124
+ this.current = parseHash(next);
125
+ this.emit();
126
+ }
127
+ return;
128
+ }
129
+ write(next);
130
+ }
131
+
132
+ /** 回拨地址;当前地址已相同则不写入。 */
133
+ private revertTo(raw: string): void {
134
+ const target = `#${raw}`;
135
+ if (sameHash(this.deps.win.location.hash, target)) {
136
+ this.pendingRevert = null;
137
+ return;
138
+ }
139
+ this.pendingRevert = target;
140
+ this.deps.win.location.hash = target;
141
+ }
142
+
143
+ private firstBlock(): string | null {
144
+ for (const g of this.guards) {
145
+ let msg: string | null = null;
146
+ try {
147
+ msg = g();
148
+ } catch (err) {
149
+ this.deps.onError?.(err);
150
+ }
151
+ if (msg) return msg;
152
+ }
153
+ return null;
154
+ }
155
+
156
+ private async onHashChange(): Promise<void> {
157
+ const win = this.deps.win;
158
+ // 这次变更是我们自己拨回去造成的,不算一次导航。
159
+ if (this.pendingRevert !== null && sameHash(win.location.hash, this.pendingRevert)) {
160
+ this.pendingRevert = null;
161
+ return;
162
+ }
163
+ // 已经在问用户了:再来的变更一律先拨回去,不叠第二个确认框。
164
+ if (this.confirming) {
165
+ this.revertTo(this.current.raw);
166
+ return;
167
+ }
168
+
169
+ const block = this.firstBlock();
170
+ if (block) {
171
+ this.confirming = true;
172
+ let ok = false;
173
+ try {
174
+ ok = await this.deps.confirmLeave(block);
175
+ } catch (err) {
176
+ this.deps.onError?.(err); // 问不出结果就当没同意
177
+ } finally {
178
+ this.confirming = false;
179
+ }
180
+ if (!ok) {
181
+ this.revertTo(this.current.raw);
182
+ return;
183
+ }
184
+ // 用户确认离开:拦截器归它自己的面板所有,unmount 时会解除,这里不动它。
185
+ }
186
+ this.current = parseHash(win.location.hash);
187
+ this.emit();
188
+ }
189
+
190
+ private emit(): void {
191
+ for (const cb of [...this.listeners]) {
192
+ try {
193
+ cb(this.current);
194
+ } catch (err) {
195
+ this.deps.onError?.(err);
196
+ }
197
+ }
198
+ }
199
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * 面板的 WebSocket 流客户端。断线后退避重连,CONNECTING 期间的 send 排队;unmount 关闭连接并取消待执行的重连定时器。
3
+ */
4
+
5
+ import type { ConsoleStreamHandle, ConsoleStreamHandlers } from '../../shared/client-panel.ts';
6
+
7
+ /** 只用到 WebSocket 的这几样;注入假件即可测。 */
8
+ export interface SocketLike {
9
+ send(data: string): void;
10
+ close(): void;
11
+ readyState: number;
12
+ onopen: ((this: unknown, ev: unknown) => unknown) | null;
13
+ onclose: ((this: unknown, ev: unknown) => unknown) | null;
14
+ onerror: ((this: unknown, ev: unknown) => unknown) | null;
15
+ onmessage: ((this: unknown, ev: { data: unknown }) => unknown) | null;
16
+ }
17
+
18
+ export interface StreamOptions {
19
+ url: string;
20
+ handlers: ConsoleStreamHandlers;
21
+ /** 面板的 signal。abort = 连同重连一起停。 */
22
+ signal: AbortSignal;
23
+ createSocket(url: string): SocketLike;
24
+ /** 退避定时器;注入以便测试用假时钟。 */
25
+ setTimer(fn: () => void, ms: number): number;
26
+ clearTimer(id: number): void;
27
+ onError?(err: unknown): void;
28
+ }
29
+
30
+ const OPEN = 1;
31
+ const BACKOFF_START_MS = 500;
32
+ const BACKOFF_MAX_MS = 15_000;
33
+
34
+ export function openStream(opts: StreamOptions): ConsoleStreamHandle {
35
+ /** 连上之前攒着的帧。 */
36
+ const outbox: string[] = [];
37
+ let socket: SocketLike | null = null;
38
+ let timer: number | null = null;
39
+ let backoff = BACKOFF_START_MS;
40
+ let stopped = false;
41
+
42
+ const stop = (): void => {
43
+ if (stopped) return;
44
+ stopped = true;
45
+ if (timer !== null) {
46
+ opts.clearTimer(timer);
47
+ timer = null;
48
+ }
49
+ const s = socket;
50
+ socket = null;
51
+ if (s) {
52
+ // 先摘回调再关:close() 会同步触发 onclose,那时候不该再排重连。
53
+ s.onopen = null;
54
+ s.onclose = null;
55
+ s.onerror = null;
56
+ s.onmessage = null;
57
+ try {
58
+ s.close();
59
+ } catch (err) {
60
+ opts.onError?.(err);
61
+ }
62
+ }
63
+ outbox.length = 0;
64
+ };
65
+
66
+ const safely = (fn: () => void): void => {
67
+ try {
68
+ fn();
69
+ } catch (err) {
70
+ opts.onError?.(err);
71
+ }
72
+ };
73
+
74
+ const scheduleReconnect = (): void => {
75
+ if (stopped || timer !== null) return;
76
+ // 全额抖动:退避上限内均匀取一点,避免多个面板同时重连撞在一起。
77
+ const wait = Math.round(backoff * (0.5 + Math.random() * 0.5));
78
+ backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
79
+ timer = opts.setTimer(() => {
80
+ timer = null;
81
+ connect();
82
+ }, wait);
83
+ };
84
+
85
+ function connect(): void {
86
+ if (stopped) return;
87
+ let s: SocketLike;
88
+ try {
89
+ s = opts.createSocket(opts.url);
90
+ } catch (err) {
91
+ opts.onError?.(err);
92
+ scheduleReconnect();
93
+ return;
94
+ }
95
+ socket = s;
96
+
97
+ s.onopen = () => {
98
+ if (stopped) return;
99
+ backoff = BACKOFF_START_MS; // 连上了才算数,否则一直翻倍
100
+ const queued = outbox.splice(0, outbox.length);
101
+ for (const text of queued) {
102
+ safely(() => s.send(text));
103
+ }
104
+ if (opts.handlers.open) safely(() => opts.handlers.open!());
105
+ };
106
+
107
+ s.onmessage = (ev) => {
108
+ if (stopped) return;
109
+ const data = ev?.data;
110
+ safely(() => opts.handlers.message(typeof data === 'string' ? data : String(data)));
111
+ };
112
+
113
+ s.onerror = (err) => {
114
+ opts.onError?.(err);
115
+ // 不在这里重连:onerror 之后浏览器一定还会给一次 onclose。
116
+ };
117
+
118
+ s.onclose = () => {
119
+ if (socket === s) socket = null;
120
+ if (stopped) return;
121
+ if (opts.handlers.close) safely(() => opts.handlers.close!(true));
122
+ scheduleReconnect();
123
+ };
124
+ }
125
+
126
+ if (opts.signal.aborted) {
127
+ stopped = true;
128
+ } else {
129
+ opts.signal.addEventListener('abort', () => {
130
+ const wasRunning = !stopped;
131
+ stop();
132
+ if (wasRunning && opts.handlers.close) safely(() => opts.handlers.close!(false));
133
+ }, { once: true });
134
+ connect();
135
+ }
136
+
137
+ const handle: ConsoleStreamHandle = {
138
+ send(text: string): void {
139
+ if (stopped) return;
140
+ const s = socket;
141
+ if (s && s.readyState === OPEN) {
142
+ safely(() => s.send(text));
143
+ } else {
144
+ outbox.push(text);
145
+ }
146
+ },
147
+ get open(): boolean {
148
+ return !stopped && socket?.readyState === OPEN;
149
+ },
150
+ dispose(): void {
151
+ const wasRunning = !stopped;
152
+ stop();
153
+ if (wasRunning && opts.handlers.close) safely(() => opts.handlers.close!(false));
154
+ },
155
+ };
156
+ return handle;
157
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 框架 /ws/debug 与 /ws/sessions 的 JSON WebSocket 入口,复用 core/stream 的退避、发送排队和生命周期取消。
3
+ * 按页面协议选择 ws/wss;文本帧解析为 JSON,坏帧报告 onError 后丢弃;网络状态通过回调交给调用方呈现。
4
+ */
5
+
6
+ import type { ConsoleStreamHandle } from '../../shared/client-panel.ts';
7
+ import { withLanguage } from './language.ts';
8
+ import type { Lifecycle } from './lifecycle.ts';
9
+ import { openStream, type SocketLike } from './stream.ts';
10
+
11
+ export interface SocketEnv {
12
+ /** `/ws/debug` → 绝对地址 */
13
+ wsUrl(path: string): string;
14
+ createSocket(url: string): SocketLike;
15
+ setTimer(fn: () => void, ms: number): number;
16
+ clearTimer(id: number): void;
17
+ }
18
+
19
+ export interface LocationLike {
20
+ protocol: string;
21
+ host: string;
22
+ }
23
+
24
+ /** HTTPS 页面使用 WSS。 */
25
+ export function wsUrlOf(loc: LocationLike, path: string): string {
26
+ const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
27
+ return `${proto}//${loc.host}${path}`;
28
+ }
29
+
30
+ /** typeof globalThis 提供 WebSocket 构造器类型。 */
31
+ export function browserSocketEnv(win: Window & typeof globalThis): SocketEnv {
32
+ return {
33
+ wsUrl: (path) => wsUrlOf(win.location, withLanguage(path)),
34
+ createSocket: (url) => new win.WebSocket(url) as unknown as SocketLike,
35
+ setTimer: (fn, ms) => win.setTimeout(fn, ms),
36
+ clearTimer: (id) => win.clearTimeout(id),
37
+ };
38
+ }
39
+
40
+ export interface FrameworkSocketOptions {
41
+ /** `/ws/debug` / `/ws/sessions` */
42
+ path: string;
43
+ /** abort 停止连接与重连;句柄登记在该 lifecycle。 */
44
+ lifecycle: Lifecycle;
45
+ /** 一帧(已解析)。非对象的帧(数组、裸字符串)不会送到这儿。 */
46
+ onFrame(frame: Readonly<Record<string, unknown>>): void;
47
+ /** 连接和断线通知;主动关闭不报告断线。 */
48
+ onNet?(online: boolean): void;
49
+ onError?(err: unknown): void;
50
+ env: SocketEnv;
51
+ }
52
+
53
+ /** 返回的通道句柄已登记在 lifecycle,可提前 dispose。 */
54
+ export function openFrameworkSocket(opts: FrameworkSocketOptions): ConsoleStreamHandle {
55
+ const { env } = opts;
56
+ const handle = openStream({
57
+ url: env.wsUrl(opts.path),
58
+ signal: opts.lifecycle.signal,
59
+ createSocket: env.createSocket,
60
+ setTimer: env.setTimer,
61
+ clearTimer: env.clearTimer,
62
+ ...(opts.onError ? { onError: opts.onError } : {}),
63
+ handlers: {
64
+ open: () => opts.onNet?.(true),
65
+ close: (willRetry) => {
66
+ if (willRetry) opts.onNet?.(false);
67
+ },
68
+ message: (text) => {
69
+ let frame: unknown;
70
+ try {
71
+ frame = JSON.parse(text);
72
+ } catch (err) {
73
+ opts.onError?.(err);
74
+ return;
75
+ }
76
+ if (!frame || typeof frame !== 'object' || Array.isArray(frame)) return;
77
+ opts.onFrame(frame as Record<string, unknown>);
78
+ },
79
+ },
80
+ });
81
+ return opts.lifecycle.own(handle);
82
+ }