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,428 @@
1
+ /**
2
+ * 按 ConfigGroup JSON Schema 渲染配置,供框架设置与贡献页使用。
3
+ * 可编辑 integer、number、boolean、string(enum / x-options)和双数值数组;其余只读且不提交。
4
+ * x-scale:显示值 = 存储值 / scale,提交时乘回;取整与范围校验由后端完成。
5
+ * x-suffix 提供单位;x-hot=false 的 World 配置需重启 World,其余需重启进程。
6
+ * x-options 动态获取候选;x-path 选择服务器本机路径;x-download 提供浏览器下载链接。
7
+ */
8
+
9
+ import type { ConsoleUi, Disposable } from '../../../shared/client-panel.ts';
10
+ import { get, pickPath, post } from '../../core/api.ts';
11
+ import type { Lifecycle } from '../../core/lifecycle.ts';
12
+ import { icon } from '../../ui/icons.ts';
13
+ import { S } from './strings.ts';
14
+
15
+ /** 连续输入的请求去抖间隔。 */
16
+ const SAVE_DEBOUNCE_MS = 400;
17
+
18
+ /** 一条属性的声明。字段与 `src/core/config-schema.ts` 的 `ConfigProperty` 同形。 */
19
+ export interface ConfigProperty {
20
+ type?: string;
21
+ title?: string;
22
+ description?: string;
23
+ minimum?: number;
24
+ maximum?: number;
25
+ multipleOf?: number;
26
+ enum?: string[];
27
+ items?: { type?: string; minimum?: number; maximum?: number };
28
+ nullable?: boolean;
29
+ 'x-scale'?: number;
30
+ 'x-suffix'?: string;
31
+ 'x-hot'?: boolean;
32
+ 'x-options'?: string;
33
+ 'x-path'?: {
34
+ kind: 'file' | 'directory';
35
+ extensions?: string[];
36
+ recommendedDir?: string;
37
+ };
38
+ 'x-download'?: {
39
+ href: string;
40
+ label?: string;
41
+ };
42
+ }
43
+
44
+ type OptionItem = { value: string; label: string };
45
+
46
+ /**
47
+ * `/api/config/options/:kind` 给的整张表原样画;当前值不在表里也留下,避免写不回。
48
+ * "系统默认 / 静音"这类固定项也由声明该 kind 的一方随表给出——控制台不认识任何 kind。
49
+ */
50
+ function mergeOptionList(live: OptionItem[], current: string): OptionItem[] {
51
+ const seen = new Set<string>();
52
+ const out: OptionItem[] = [];
53
+ const add = (value: string, label: string): void => {
54
+ if (seen.has(value)) return;
55
+ seen.add(value);
56
+ out.push({ value, label });
57
+ };
58
+ for (const item of live) add(item.value, item.label);
59
+ if (!seen.has(current)) add(current, current || S.optionCurrent);
60
+ return out;
61
+ }
62
+
63
+ function fillSelect(ui: ConsoleUi, sel: HTMLSelectElement, items: OptionItem[], current: string): void {
64
+ sel.replaceChildren();
65
+ for (const item of items) {
66
+ const opt = ui.h('option', null, item.label);
67
+ opt.value = item.value;
68
+ sel.appendChild(opt);
69
+ }
70
+ sel.value = current;
71
+ }
72
+
73
+ export interface ConfigGroup {
74
+ id: string;
75
+ owner: string;
76
+ schema: {
77
+ title?: string;
78
+ description?: string;
79
+ properties?: Record<string, ConfigProperty>;
80
+ };
81
+ }
82
+
83
+ export type ConfigValue = number | boolean | string | null | [number, number];
84
+
85
+ export interface ConfigGroupEntry {
86
+ group: ConfigGroup;
87
+ values?: Record<string, ConfigValue>;
88
+ }
89
+
90
+ /**
91
+ * 一个渲染好的字段:节点 + **存储单位**的取值器。
92
+ * `read` 为 `null` 表示这一项只读(不认识的 type),不参与提交。
93
+ */
94
+ export interface ConfigField {
95
+ node: HTMLElement;
96
+ read: (() => ConfigValue) | null;
97
+ }
98
+
99
+ /** 只翻译框架自己认识的 owner;`core` 与 `world:<id>` 这类原样显示。 */
100
+ const OWNER_LABEL: Record<string, string> = {
101
+ 'persona': S.ownerPersona,
102
+ };
103
+
104
+ function errText(err: unknown): string {
105
+ return err instanceof Error ? err.message : String(err);
106
+ }
107
+
108
+ function isAbort(err: unknown): boolean {
109
+ return (err as { name?: unknown } | null)?.name === 'AbortError';
110
+ }
111
+
112
+ /**
113
+ * `x-options` 下拉。页加载先探一次(原生 `<select>` 同步弹出,第一次打开用这份);
114
+ * pointerdown/focus 再探,给下次打开用。探测失败只留当前值。
115
+ */
116
+ function optionsField(
117
+ ui: ConsoleUi,
118
+ kind: string,
119
+ current: string,
120
+ onChange: () => void,
121
+ signal?: AbortSignal,
122
+ ): ConfigField {
123
+ const sel = ui.select({
124
+ options: mergeOptionList([], current),
125
+ value: current,
126
+ onChange,
127
+ });
128
+ let seq = 0;
129
+ const refresh = (): void => {
130
+ const n = ++seq;
131
+ void get<{ options?: OptionItem[] }>(
132
+ `/api/config/options/${encodeURIComponent(kind)}`,
133
+ signal ? { signal } : undefined,
134
+ ).then((d) => {
135
+ if (n !== seq || signal?.aborted) return;
136
+ const live = Array.isArray(d.options) ? d.options : [];
137
+ fillSelect(ui, sel, mergeOptionList(live, sel.value), sel.value);
138
+ }).catch((err: unknown) => {
139
+ if (isAbort(err) || signal?.aborted) return;
140
+ });
141
+ };
142
+ const listenOpts = signal ? { signal } : undefined;
143
+ sel.addEventListener('pointerdown', refresh, listenOpts);
144
+ sel.addEventListener('focus', refresh, listenOpts);
145
+ refresh();
146
+ return { node: sel, read: () => sel.value };
147
+ }
148
+
149
+ function httpDownloadHref(raw: string): string | null {
150
+ try {
151
+ const url = new URL(raw);
152
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : null;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ function pathField(
159
+ ui: ConsoleUi,
160
+ prop: ConfigProperty,
161
+ current: string,
162
+ onChange: () => void,
163
+ signal?: AbortSignal,
164
+ ): ConfigField {
165
+ const spec = prop['x-path']!;
166
+ const input = ui.input({ type: 'text', value: current, onChange });
167
+ const controls = ui.h('div', 'pathfield-controls');
168
+ const choose = ui.button('', {
169
+ size: 'sm',
170
+ onClick: () => {
171
+ choose.disabled = true;
172
+ void pickPath({
173
+ kind: spec.kind,
174
+ title: prop.title,
175
+ currentPath: input.value.trim() || undefined,
176
+ recommendedDir: spec.recommendedDir,
177
+ extensions: spec.extensions,
178
+ }, signal ? { signal } : undefined).then((selected) => {
179
+ if (!selected || signal?.aborted) return;
180
+ input.value = selected;
181
+ onChange();
182
+ }).catch((err: unknown) => {
183
+ if (isAbort(err) || signal?.aborted) return;
184
+ ui.toast(errText(err), 'bad');
185
+ }).finally(() => {
186
+ choose.disabled = false;
187
+ });
188
+ },
189
+ });
190
+ choose.className += ' pathpick';
191
+ choose.title = spec.kind === 'file' ? S.chooseFile : S.chooseDirectory;
192
+ choose.setAttribute('aria-label', choose.title);
193
+ choose.appendChild(icon(choose.ownerDocument, 'folder-open'));
194
+ controls.append(input, choose);
195
+
196
+ const field = ui.h('div', 'pathfield');
197
+ field.appendChild(controls);
198
+ const meta = ui.h('div', 'pathfield-meta');
199
+ if (spec.recommendedDir) {
200
+ const recommended = ui.h('span', 'pathrecommended', S.recommendedDir(spec.recommendedDir));
201
+ recommended.title = spec.recommendedDir;
202
+ meta.appendChild(recommended);
203
+ }
204
+ const download = prop['x-download'];
205
+ const href = download ? httpDownloadHref(download.href) : null;
206
+ if (download && href) {
207
+ const link = ui.h('a', 'pathdownload');
208
+ link.setAttribute('href', href);
209
+ link.setAttribute('target', '_blank');
210
+ link.setAttribute('rel', 'noopener noreferrer');
211
+ link.append(icon(link.ownerDocument, 'download'), ui.h('span', null, download.label || S.download));
212
+ meta.appendChild(link);
213
+ }
214
+ if (meta.children.length) field.appendChild(meta);
215
+ return { node: field, read: () => input.value };
216
+ }
217
+
218
+ /** 渲染属性并返回存储单位的取值器;值变化经 onChange 提交。 */
219
+ export function configField(
220
+ ui: ConsoleUi,
221
+ prop: ConfigProperty,
222
+ val: unknown,
223
+ onChange?: () => void,
224
+ signal?: AbortSignal,
225
+ ): ConfigField {
226
+ const changed = (): void => onChange?.();
227
+ if (prop.type === 'boolean') {
228
+ const box = ui.checkbox(S.on, { checked: !!val, onChange: changed });
229
+ return { node: box.el, read: () => box.checked };
230
+ }
231
+
232
+ if (prop.type === 'string') {
233
+ if (prop['x-options']) {
234
+ return optionsField(ui, prop['x-options'], val == null ? '' : String(val), changed, signal);
235
+ }
236
+ if (Array.isArray(prop.enum)) {
237
+ const sel = ui.select({
238
+ options: prop.enum,
239
+ value: val == null ? prop.enum[0] : String(val),
240
+ onChange: changed,
241
+ });
242
+ return { node: sel, read: () => sel.value };
243
+ }
244
+ if (prop['x-path']) {
245
+ return pathField(ui, prop, val == null ? '' : String(val), changed, signal);
246
+ }
247
+ const inp = ui.input({ type: 'text', value: val == null ? '' : String(val), onChange: changed });
248
+ return { node: inp, read: () => inp.value };
249
+ }
250
+
251
+ if (prop.type === 'integer' || prop.type === 'number') {
252
+ const scale = prop['x-scale'] || 1;
253
+ const inp = ui.input({ type: 'number', onChange: changed });
254
+ if (prop.minimum != null) inp.min = String(prop.minimum / scale);
255
+ if (prop.maximum != null) inp.max = String(prop.maximum / scale);
256
+ if (prop.multipleOf != null) inp.step = String(prop.multipleOf / scale);
257
+ if (prop.nullable) {
258
+ // nullable 字段以空输入表示 null,0 保持为数值。
259
+ inp.value = val == null ? '' : String(Number(val) / scale);
260
+ inp.placeholder = S.leaveBlank;
261
+ return {
262
+ node: inp,
263
+ read: () => (inp.value.trim() === '' ? null : Number(inp.value) * scale),
264
+ };
265
+ }
266
+ inp.value = String((Number(val) || 0) / scale);
267
+ return { node: inp, read: () => Number(inp.value) * scale };
268
+ }
269
+
270
+ // 仅两个 number/integer 的数组可编辑,其余数组只读。
271
+ const itemType = prop.items?.type;
272
+ if (prop.type === 'array' && (itemType === 'integer' || itemType === 'number')) {
273
+ const scale = prop['x-scale'] || 1;
274
+ const spec = prop.items || {};
275
+ const mk = (v: unknown): HTMLInputElement => {
276
+ const inp = ui.input({ type: 'number', value: String((Number(v) || 0) / scale), onChange: changed });
277
+ if (spec.minimum != null) inp.min = String(spec.minimum / scale);
278
+ if (spec.maximum != null) inp.max = String(spec.maximum / scale);
279
+ return inp;
280
+ };
281
+ const pair = Array.isArray(val) ? (val as unknown[]) : [0, 0];
282
+ const a = mk(pair[0]);
283
+ const b = mk(pair[1]);
284
+ const wrap = ui.h('div', 'pairfield');
285
+ wrap.append(a, ui.h('span', 'pairsep', '–'), b);
286
+ return {
287
+ node: wrap,
288
+ read: () => [Number(a.value) * scale, Number(b.value) * scale],
289
+ };
290
+ }
291
+
292
+ // 不认识的 type:只读展示,不参与提交
293
+ return { node: ui.h('span', 'tdesc', JSON.stringify(val)), read: null };
294
+ }
295
+
296
+ export interface ConfigViewDeps {
297
+ ui: ConsoleUi;
298
+ lifecycle: Lifecycle;
299
+ signal: AbortSignal;
300
+ /**
301
+ * 这一处要渲染哪些组。省略 = 全部。
302
+ *
303
+ * 判据是**归属**:provider 页只画自己认领的那几组,设置页只画没人认领的
304
+ * (框架自己的)。取舍放在调用方,视图本身不认识任何一个 owner。
305
+ */
306
+ filter?(group: ConfigGroup): boolean;
307
+ /** 一条也不剩时说什么。默认那句是给设置页的措辞。 */
308
+ emptyText?: string;
309
+ /** 是否显示组的 owner 标签。 */
310
+ showOwner?: boolean;
311
+ }
312
+
313
+ export interface ConfigView {
314
+ /** 视图根节点。调用方自己决定插到哪。 */
315
+ el: HTMLElement;
316
+ /** 取一次 `/api/config` 并重建。可反复调。 */
317
+ load(): Promise<void>;
318
+ }
319
+
320
+ /** 修改自动保存到 config.json;热配置同时更新运行态。连续输入合并请求。 */
321
+ export function createConfigView(deps: ConfigViewDeps): ConfigView {
322
+ const { ui, lifecycle, signal } = deps;
323
+ const showOwner = deps.showOwner !== false;
324
+
325
+ const el = ui.h('div', 'configview');
326
+ const body = ui.h('div');
327
+ const bar = ui.actions();
328
+ const msg = ui.msgline();
329
+ bar.append(msg, ui.h('span', 'grow'));
330
+ el.append(body, bar);
331
+
332
+ /** 每次 load 重建本次页面的组、路径与取值器列表。 */
333
+ let readers: Array<{ groupId: string; path: string; read: () => ConfigValue }> = [];
334
+
335
+ function setMsg(text: string, bad?: boolean): void {
336
+ msg.textContent = text;
337
+ msg.className = 'msgline' + (bad ? ' bad' : '');
338
+ }
339
+
340
+ /** 提交一组:`/api/config` 一次收一组(校验按那一组的 schema 走)。 */
341
+ async function persist(groupId: string): Promise<void> {
342
+ if(signal.aborted)return;
343
+ try {
344
+ const values: Record<string, ConfigValue> = {};
345
+ for (const r of readers) if (r.groupId === groupId) values[r.path] = r.read();
346
+ setMsg(S.saving);
347
+ const out = await post<{ result?: string }>(
348
+ '/api/config',
349
+ { group: groupId, values },
350
+ { signal },
351
+ );
352
+ if (signal.aborted) return;
353
+ setMsg('✓ ' + (out.result || groupId));
354
+ } catch (err) {
355
+ if (isAbort(err) || signal.aborted) return;
356
+ setMsg(S.saveFailed(errText(err)), true);
357
+ }
358
+ }
359
+
360
+ const writes=new Map<string,Promise<void>>();
361
+ function save(groupId:string):Promise<void>{
362
+ const next=(writes.get(groupId) ?? Promise.resolve()).then(()=>persist(groupId));
363
+ writes.set(groupId,next);
364
+ void next.finally(()=>{if(writes.get(groupId)===next)writes.delete(groupId);});
365
+ return next;
366
+ }
367
+
368
+ const pending = new Map<string, Disposable>();
369
+ function queueSave(groupId: string): void {
370
+ pending.get(groupId)?.dispose();
371
+ pending.set(groupId, lifecycle.timeout(() => {
372
+ pending.delete(groupId);
373
+ void save(groupId);
374
+ }, SAVE_DEBOUNCE_MS));
375
+ }
376
+
377
+ async function load(): Promise<void> {
378
+ try {
379
+ const d = await get<{ groups?: ConfigGroupEntry[] }>('/api/config', { signal });
380
+ if (signal.aborted) return;
381
+ const all = Array.isArray(d.groups) ? d.groups : [];
382
+ const groups = deps.filter ? all.filter((entry) => deps.filter!(entry.group)) : all;
383
+ readers = [];
384
+ body.replaceChildren();
385
+ if (!groups.length) {
386
+ body.appendChild(ui.placeholder(
387
+ all.length
388
+ ? (deps.emptyText ?? S.emptyDefault)
389
+ : S.noSchema,
390
+ ));
391
+ return;
392
+ }
393
+ for (const { group, values } of groups) {
394
+ const sec = ui.h('div', 'tsection', group.schema.title || group.id);
395
+ if (showOwner) sec.appendChild(ui.h('span', 'ttag', OWNER_LABEL[group.owner] || group.owner));
396
+ body.appendChild(sec);
397
+ if (group.schema.description) {
398
+ body.appendChild(ui.h('div', 'tsecdesc', group.schema.description));
399
+ }
400
+ for (const [path, prop] of Object.entries(group.schema.properties || {})) {
401
+ const row = ui.h('div', 'trow');
402
+ const label = ui.h('span', 'tlabel', prop.title || path);
403
+ if (prop['x-hot'] === false) {
404
+ // World 的配置在构造时读走:重启那个 World 即生效,不必重启进程。
405
+ label.appendChild(ui.h('span', 'ttag', group.owner.startsWith('world:') ? S.restartWorld : S.restartProcess));
406
+ }
407
+ row.appendChild(label);
408
+ const { node, read } = configField(ui, prop, (values || {})[path], () => queueSave(group.id), signal);
409
+ const fieldWrap = ui.h('div', 'tfield');
410
+ fieldWrap.appendChild(node);
411
+ if (prop['x-suffix'] && prop.type !== 'boolean') {
412
+ fieldWrap.appendChild(ui.h('span', 'tunit', prop['x-suffix']));
413
+ }
414
+ row.appendChild(fieldWrap);
415
+ row.appendChild(ui.h('span', 'tdesc', prop.description || ''));
416
+ body.appendChild(row);
417
+ if (read) readers.push({ groupId: group.id, path, read });
418
+ }
419
+ }
420
+ } catch (err) {
421
+ if (isAbort(err) || signal.aborted) return;
422
+ body.replaceChildren(ui.placeholder(S.loadFailed(errText(err))));
423
+ }
424
+ }
425
+
426
+ body.appendChild(ui.placeholder(S.loading));
427
+ return { el, load };
428
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * 事件库翻查界面,按 from/to 游标区间分页:追新使用 latest+1,翻旧使用 earliest−1。边界与来源集合属于本次挂载。
3
+ * 来源选项由数据生成,新来源出现时保留当前选择;调试通道可用时由 event 帧触发追新,不可用时回落轮询。
4
+ */
5
+
6
+ import type { ConsoleUi, ConsoleTable, Disposable } from '../../../shared/client-panel.ts';
7
+ import { get } from '../../core/api.ts';
8
+ import type { Lifecycle } from '../../core/lifecycle.ts';
9
+ import { shouldStick } from '../../ui/index.ts';
10
+ import { S } from './strings.ts';
11
+
12
+ /** 事件库的一条。字段是服务端 `EventEnvelope` 的子集。 */
13
+ export interface EventRow {
14
+ cursor: number;
15
+ ts: string;
16
+ source: string;
17
+ type: string;
18
+ text: string;
19
+ }
20
+
21
+ interface EventPage {
22
+ latest: number;
23
+ events: EventRow[];
24
+ }
25
+
26
+ const PAGE = 100;
27
+ /** 没有推送时的轮询间隔。 */
28
+ const EVENT_POLL_MS = 3000;
29
+ /** 距底多少像素之内仍算贴着底。 */
30
+ const STICK_PX = 60;
31
+
32
+ export interface EventsViewDeps {
33
+ ui: ConsoleUi;
34
+ lifecycle: Lifecycle;
35
+ signal: AbortSignal;
36
+ /** 网络状态回报(与调试通道共用同一处显示) */
37
+ onNet(online: boolean): void;
38
+ onError(err: unknown): void;
39
+ }
40
+
41
+ export interface EventsView {
42
+ el: HTMLElement;
43
+ /** 首次进入/换来源时的全量拉取 */
44
+ init(): Promise<void>;
45
+ /** 追新(推送到了、或轮询到点) */
46
+ poll(): Promise<void>;
47
+ /** 这一页开始/停止自己轮询(没有推送时才需要) */
48
+ setPolling(on: boolean): void;
49
+ }
50
+
51
+ export function createEventsView(deps: EventsViewDeps): EventsView {
52
+ const { ui } = deps;
53
+ const sources = new Set<string>();
54
+ let source = '';
55
+ let archive = false;
56
+ let earliest: number | null = null;
57
+ let latest = 0;
58
+ let inited = false;
59
+ let poller: Disposable | null = null;
60
+
61
+ const bar = ui.rowbar();
62
+ const desc = ui.h('span', 'pagedesc grow', S.eventsDesc);
63
+ const sel = ui.select({
64
+ options: [{ value: '', label: S.allSources }],
65
+ onChange: (v) => {
66
+ source = v;
67
+ inited = false;
68
+ earliest = null;
69
+ latest = 0;
70
+ void view.init();
71
+ },
72
+ });
73
+ // 默认过滤 archiveOnly 记录;启用归档选项后同时显示未直接投递的记录。
74
+ const archiveToggle = ui.checkbox(S.showArchive, {
75
+ title: S.showArchiveTitle,
76
+ onChange: (on) => {
77
+ archive = on;
78
+ inited = false;
79
+ earliest = null;
80
+ latest = 0;
81
+ void view.init();
82
+ },
83
+ });
84
+ const more = ui.button(S.loadEarlier, { size: 'sm', onClick: () => void loadEarlier() });
85
+ bar.append(desc, sel, archiveToggle.el, more);
86
+
87
+ const table: ConsoleTable = ui.table({
88
+ head: ['#', S.evHeadTime, S.evHeadSource, S.evHeadType, S.evHeadText],
89
+ maxHeight: 'calc(100vh - 300px)',
90
+ });
91
+
92
+ const el = ui.h('div');
93
+ el.append(bar, table.el);
94
+
95
+ const rowCells = (e: EventRow): Parameters<ConsoleTable['addRow']>[0] => [
96
+ { text: `#${e.cursor}`, cls: 'mono' },
97
+ { text: ui.fmt.clock(e.ts), cls: 'mono' },
98
+ { text: e.source, cls: 'mono' },
99
+ { text: e.type, cls: 'mono' },
100
+ { text: e.text, cls: 'txt' },
101
+ ];
102
+
103
+ /** 新来源就补一项;当前选择不动(它可能正指着一个刚被重画掉的 option)。 */
104
+ const noteSources = (events: readonly EventRow[]): void => {
105
+ for (const e of events) {
106
+ if (!e.source || sources.has(e.source)) continue;
107
+ sources.add(e.source);
108
+ const op = ui.h('option', null, e.source);
109
+ op.value = e.source;
110
+ sel.appendChild(op);
111
+ }
112
+ };
113
+
114
+ const query = (extra: string): string => {
115
+ const src = source ? `&source=${encodeURIComponent(source)}` : '';
116
+ return `/api/events?${extra}${src}${archive ? '&archive=1' : ''}`;
117
+ };
118
+
119
+ const stuck = (): boolean =>
120
+ shouldStick(table.el.scrollTop, table.el.scrollHeight, table.el.clientHeight, STICK_PX);
121
+ const toEnd = (): void => {
122
+ table.el.scrollTop = table.el.scrollHeight;
123
+ };
124
+
125
+ const failed = (err: unknown): boolean => {
126
+ if ((err as { name?: string } | null)?.name === 'AbortError') return true;
127
+ deps.onNet(false);
128
+ deps.onError(err);
129
+ return false;
130
+ };
131
+
132
+ async function loadEarlier(): Promise<void> {
133
+ if (earliest === null || earliest <= 1) return;
134
+ try {
135
+ const d = await get<EventPage>(query(`to=${earliest - 1}&limit=${PAGE}`), {
136
+ signal: deps.signal,
137
+ });
138
+ const events = d?.events ?? [];
139
+ if (!events.length) return;
140
+ const before = table.el.scrollHeight;
141
+ const first = table.body.children[0] ?? null;
142
+ for (const e of events) {
143
+ const tr = table.addRow(rowCells(e));
144
+ table.body.insertBefore(tr, first);
145
+ }
146
+ // 往上插会把已有内容顶下去,补回同样多的滚动量,视野里的那一行才不动。
147
+ table.el.scrollTop += table.el.scrollHeight - before;
148
+ earliest = events[0].cursor;
149
+ noteSources(events);
150
+ } catch (err) {
151
+ failed(err);
152
+ }
153
+ }
154
+
155
+ const view: EventsView = {
156
+ el,
157
+ async init() {
158
+ try {
159
+ const d = await get<EventPage>(query(`limit=${PAGE}`), { signal: deps.signal });
160
+ const events = d?.events ?? [];
161
+ inited = true;
162
+ table.clear(events.length ? undefined : S.noEvents);
163
+ for (const e of events) table.addRow(rowCells(e));
164
+ earliest = events.length ? events[0].cursor : null;
165
+ latest = Math.max(d?.latest ?? 0, events.length ? events[events.length - 1].cursor : 0);
166
+ noteSources(events);
167
+ deps.onNet(true);
168
+ toEnd();
169
+ } catch (err) {
170
+ failed(err);
171
+ }
172
+ },
173
+ async poll() {
174
+ if (!inited) return;
175
+ try {
176
+ const d = await get<EventPage>(query(`from=${latest + 1}`), { signal: deps.signal });
177
+ const events = d?.events ?? [];
178
+ if (events.length) {
179
+ const wasStuck = stuck();
180
+ for (const e of events) table.addRow(rowCells(e));
181
+ if (earliest === null) earliest = events[0].cursor;
182
+ noteSources(events);
183
+ if (wasStuck) toEnd();
184
+ }
185
+ latest = Math.max(d?.latest ?? 0, latest);
186
+ } catch (err) {
187
+ failed(err);
188
+ }
189
+ },
190
+ setPolling(on) {
191
+ poller?.dispose();
192
+ poller = null;
193
+ if (on) poller = deps.lifecycle.interval(() => void view.poll(), EVENT_POLL_MS);
194
+ },
195
+ };
196
+ return view;
197
+ }