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,273 @@
1
+ /** 控制台入口:贡献页路由交给 ConsolePageHost,框架路由交给对应 feature;空路由默认打开终端。 */
2
+
3
+ import { fetchManifest, get } from './core/api.ts';
4
+ import { pick, withLanguage } from './core/language.ts';
5
+ import { Lifecycle } from './core/lifecycle.ts';
6
+ import { Router, type Route } from './core/router.ts';
7
+ import type { SocketLike } from './core/stream.ts';
8
+ import { wsUrlOf } from './core/websocket.ts';
9
+ import { BUILTIN_PANELS } from './console-pages/builtins.ts';
10
+ import { ConsolePageHost, PROVIDER_ROUTE } from './console-pages/host.ts';
11
+ import { ConsolePageLoader } from './console-pages/loader.ts';
12
+ import { createConsoleUi } from './ui/index.ts';
13
+ import { subscribeLamps } from './ui/lamp.ts';
14
+ import { applyStoredTheme } from './theme/studio.ts';
15
+ import { createShell } from './shell/index.ts';
16
+ import { featureAvailable, type FeatureContext, type FrameworkFeature } from './features/feature.ts';
17
+ import { liveFeature } from './features/live/index.ts';
18
+ import { coreFeature } from './features/core/index.ts';
19
+ import { usageFeature } from './features/usage/index.ts';
20
+ import { providersFeature } from './features/providers/index.ts';
21
+ import { worldsFeature } from './features/worlds/index.ts';
22
+ import { extensionsFeature } from './features/extensions/index.ts';
23
+ import { appearanceFeature } from './features/appearance/index.ts';
24
+ import { promptsFeature } from './features/prompts/index.ts';
25
+ import { settingsFeature } from './features/settings/index.ts';
26
+ import type { ConsoleMemo } from '../shared/client-panel.ts';
27
+
28
+ /**
29
+ * 控制台自己的页面。与贡献方的页无关——那一路完全由 manifest 驱动。
30
+ *
31
+ * 顺序即左栏顺序。`hidden` 的页面(外观)不进左栏,
32
+ * 但仍要在这张表里:路由分派只认这张表,设置页里嵌着它的同时,直达链接也要能开。
33
+ */
34
+ export const FEATURES: readonly FrameworkFeature[] = [
35
+ liveFeature, coreFeature, usageFeature, providersFeature,
36
+ worldsFeature, extensionsFeature, promptsFeature, appearanceFeature,
37
+ settingsFeature,
38
+ ];
39
+
40
+ /** feature 挂载抛错时那张错误卡的标题。 */
41
+ const featureLoadFailed = pick({
42
+ zh: (label: string) => `「${label}」没能加载`,
43
+ en: (label: string) => `"${label}" failed to load`,
44
+ });
45
+
46
+ /** localStorage 后端;无痕模式下静默降级成内存,不抛。 */
47
+ export function createMemo(prefix: string): ConsoleMemo {
48
+ const fallback = new Map<string, unknown>();
49
+ return {
50
+ get<T>(key: string, dflt: T): T {
51
+ const k = prefix + key;
52
+ // 本会话写过的值优先:localStorage 写不进去(无痕、配额满)时只有内存表是新的。
53
+ if (fallback.has(k)) return fallback.get(k) as T;
54
+ try {
55
+ const raw = localStorage.getItem(k);
56
+ return raw === null ? dflt : (JSON.parse(raw) as T);
57
+ } catch {
58
+ return dflt;
59
+ }
60
+ },
61
+ set(key: string, value: unknown): void {
62
+ const k = prefix + key;
63
+ fallback.set(k, value);
64
+ try {
65
+ localStorage.setItem(k, JSON.stringify(value));
66
+ } catch { /* 无痕/配额满:这轮只留在内存里 */ }
67
+ },
68
+ };
69
+ }
70
+
71
+ export function boot(doc: Document = document): { dispose(): void } {
72
+ // 首次渲染前应用主题。
73
+ try {
74
+ applyStoredTheme(doc);
75
+ } catch { /* 主题读坏了不该拦住整个控制台 */ }
76
+
77
+ // 首屏提示已经完成使命——内核跑起来了。摘不掉它才说明脚本没起来。
78
+ doc.getElementById('boot-note')?.remove();
79
+
80
+ let root = doc.getElementById('kernel-root');
81
+ if (!root) {
82
+ root = doc.createElement('div');
83
+ root.id = 'kernel-root';
84
+ doc.body.appendChild(root);
85
+ }
86
+ /**
87
+ * 贡献方的页与 framework feature 各自拥有独立根容器。ConsolePageHost.unmount() 清空其根节点时,不得影响 feature 的 DOM。
88
+ */
89
+ const pageRoot = doc.createElement('div');
90
+ const featureRoot = doc.createElement('div');
91
+ root.replaceChildren(pageRoot, featureRoot);
92
+
93
+ const onError = (err: unknown): void => {
94
+ console.error('[console]', err);
95
+ };
96
+
97
+ const loader = new ConsolePageLoader({
98
+ importModule: (url) => import(/* @vite-ignore */ url),
99
+ styleHost: doc.head,
100
+ createLink: () => doc.createElement('link'),
101
+ log: (msg, detail) => console.warn('[console]', msg, detail ?? ''),
102
+ });
103
+
104
+ const router = new Router({
105
+ win: window,
106
+ confirmLeave: async (message) => window.confirm(message),
107
+ onError,
108
+ });
109
+
110
+ const memo = createMemo('cortico.panel.');
111
+
112
+ const hostDeps = {
113
+ doc,
114
+ overlayHost: doc.body,
115
+ loader,
116
+ builtins: BUILTIN_PANELS,
117
+ router,
118
+ fetchManifest: () => fetchManifest(),
119
+ memo,
120
+ createSocket: (url: string) => new WebSocket(url) as unknown as SocketLike,
121
+ wsUrl: (path: string) => wsUrlOf(location, withLanguage(path)),
122
+ onError,
123
+ };
124
+ const host = new ConsolePageHost({ ...hostDeps, root: pageRoot });
125
+ /** 框架页里嵌别的页的面板用的宿主:同一套加载器与 memo,只换容器与路由前缀。 */
126
+ const consolePageHost: NonNullable<FeatureContext['consolePageHost']> = (opts) =>
127
+ new ConsolePageHost({ ...hostDeps, root: opts.root, route: opts.route });
128
+
129
+ /**
130
+ * 左栏外壳。它自己不探活、不认识任何具体 World:框架页那段由 FEATURES 按
131
+ * capability 过滤,贡献方那段完全由 manifest 驱动。
132
+ */
133
+ const shellLife = new Lifecycle(onError);
134
+ const shell = createShell({
135
+ doc,
136
+ ui: createConsoleUi({ memo, overlayHost: doc.body, signal: shellLife.signal, doc }),
137
+ router,
138
+ features: FEATURES,
139
+ onError,
140
+ });
141
+ doc.body.insertBefore(shell.el, doc.body.firstChild);
142
+ const offNav = host.onNavChange(() => shell.setPages(host.pages));
143
+
144
+ /**
145
+ * 状态灯的活数据。manifest 只在开页与显式刷新时取,而灯要跟得上"引擎起来了没",
146
+ * 所以走那条只回灯的轻端点(节拍与不叠发都归 `subscribeLamps`)。
147
+ */
148
+ shellLife.own(subscribeLamps(doc, (lamps) => shell.setLamps(lamps)));
149
+
150
+ /** 框架能力清单;获取失败时不启用可选能力。 */
151
+ let capabilities: Record<string, boolean> = {};
152
+ /** capabilities 与 manifest 都到齐了吗。到齐之前不渲染任何一页。 */
153
+ let ready = false;
154
+ /** 到齐之前就 dispose 了:那一拍回来什么都不做。 */
155
+ let disposed = false;
156
+
157
+ /** 当前挂着的 framework feature(贡献方那边由 host 自己管)。 */
158
+ let mounted: { route: string; lifecycle: Lifecycle } | null = null;
159
+ let generation = 0;
160
+
161
+ const unmountFeature = (): void => {
162
+ const cur = mounted;
163
+ mounted = null;
164
+ generation++;
165
+ if (cur) cur.lifecycle.dispose();
166
+ featureRoot.replaceChildren();
167
+ };
168
+
169
+ const findFeature = (name: string | undefined): FrameworkFeature | undefined =>
170
+ name === undefined ? undefined : FEATURES.find((f) => f.route === name);
171
+
172
+ const mountFeature = (feature: FrameworkFeature, route: Route): void => {
173
+ unmountFeature();
174
+ const gen = generation;
175
+ const lifecycle = new Lifecycle(onError);
176
+ mounted = { route: feature.route, lifecycle };
177
+
178
+ const slot = doc.createElement('div');
179
+ slot.className = `featureslot featureslot-${feature.route}`;
180
+ featureRoot.appendChild(slot);
181
+
182
+ const ui = createConsoleUi({ memo, overlayHost: doc.body, signal: lifecycle.signal, doc });
183
+ void (async () => {
184
+ try {
185
+ const out = await feature.mount({
186
+ root: slot, lifecycle, signal: lifecycle.signal, ui, router, route,
187
+ capabilities, onError, consolePageHost,
188
+ refreshNav: () => host.refresh(),
189
+ });
190
+ if (gen !== generation) {
191
+ if (out && typeof out.dispose === 'function') out.dispose();
192
+ return;
193
+ }
194
+ if (out && typeof out.dispose === 'function') lifecycle.own(out);
195
+ } catch (err) {
196
+ if (gen !== generation) return;
197
+ onError(err);
198
+ lifecycle.dispose();
199
+ slot.replaceChildren();
200
+ const card = ui.sheet({ title: featureLoadFailed(feature.label), en: 'feature error' });
201
+ card.body.appendChild(ui.msgline(err instanceof Error ? err.message : String(err), true));
202
+ slot.appendChild(card.el);
203
+ }
204
+ })();
205
+ };
206
+
207
+ const apply = (route: Route): void => {
208
+ shell.setRoute(route);
209
+ // 页面加载依赖完整的 capabilities;就绪后重新应用当前路由。
210
+ if (!ready) return;
211
+ // 空路由替换为终端页,不增加历史条目。
212
+ if (route.segments.length === 0) {
213
+ router.replace(['live']);
214
+ return;
215
+ }
216
+ const head = route.segments[0];
217
+
218
+ if (head === PROVIDER_ROUTE) {
219
+ unmountFeature();
220
+ const pageId = route.segments[1];
221
+ if (!pageId) { host.unmount(); return; }
222
+ void host.show(pageId, route.segments[2]);
223
+ return;
224
+ }
225
+
226
+ const feature = findFeature(head);
227
+ if (feature && featureAvailable(feature, capabilities)) {
228
+ host.unmount();
229
+ // 同一个 feature 内部换子页签(segments[1] 变)由它自己处理,不重挂。
230
+ if (mounted?.route === feature.route) return;
231
+ mountFeature(feature, route);
232
+ return;
233
+ }
234
+
235
+ // 没人认领:清空台面。左栏仍在,导航照常可用。
236
+ host.unmount();
237
+ unmountFeature();
238
+ };
239
+
240
+ const offRoute = router.onChange(apply);
241
+ const stopRouter = router.start();
242
+
243
+ // capabilities 与 manifest 就绪后按当前路由渲染。
244
+ void Promise.allSettled([
245
+ get<{ capabilities?: Record<string, boolean> }>('/api/capabilities')
246
+ .then((r) => { capabilities = r?.capabilities ?? {}; }),
247
+ host.load(),
248
+ ]).then(() => {
249
+ if (disposed) return;
250
+ ready = true;
251
+ shell.setCapabilities(capabilities);
252
+ shell.setPages(host.pages);
253
+ apply(router.route);
254
+ });
255
+
256
+ return {
257
+ dispose(): void {
258
+ disposed = true;
259
+ offNav.dispose();
260
+ shell.dispose();
261
+ shellLife.dispose();
262
+ offRoute.dispose();
263
+ stopRouter.dispose();
264
+ unmountFeature();
265
+ host.unmount();
266
+ },
267
+ };
268
+ }
269
+
270
+ // 作为 bundle 入口被加载时自动启动:真页面带着 boot-note。测试 import 时没有它,不自动跑。
271
+ if (typeof document !== 'undefined' && document.getElementById('boot-note')) {
272
+ boot();
273
+ }
@@ -0,0 +1,233 @@
1
+ import type { ConsoleUi } from '../../shared/client-panel.ts';
2
+ import { postBlob } from '../core/api.ts';
3
+ import { pick } from '../core/language.ts';
4
+ import { icon } from '../ui/icons.ts';
5
+
6
+ const EDITOR_SIZE = 300;
7
+ const OUTPUT_SIZE = 512;
8
+
9
+ const zh = {
10
+ changeAvatar: '更换 bot 头像',
11
+ uploadAvatar: '上传并裁剪 bot 头像',
12
+ unreadable: '无法读取这张图片',
13
+ cropPreview: '头像裁剪预览',
14
+ zoomAria: '头像缩放',
15
+ zoom: '缩放',
16
+ note: '拖动图片调整位置,滚动鼠标滚轮或使用滑杆缩放;保存后写入 bot 根目录的 avatar.png。',
17
+ cancel: '取消',
18
+ save: '保存头像',
19
+ cropTitle: '裁剪头像',
20
+ noCanvas: '浏览器无法创建图片画布',
21
+ saved: '头像已保存',
22
+ };
23
+ const en: typeof zh = {
24
+ changeAvatar: 'Change bot avatar',
25
+ uploadAvatar: 'Upload and crop the bot avatar',
26
+ unreadable: 'Could not read this image',
27
+ cropPreview: 'Avatar crop preview',
28
+ zoomAria: 'Avatar zoom',
29
+ zoom: 'Zoom',
30
+ note: 'Drag the image to reposition; use the mouse wheel or the slider to zoom. Saving writes avatar.png to the bot root directory.',
31
+ cancel: 'Cancel',
32
+ save: 'Save avatar',
33
+ cropTitle: 'Crop avatar',
34
+ noCanvas: 'The browser could not create an image canvas',
35
+ saved: 'Avatar saved',
36
+ };
37
+ const S = pick({ zh, en });
38
+
39
+ export interface AvatarControl {
40
+ readonly el: HTMLDivElement;
41
+ setLabel(label: string): void;
42
+ }
43
+
44
+ export function createAvatarControl(opts: {
45
+ doc: Document;
46
+ ui: ConsoleUi;
47
+ signal: AbortSignal;
48
+ onError(err: unknown): void;
49
+ }): AvatarControl {
50
+ const { doc, ui, signal } = opts;
51
+ const el = ui.h('div', 'rail-avatar-wrap');
52
+ const button = ui.h('button', 'rail-avatar');
53
+ button.type = 'button';
54
+ button.setAttribute('aria-label', S.changeAvatar);
55
+ button.title = S.uploadAvatar;
56
+
57
+ const image = ui.h('img', 'rail-avatar-image');
58
+ image.alt = '';
59
+ image.src = `/api/avatar?v=${Date.now()}`;
60
+ const fallback = ui.h('span', 'rail-avatar-fallback', 'B');
61
+ const edit = ui.h('span', 'rail-avatar-edit');
62
+ edit.appendChild(icon(doc, 'image'));
63
+ button.append(image, fallback, edit);
64
+
65
+ const showImage = (shown: boolean): void => {
66
+ image.hidden = !shown;
67
+ fallback.hidden = shown;
68
+ };
69
+ image.addEventListener('load', () => showImage(true), { signal });
70
+ image.addEventListener('error', () => showImage(false), { signal });
71
+
72
+ const picker = ui.h('input');
73
+ picker.type = 'file';
74
+ picker.accept = 'image/png,image/jpeg,image/webp,image/svg+xml';
75
+ picker.className = 'visually-hidden';
76
+ picker.setAttribute('aria-hidden', 'true');
77
+ button.addEventListener('click', () => picker.click(), { signal });
78
+ picker.addEventListener('change', () => {
79
+ const file = picker.files?.[0];
80
+ picker.value = '';
81
+ if (file) openEditor(file);
82
+ }, { signal });
83
+ el.append(button, picker);
84
+
85
+ function openEditor(file: File): void {
86
+ const source = new Image();
87
+ const sourceUrl = URL.createObjectURL(file);
88
+ source.onload = () => {
89
+ URL.revokeObjectURL(sourceUrl);
90
+ mountEditor(source);
91
+ };
92
+ source.onerror = () => {
93
+ URL.revokeObjectURL(sourceUrl);
94
+ ui.toast(S.unreadable, 'bad');
95
+ };
96
+ source.src = sourceUrl;
97
+ }
98
+
99
+ function mountEditor(source: HTMLImageElement): void {
100
+ const body = ui.h('div', 'avatar-editor');
101
+ const canvas = ui.h('canvas', 'avatar-canvas');
102
+ canvas.width = EDITOR_SIZE * 2;
103
+ canvas.height = EDITOR_SIZE * 2;
104
+ canvas.setAttribute('aria-label', S.cropPreview);
105
+ const zoom = ui.h('input', 'avatar-zoom');
106
+ zoom.type = 'range';
107
+ zoom.min = '1';
108
+ zoom.max = '3';
109
+ zoom.step = '0.01';
110
+ zoom.value = '1';
111
+ zoom.setAttribute('aria-label', S.zoomAria);
112
+ const controls = ui.h('label', 'avatar-zoom-row');
113
+ controls.append(ui.h('span', null, S.zoom), zoom);
114
+ const note = ui.h('p', 'avatar-note', S.note);
115
+ const actions = ui.actions();
116
+ const cancel = ui.button(S.cancel);
117
+ const save = ui.button(S.save, { variant: 'primary' });
118
+ actions.append(ui.h('span', 'grow'), cancel, save);
119
+ body.append(canvas, controls, note, actions);
120
+ const drawer = ui.drawer(S.cropTitle, body);
121
+
122
+ const ctx = canvas.getContext('2d');
123
+ if (!ctx) {
124
+ drawer.dispose();
125
+ ui.toast(S.noCanvas, 'bad');
126
+ return;
127
+ }
128
+ const cover = Math.max(EDITOR_SIZE / source.naturalWidth, EDITOR_SIZE / source.naturalHeight);
129
+ let factor = 1;
130
+ let offsetX = 0;
131
+ let offsetY = 0;
132
+ let drag: { x: number; y: number; ox: number; oy: number } | null = null;
133
+
134
+ const clampOffsets = (): void => {
135
+ const width = source.naturalWidth * cover * factor;
136
+ const height = source.naturalHeight * cover * factor;
137
+ const maxX = Math.max(0, (width - EDITOR_SIZE) / 2);
138
+ const maxY = Math.max(0, (height - EDITOR_SIZE) / 2);
139
+ offsetX = Math.max(-maxX, Math.min(maxX, offsetX));
140
+ offsetY = Math.max(-maxY, Math.min(maxY, offsetY));
141
+ };
142
+ const draw = (): void => {
143
+ clampOffsets();
144
+ const scale = cover * factor;
145
+ const width = source.naturalWidth * scale;
146
+ const height = source.naturalHeight * scale;
147
+ ctx.setTransform(2, 0, 0, 2, 0, 0);
148
+ ctx.clearRect(0, 0, EDITOR_SIZE, EDITOR_SIZE);
149
+ ctx.drawImage(
150
+ source,
151
+ (EDITOR_SIZE - width) / 2 + offsetX,
152
+ (EDITOR_SIZE - height) / 2 + offsetY,
153
+ width,
154
+ height,
155
+ );
156
+ };
157
+
158
+ zoom.addEventListener('input', () => {
159
+ factor = Number(zoom.value);
160
+ draw();
161
+ }, { signal });
162
+ canvas.addEventListener('wheel', (event) => {
163
+ event.preventDefault();
164
+ const rect = canvas.getBoundingClientRect();
165
+ const pointX = event.clientX - rect.left - rect.width / 2;
166
+ const pointY = event.clientY - rect.top - rect.height / 2;
167
+ const next = Math.max(1, Math.min(3, factor * Math.exp(-event.deltaY * 0.0015)));
168
+ const ratio = next / factor;
169
+ offsetX = pointX - (pointX - offsetX) * ratio;
170
+ offsetY = pointY - (pointY - offsetY) * ratio;
171
+ factor = next;
172
+ zoom.value = factor.toFixed(2);
173
+ draw();
174
+ }, { passive: false, signal });
175
+ canvas.addEventListener('pointerdown', (event) => {
176
+ drag = { x: event.clientX, y: event.clientY, ox: offsetX, oy: offsetY };
177
+ canvas.setPointerCapture(event.pointerId);
178
+ }, { signal });
179
+ canvas.addEventListener('pointermove', (event) => {
180
+ if (!drag) return;
181
+ offsetX = drag.ox + event.clientX - drag.x;
182
+ offsetY = drag.oy + event.clientY - drag.y;
183
+ draw();
184
+ }, { signal });
185
+ const endDrag = (): void => { drag = null; };
186
+ canvas.addEventListener('pointerup', endDrag, { signal });
187
+ canvas.addEventListener('pointercancel', endDrag, { signal });
188
+ cancel.addEventListener('click', () => drawer.dispose(), { signal });
189
+ save.addEventListener('click', () => {
190
+ const output = doc.createElement('canvas');
191
+ output.width = OUTPUT_SIZE;
192
+ output.height = OUTPUT_SIZE;
193
+ const out = output.getContext('2d');
194
+ if (!out) return;
195
+ const ratio = OUTPUT_SIZE / EDITOR_SIZE;
196
+ const scale = cover * factor * ratio;
197
+ const width = source.naturalWidth * scale;
198
+ const height = source.naturalHeight * scale;
199
+ out.drawImage(
200
+ source,
201
+ (OUTPUT_SIZE - width) / 2 + offsetX * ratio,
202
+ (OUTPUT_SIZE - height) / 2 + offsetY * ratio,
203
+ width,
204
+ height,
205
+ );
206
+ output.toBlob((blob) => {
207
+ if (!blob) return;
208
+ const lock = ui.disable(save);
209
+ void postBlob<{ ok: boolean }>('/api/avatar', blob, { signal }).then(
210
+ () => {
211
+ image.src = `/api/avatar?v=${Date.now()}`;
212
+ showImage(true);
213
+ drawer.dispose();
214
+ ui.toast(S.saved);
215
+ },
216
+ (err) => {
217
+ if ((err as { name?: string } | null)?.name === 'AbortError') return;
218
+ opts.onError(err);
219
+ ui.toast(err instanceof Error ? err.message : String(err), 'bad');
220
+ },
221
+ ).finally(() => lock.dispose());
222
+ }, 'image/png');
223
+ }, { signal });
224
+ draw();
225
+ }
226
+
227
+ return {
228
+ el,
229
+ setLabel(label): void {
230
+ fallback.textContent = label.trim().slice(0, 1).toUpperCase() || 'B';
231
+ },
232
+ };
233
+ }