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,154 @@
1
+ /*
2
+ * Minecraft 面板的专有样式。
3
+ *
4
+ * 只放 `ctx.ui` 没有对应原语的那几块:挂载行里状态词的三档配色、一行排开的
5
+ * 表单块、保留换行的正文段、试跑用的图片预览。其余一律用原语
6
+ * (sheet / foldSheet / rowbar / btn / pill / chip / kv / table / log / field /
7
+ * checkbox / select / textarea / placeholder / drawer),那些 class 归控制台管,
8
+ * 这里一个字都不覆盖。
9
+ *
10
+ * 两条自律(与 QQ 那份同源):
11
+ *
12
+ * 1. **类名一律 `mc-` 前缀。** 这份样式表跟着 provider 的 bundle 注入到同一个
13
+ * 文档里,不加前缀就是往全局命名空间里丢东西。
14
+ * 2. **颜色/字体只用控制台的主题变量。** 写死色值的话,主题一换这块就成了补丁。
15
+ */
16
+
17
+ /* ---- 段落小标题:与左栏的 .stacklabel 同一副字面,但不带那份左外边距 ---- */
18
+
19
+ .mountrow .mstate.mc-on { color: var(--ok); }
20
+ .mountrow .mstate.mc-off { color: var(--danger); }
21
+ .mountrow .mstate.mc-warn,
22
+ .mountrow .mstate.mc-plain { color: var(--ink-soft); }
23
+
24
+ /* ---- 一行排开的表单块(`ui.field` 缺省是整行块级,这里让它们并排) ---- */
25
+ .mc-form {
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ align-items: flex-end;
29
+ gap: 8px 14px;
30
+ margin: 6px 0 2px;
31
+ }
32
+
33
+ .mc-form .fieldrow { margin: 0; }
34
+ .mc-form .fieldrow > input.field { width: 7em; }
35
+ .mc-form .fieldrow > select.field { min-width: 12em; }
36
+
37
+ /* ---- 保留换行的正文段(最近看到的那一眼、试跑输出)。
38
+ `kv` 的值格是等宽且只 break-word,放不下带换行的大段正文。 ---- */
39
+ .mc-para {
40
+ white-space: pre-wrap;
41
+ margin: 4px 0;
42
+ }
43
+
44
+ .mc-dim { opacity: .75; }
45
+
46
+ /* ---- 试跑用的图片预览:点一下进 `drawer` 看大的 ---- */
47
+ .mc-shot {
48
+ max-width: 100%;
49
+ border-radius: 9px;
50
+ border: 1px solid var(--line-2);
51
+ margin-top: 6px;
52
+ cursor: zoom-in;
53
+ }
54
+
55
+ /* 抽屉里那张大的:占满抽屉宽度,不再可点 */
56
+ .mc-shot-big {
57
+ max-width: 100%;
58
+ border-radius: 9px;
59
+ border: 1px solid var(--line-2);
60
+ }
61
+
62
+ /* 藏起来的 file input:点的是旁边那颗按钮,它本身不该占位 */
63
+ .mc-hidden { display: none; }
64
+
65
+ /* ---- 存档分组:一组一份 <details>,最近活跃的那组默认展开 ---- */
66
+ .mc-group { border-top: 1px solid var(--line); }
67
+ .mc-group > summary {
68
+ cursor: pointer;
69
+ padding: 7px 2px;
70
+ display: flex;
71
+ align-items: baseline;
72
+ gap: 8px;
73
+ color: var(--ink-soft);
74
+ font-size: 13px;
75
+ }
76
+ .mc-group > summary::marker { color: var(--ink-dim); }
77
+ .mc-groupcount { color: var(--ink-dim); font-size: 12px; }
78
+
79
+ /* ---- 存档卡片。网格是"一眼扫完有哪些",详情是"每份摊开自述"----
80
+ 两种版式只差列宽与卡里放什么,所以是同一份卡片加一个容器 class。 */
81
+ .mc-worlds { display: grid; gap: 10px; margin: 2px 0 10px; }
82
+ .mc-worlds.grid { grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); }
83
+ .mc-worlds.detail { grid-template-columns: 1fr; }
84
+
85
+ .mc-world {
86
+ border: 1px solid var(--line-2);
87
+ border-radius: 10px;
88
+ padding: 8px 10px;
89
+ background: var(--sheet-2);
90
+ }
91
+ .mc-world.cur { border-color: var(--accent); }
92
+ .mc-worldhead { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
93
+ .mc-worldname { font-weight: 600; overflow-wrap: anywhere; }
94
+ .mc-worldmeta {
95
+ display: flex;
96
+ flex-wrap: wrap;
97
+ gap: 2px 10px;
98
+ margin-top: 4px;
99
+ color: var(--ink-soft);
100
+ font-size: 12px;
101
+ }
102
+ .mc-worldacts { margin-top: 8px; }
103
+ .mc-world .kvtable { margin: 6px 0 0; }
104
+
105
+ /* ---- 卡片内的折叠块:次要参数平时收着(整卡折叠是 ui.foldSheet 的事) ---- */
106
+ .mc-fold { margin: 6px 0 2px; }
107
+ .mc-fold > summary {
108
+ cursor: pointer;
109
+ padding: 6px 2px;
110
+ color: var(--ink-soft);
111
+ font-size: 13px;
112
+ }
113
+ .mc-foldbody {
114
+ padding: 2px 0 4px 10px;
115
+ border-left: 2px solid var(--line);
116
+ }
117
+ .mc-foldbody textarea.field { min-height: 4.5em; }
118
+
119
+ /* ---- 皮肤:一个角色一卡,卡头是那张脸 ---- */
120
+ .mc-skins {
121
+ display: grid;
122
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
123
+ gap: 10px;
124
+ margin: 2px 0 10px;
125
+ }
126
+
127
+ .mc-skin {
128
+ border: 1px solid var(--line-2);
129
+ border-radius: 10px;
130
+ padding: 8px 10px;
131
+ background: var(--sheet-2);
132
+ }
133
+ .mc-skin.cur { border-color: var(--accent); }
134
+ .mc-skinhead { display: flex; align-items: center; gap: 10px; }
135
+ .mc-skinhead .mc-worldmeta { margin-top: 2px; align-items: center; }
136
+
137
+ /* 皮肤材质放大后必须保持硬边:插值一上来那张脸就糊成一团 */
138
+ .mc-face {
139
+ image-rendering: pixelated;
140
+ border-radius: 6px;
141
+ border: 1px solid var(--line-2);
142
+ cursor: zoom-in;
143
+ flex: none;
144
+ }
145
+
146
+ /* 还没选皮肤时占同样大小的一格,免得卡片在选中前后跳一下 */
147
+ .mc-facenone {
148
+ width: 72px;
149
+ height: 72px;
150
+ cursor: default;
151
+ background: repeating-linear-gradient(
152
+ 45deg, var(--sheet-2), var(--sheet-2) 6px, var(--line) 6px, var(--line) 12px
153
+ );
154
+ }
@@ -0,0 +1,449 @@
1
+ /**
2
+ * 面板 `world` —— 存档与玩法。
3
+ *
4
+ * 改动写在 server.properties,而服务器只在**启动时**读一次:停机时改什么都行
5
+ * (下次启动生效),跑着的时候只有**难度**和**默认游戏模式**能当场改(走控制台
6
+ * 指令,只对本轮有效,不落盘)。所以这一屏的禁用规则不是装饰,它就是那条时机规则
7
+ * 的界面形态。
8
+ *
9
+ * 世界生成那几项还多一条时机:它们只在存档**第一次生成**时被读到,对着已有的
10
+ * 世界改毫无作用。所以它们不在"应用"那条路上,而是「开新存档」这个动作的参数。
11
+ *
12
+ * 存档列表按**最近活跃**分组,默认只展开最近的那一组——服务器目录里躺着几十份
13
+ * 存档时,一条长表格里真正要找的永远是最上面那几份。两种版式:网格(一眼扫过
14
+ * 所有存档)与详情(每份摊开 level.dat 自述)。
15
+ */
16
+
17
+ import type {
18
+ ConsolePanelContext,
19
+ ConsolePanel,
20
+ } from '../../../web/shared/client-panel.ts';
21
+ import {
22
+ errText,
23
+ msgLine,
24
+ type MinecraftWorldInfo,
25
+ type MinecraftWorldState,
26
+ } from './client.ts';
27
+
28
+ const DESC =
29
+ '换存档、开新世界、调难度与游戏模式。这些写在 server.properties 里,'
30
+ + '而服务器只在启动时读一次:停机时改什么都行(下次启动生效),'
31
+ + '跑着的时候只有难度和默认游戏模式能当场改(走控制台指令,只对本轮有效)。';
32
+
33
+ const GAMEMODES = [
34
+ { value: 'survival', label: '生存' },
35
+ { value: 'creative', label: '创造' },
36
+ { value: 'adventure', label: '冒险' },
37
+ { value: 'spectator', label: '旁观' },
38
+ ] as const;
39
+
40
+ const DIFFICULTIES = [
41
+ { value: 'peaceful', label: '和平' },
42
+ { value: 'easy', label: '简单' },
43
+ { value: 'normal', label: '普通' },
44
+ { value: 'hard', label: '困难' },
45
+ ] as const;
46
+
47
+ /** level.dat 里的 GameType / Difficulty 是数字 */
48
+ const GAMEMODE_ZH = ['生存', '创造', '冒险', '旁观'];
49
+ const DIFFICULTY_ZH = ['和平', '简单', '普通', '困难'];
50
+ const GENERATOR_ZH: Record<string, string> = {
51
+ flat: '超平坦',
52
+ amplified: '放大化',
53
+ large_biomes: '大型生物群系',
54
+ normal: '默认',
55
+ overworld: '默认',
56
+ };
57
+
58
+ const DAY_MS = 86_400_000;
59
+
60
+ /** 分组:按最近活跃分档,顺序即从近到远。 */
61
+ const GROUPS: ReadonlyArray<{ id: string; label: string; within: number }> = [
62
+ { id: 'today', label: '今天', within: 1 },
63
+ { id: 'week', label: '最近 7 天', within: 7 },
64
+ { id: 'month', label: '最近 30 天', within: 30 },
65
+ { id: 'older', label: '更早', within: Infinity },
66
+ ];
67
+
68
+ /** 本地日历日的差值:23:59 与次日 00:01 是两天,差 2 分钟也算 */
69
+ function daysAgo(iso: string, now: Date): number {
70
+ const then = new Date(iso);
71
+ const midnight = (d: Date): number =>
72
+ new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
73
+ return Math.floor((midnight(now) - midnight(then)) / DAY_MS);
74
+ }
75
+
76
+ function groupOf(world: MinecraftWorldInfo, now: Date): string {
77
+ if (!world.modified) return 'pending';
78
+ const days = daysAgo(world.modified, now);
79
+ return GROUPS.find((g) => days < g.within)!.id;
80
+ }
81
+
82
+ function fmtBytes(n: number): string {
83
+ if (n <= 0) return '—';
84
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
85
+ let v = n;
86
+ let i = 0;
87
+ while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
88
+ return `${v < 10 && i > 0 ? v.toFixed(1) : Math.round(v)} ${units[i]}`;
89
+ }
90
+
91
+ function fmtWhen(iso: string | null, now: Date): string {
92
+ if (!iso) return '还没生成';
93
+ const days = daysAgo(iso, now);
94
+ const clock = iso.slice(11, 16);
95
+ if (days === 0) return `今天 ${clock}`;
96
+ if (days === 1) return `昨天 ${clock}`;
97
+ if (days < 30) return `${days} 天前`;
98
+ return iso.slice(0, 10);
99
+ }
100
+
101
+ /** 存档自述里能读出多少写多少;一项都读不出来就不摆这一行 */
102
+ function infoRows(w: MinecraftWorldInfo, now: Date): Array<{ k: string; v: string }> {
103
+ const rows: Array<{ k: string; v: string }> = [
104
+ { k: '最后活跃', v: `${fmtWhen(w.modified, now)}${w.modified ? `(${w.modified.slice(0, 16).replace('T', ' ')})` : ''}` },
105
+ { k: '占盘', v: fmtBytes(w.sizeBytes) },
106
+ ];
107
+ const info = w.info;
108
+ if (info) {
109
+ if (info.generator) rows.push({ k: '世界类型', v: GENERATOR_ZH[info.generator] ?? info.generator });
110
+ if (info.seed) rows.push({ k: '种子', v: info.seed });
111
+ if (info.gameType !== null) rows.push({ k: '游戏模式', v: GAMEMODE_ZH[info.gameType] ?? String(info.gameType) });
112
+ if (info.difficulty !== null) {
113
+ rows.push({
114
+ k: '难度',
115
+ v: `${DIFFICULTY_ZH[info.difficulty] ?? info.difficulty}${info.hardcore ? ' · 极限' : ''}`,
116
+ });
117
+ }
118
+ if (info.version) rows.push({ k: '版本', v: info.version });
119
+ if (info.dayTime !== null) rows.push({ k: '世界天数', v: `第 ${Math.floor(info.dayTime / 24_000) + 1} 天` });
120
+ if (info.levelName && info.levelName !== w.name) rows.push({ k: '存档自述名', v: info.levelName });
121
+ }
122
+ if (w.dimensions.length) {
123
+ rows.push({ k: '已生成维度', v: w.dimensions.map((d) => (d === 'nether' ? '下界' : '末地')).join('、') });
124
+ }
125
+ return rows;
126
+ }
127
+
128
+ export const worldPanel: ConsolePanel = {
129
+ mount(ctx: ConsolePanelContext) {
130
+ const { ui, root } = ctx;
131
+
132
+ // -----------------------------------------------------------------------
133
+ // 卡一:存档
134
+ // -----------------------------------------------------------------------
135
+ const card = ui.sheet({ title: '存档与玩法', en: 'world & rules', desc: DESC });
136
+ const s = card.body;
137
+ root.appendChild(card.el);
138
+
139
+ const msg = msgLine(ctx);
140
+ s.appendChild(msg.el);
141
+
142
+ const stateBox = ui.h('div');
143
+ s.append(ui.section('当前世界'), stateBox);
144
+
145
+ const layout = ui.segmented(
146
+ [{ value: 'grid', label: '网格' }, { value: 'detail', label: '详情' }],
147
+ {
148
+ value: ctx.memo.get<string>('layout', 'grid'),
149
+ size: 'sm',
150
+ onSelect: (v) => { ctx.memo.set('layout', v); render(cur); },
151
+ },
152
+ );
153
+ const listBar = ui.rowbar();
154
+ listBar.append(ui.h('span', 'grow'), layout.el);
155
+ const listBox = ui.h('div');
156
+ s.append(ui.section('存档', '按最近活跃分组,默认只展开最近的一组'), listBar, listBox);
157
+
158
+ // ---- 玩法 ----
159
+ const gmSel = ui.select({ options: GAMEMODES });
160
+ const diffSel = ui.select({ options: DIFFICULTIES });
161
+ const hardcoreIn = ui.checkbox('极限模式');
162
+ const pvpIn = ui.checkbox('PVP');
163
+ const monstersIn = ui.checkbox('刷怪');
164
+ const btnApply = ui.button('应用', { size: 'sm', variant: 'primary' });
165
+ const ruleForm = ui.h('div', 'mc-form');
166
+ ruleForm.append(ui.field('游戏模式', gmSel), ui.field('难度', diffSel));
167
+ const ruleBar = ui.actions();
168
+ ruleBar.append(hardcoreIn.el, pvpIn.el, monstersIn.el, ui.h('span', 'grow'), btnApply);
169
+ s.append(ui.section('玩法'), ruleForm, ruleBar);
170
+
171
+ // ---- 世界规则(收起来的那些) ----
172
+ const netherIn = ui.checkbox('允许下界');
173
+ const spawnProtIn = ui.input({ type: 'number', value: '16' });
174
+ const viewIn = ui.input({ type: 'number', value: '10' });
175
+ const simIn = ui.input({ type: 'number', value: '10' });
176
+ const borderIn = ui.input({ type: 'number', value: '29999984' });
177
+ const rulesForm = ui.h('div', 'mc-form');
178
+ rulesForm.append(
179
+ ui.field('出生点保护(格)', spawnProtIn),
180
+ ui.field('视距(区块)', viewIn),
181
+ ui.field('模拟距离(区块)', simIn),
182
+ ui.field('世界边界(格)', borderIn),
183
+ );
184
+ const rulesBar = ui.actions();
185
+ rulesBar.append(netherIn.el, ui.h('span', 'grow'));
186
+ const moreRules = fold(ctx, '更多世界规则', '这几项都要停机改,下次启动生效。');
187
+ moreRules.body.append(rulesForm, rulesBar);
188
+ s.append(moreRules.el);
189
+
190
+ // -----------------------------------------------------------------------
191
+ // 卡二:开新存档
192
+ // -----------------------------------------------------------------------
193
+ const newCard = ui.sheet({
194
+ title: '开新存档',
195
+ en: 'new world',
196
+ desc: '这里的生成参数只在世界第一次生成时被读到;开出来之后再改不会重塑地形。'
197
+ + '服务器停机时登记,下次启动按它生成。',
198
+ });
199
+ const n = newCard.body;
200
+ root.appendChild(newCard.el);
201
+
202
+ const newName = ui.input({ placeholder: '新存档名' });
203
+ const newSeed = ui.input({ placeholder: '留空 = 随机' });
204
+ const typeSel = ui.select();
205
+ const structIn = ui.checkbox('生成村庄、神殿这些结构', { checked: true });
206
+ const typeNote = ui.h('div', 'pagedesc');
207
+ const newForm = ui.h('div', 'mc-form');
208
+ newForm.append(ui.field('存档名', newName), ui.field('种子', newSeed), ui.field('世界类型', typeSel));
209
+
210
+ const presetSel = ui.select();
211
+ const genText = ui.textarea({ placeholder: '留空 = 该世界类型的默认生成', cls: 'mono' });
212
+ const presetNote = ui.h('div', 'pagedesc');
213
+ const moreGen = fold(ctx, '更多世界生成', '超平坦的层配方、单一群系的群系名都写在这份 JSON 里。');
214
+ const presetForm = ui.h('div', 'mc-form');
215
+ presetForm.append(ui.field('超平坦预设', presetSel));
216
+ moreGen.body.append(presetForm, presetNote, ui.field('生成器细则(generator-settings)', genText));
217
+
218
+ const btnCreate = ui.button('登记新存档', { size: 'sm', variant: 'primary' });
219
+ const newBar = ui.actions();
220
+ newBar.append(structIn.el, ui.h('span', 'grow'), btnCreate);
221
+ n.append(newForm, typeNote, moreGen.el, newBar);
222
+
223
+ // -----------------------------------------------------------------------
224
+
225
+ /** 服务端那份权威状态。应用玩法时要看它决定提交哪几项。 */
226
+ let cur: MinecraftWorldState | null = null;
227
+ /** 展开着的分组;首次渲染时钉在最近的那一组上,之后跟着用户开合走 */
228
+ let opened: Set<string> | null = null;
229
+
230
+ function fillPresets(st: MinecraftWorldState): void {
231
+ if (presetSel.options.length) return;
232
+ const custom = ui.h('option', null, '自定义');
233
+ custom.value = '';
234
+ presetSel.appendChild(custom);
235
+ for (const p of st.flatPresets) {
236
+ const op = ui.h('option', null, p.label);
237
+ op.value = p.id;
238
+ presetSel.appendChild(op);
239
+ }
240
+ presetSel.addEventListener('change', () => {
241
+ const hit = st.flatPresets.find((p) => p.id === presetSel.value);
242
+ presetNote.textContent = hit ? hit.note : '';
243
+ if (!hit) return;
244
+ // 选了平坦预设就是想开一个平坦世界,类型跟着走,免得配方写好了却是默认地形
245
+ genText.value = hit.json;
246
+ typeSel.value = 'minecraft:flat';
247
+ typeSel.dispatchEvent(new Event('change'));
248
+ }, { signal: ctx.signal });
249
+ }
250
+
251
+ function fillLevelTypes(st: MinecraftWorldState): void {
252
+ if (typeSel.options.length) return;
253
+ for (const t of st.levelTypes) {
254
+ const op = ui.h('option', null, t.label);
255
+ op.value = t.value;
256
+ typeSel.appendChild(op);
257
+ }
258
+ const note = (): void => {
259
+ typeNote.textContent = st.levelTypes.find((t) => t.value === typeSel.value)?.note ?? '';
260
+ };
261
+ typeSel.addEventListener('change', note, { signal: ctx.signal });
262
+ note();
263
+ }
264
+
265
+ /** 一份存档一张卡片。两种版式的差别只在卡片里摆什么。 */
266
+ function worldCard(w: MinecraftWorldInfo, st: MinecraftWorldState, now: Date, detail: boolean): HTMLElement {
267
+ const isCur = w.name === st.settings.levelName;
268
+ const el = ui.h('div', `mc-world${isCur ? ' cur' : ''}`);
269
+ const head = ui.h('div', 'mc-worldhead');
270
+ head.append(ui.h('span', 'mc-worldname', w.name));
271
+ if (isCur) head.append(ui.pill('当前', 'on'));
272
+ if (!w.generated) head.append(ui.pill('还没生成', 'plain'));
273
+ el.append(head);
274
+
275
+ if (detail) {
276
+ el.append(ui.kv(infoRows(w, now)));
277
+ } else {
278
+ const meta = ui.h('div', 'mc-worldmeta');
279
+ meta.append(ui.h('span', null, fmtWhen(w.modified, now)));
280
+ if (w.sizeBytes > 0) meta.append(ui.h('span', null, fmtBytes(w.sizeBytes)));
281
+ if (w.info?.version) meta.append(ui.h('span', null, w.info.version));
282
+ if (w.info?.generator) {
283
+ meta.append(ui.h('span', null, GENERATOR_ZH[w.info.generator] ?? w.info.generator));
284
+ }
285
+ el.append(meta);
286
+ }
287
+
288
+ const acts = ui.h('div', 'mc-worldacts');
289
+ const pick = ui.button('换到这个存档', { size: 'sm' });
290
+ pick.disabled = isCur || !st.configured || st.live;
291
+ pick.addEventListener('click', () => {
292
+ void act(() => ctx.invoke<MinecraftWorldState>('select', [w.name]), '切换中…');
293
+ }, { signal: ctx.signal });
294
+ acts.append(pick);
295
+ el.append(acts);
296
+ return el;
297
+ }
298
+
299
+ function renderWorlds(st: MinecraftWorldState): void {
300
+ const now = new Date();
301
+ const detail = layout.value === 'detail';
302
+ const buckets = new Map<string, MinecraftWorldInfo[]>();
303
+ for (const w of st.worlds) {
304
+ const id = groupOf(w, now);
305
+ (buckets.get(id) ?? buckets.set(id, []).get(id)!).push(w);
306
+ }
307
+ const order = [...GROUPS.map((g) => ({ id: g.id, label: g.label })), { id: 'pending', label: '还没生成' }]
308
+ .filter((g) => buckets.has(g.id));
309
+ if (opened === null) opened = new Set(order.length ? [order[0].id] : []);
310
+
311
+ listBox.replaceChildren();
312
+ if (!order.length) {
313
+ listBox.append(ui.placeholder('还没有存档(下次启动会按当前存档名生成一个)'));
314
+ return;
315
+ }
316
+ for (const g of order) {
317
+ const items = buckets.get(g.id)!;
318
+ const box = ui.h('details', 'mc-group');
319
+ box.open = opened.has(g.id);
320
+ const sum = ui.h('summary');
321
+ sum.append(ui.h('span', null, g.label), ui.h('span', 'mc-groupcount', `${items.length} 份`));
322
+ box.append(sum);
323
+ const grid = ui.h('div', `mc-worlds ${detail ? 'detail' : 'grid'}`);
324
+ for (const w of items) grid.append(worldCard(w, st, now, detail));
325
+ box.append(grid);
326
+ box.addEventListener('toggle', () => {
327
+ if (box.open) opened!.add(g.id);
328
+ else opened!.delete(g.id);
329
+ }, { signal: ctx.signal });
330
+ listBox.append(box);
331
+ }
332
+ }
333
+
334
+ function render(st: MinecraftWorldState | null): void {
335
+ if (ctx.signal.aborted) return;
336
+ cur = st;
337
+ if (!st) {
338
+ msg.say('存档设置不可用', true);
339
+ stateBox.replaceChildren(ui.placeholder('读不到服务器目录里的 server.properties'));
340
+ listBox.replaceChildren(ui.placeholder('—'));
341
+ return;
342
+ }
343
+ const stopped = !st.live;
344
+ fillLevelTypes(st);
345
+ fillPresets(st);
346
+
347
+ const genZh = st.levelTypes.find((t) => t.value === st.settings.levelType)?.label ?? st.settings.levelType;
348
+ stateBox.replaceChildren(ui.kv([
349
+ { k: '当前存档', v: st.settings.levelName },
350
+ { k: '种子', v: st.settings.levelSeed || '(随机)' },
351
+ { k: '世界类型', v: `${genZh}${st.settings.generatorSettings ? ' · 带生成器细则' : ''}` },
352
+ { k: '服务器', v: ui.pill(
353
+ st.live ? (st.hosted ? '运行中(本 World 托管)' : '运行中(外部)') : '已停机',
354
+ st.live ? 'on' : 'plain',
355
+ ) },
356
+ { k: '服务器目录', v: st.serverDir || '(未配置)' },
357
+ ]));
358
+
359
+ renderWorlds(st);
360
+
361
+ gmSel.value = st.settings.gamemode;
362
+ diffSel.value = st.settings.difficulty;
363
+ hardcoreIn.setChecked(st.settings.hardcore);
364
+ pvpIn.setChecked(st.settings.pvp);
365
+ monstersIn.setChecked(st.settings.spawnMonsters);
366
+ netherIn.setChecked(st.settings.allowNether);
367
+ spawnProtIn.value = String(st.settings.spawnProtection);
368
+ viewIn.value = String(st.settings.viewDistance);
369
+ simIn.value = String(st.settings.simulationDistance);
370
+ borderIn.value = String(st.settings.maxWorldSize);
371
+
372
+ const off = !st.configured;
373
+ newName.disabled = newSeed.disabled = typeSel.disabled = presetSel.disabled = off || !stopped;
374
+ genText.disabled = structIn.input.disabled = btnCreate.disabled = off || !stopped;
375
+ hardcoreIn.input.disabled = pvpIn.input.disabled = monstersIn.input.disabled = off || !stopped;
376
+ netherIn.input.disabled = spawnProtIn.disabled = viewIn.disabled = off || !stopped;
377
+ simIn.disabled = borderIn.disabled = off || !stopped;
378
+ gmSel.disabled = diffSel.disabled = btnApply.disabled = off;
379
+ msg.say(st.detail || (st.live ? '服务器跑着:只有难度和默认游戏模式能当场改' : ''));
380
+ }
381
+
382
+ const refresh = (): Promise<void> =>
383
+ ctx.invoke<MinecraftWorldState>('state').then(render, () => render(null));
384
+
385
+ /**
386
+ * 一次动作:置灰两颗钮 → 调服务端 → 拿回来的新状态重画。
387
+ *
388
+ * **先解锁再重画。** `ui.disable` 恢复的是各自调用前的原值,而重画本身要按
389
+ * 新状态重设这些钮的可用性——顺序反了的话,刚算出来的禁用规则会被恢复动作
390
+ * 原样抹掉。
391
+ */
392
+ async function act(fn: () => Promise<MinecraftWorldState>, working: string): Promise<void> {
393
+ msg.say(working);
394
+ const lock = ui.disable(btnCreate, btnApply);
395
+ let next: MinecraftWorldState | null = null;
396
+ try {
397
+ next = await fn();
398
+ } catch (err) {
399
+ if (!ctx.signal.aborted) msg.say(`操作失败: ${errText(err)}`, true);
400
+ } finally {
401
+ lock.dispose();
402
+ }
403
+ if (next) render(next);
404
+ }
405
+
406
+ btnCreate.addEventListener('click', () => {
407
+ void act(() => ctx.invoke<MinecraftWorldState>('create', [newName.value, {
408
+ seed: newSeed.value,
409
+ levelType: typeSel.value,
410
+ generatorSettings: genText.value,
411
+ generateStructures: structIn.checked,
412
+ }]), '登记中…');
413
+ }, { signal: ctx.signal });
414
+
415
+ btnApply.addEventListener('click', () => {
416
+ // 运行期间仅提交支持热更新的字段。
417
+ const patch = cur?.live
418
+ ? { gamemode: gmSel.value, difficulty: diffSel.value }
419
+ : {
420
+ gamemode: gmSel.value,
421
+ difficulty: diffSel.value,
422
+ hardcore: hardcoreIn.checked,
423
+ pvp: pvpIn.checked,
424
+ spawnMonsters: monstersIn.checked,
425
+ allowNether: netherIn.checked,
426
+ spawnProtection: Number(spawnProtIn.value),
427
+ viewDistance: Number(viewIn.value),
428
+ simulationDistance: Number(simIn.value),
429
+ maxWorldSize: Number(borderIn.value),
430
+ };
431
+ void act(() => ctx.invoke<MinecraftWorldState>('apply', [patch]), '应用中…');
432
+ }, { signal: ctx.signal });
433
+
434
+ void refresh();
435
+ },
436
+ };
437
+
438
+ /**
439
+ * 卡片内的折叠块。`ui.foldSheet` 折的是整张卡,而这里要的是"卡里那几项平时收着"
440
+ * ——把次要参数摊在主表单里,主表单就没有主次可言了。
441
+ */
442
+ function fold(ctx: ConsolePanelContext, title: string, note: string): { el: HTMLElement; body: HTMLElement } {
443
+ const el = ctx.ui.h('details', 'mc-fold');
444
+ const sum = ctx.ui.h('summary', null, title);
445
+ const body = ctx.ui.h('div', 'mc-foldbody');
446
+ if (note) body.append(ctx.ui.h('div', 'pagedesc', note));
447
+ el.append(sum, body);
448
+ return { el, body };
449
+ }