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,492 @@
1
+ /**
2
+ * Palette 蓝图格式的解析与校验。layers[y][z][x] 为 Palette 索引满矩阵,y 自下向上、x 自西向东、z 自北向南。
3
+ * Minecraft 状态校验由 blueprint-registry.ts 负责;格式修复由 blueprint-repair.ts 负责。
4
+ */
5
+
6
+ export type SizeXYZ = [x: number, y: number, z: number];
7
+ export type PositionXYZ = [x: number, y: number, z: number];
8
+
9
+ /** 新建与改造均须确认冲突后清场;改造首次调用只调查现场。 */
10
+ type BlueprintSiteMode = 'new' | 'retrofit';
11
+
12
+ export interface ParsedBlockState {
13
+ id: string;
14
+ properties: Record<string, string>;
15
+ }
16
+
17
+ /** 规范化后的蓝图:每一格都是一条完整 block-state 串,索引已经展开 */
18
+ export interface NormalizedBlueprint {
19
+ size_xyz: SizeXYZ;
20
+ site_mode: BlueprintSiteMode;
21
+ /** layers[y][z][x] */
22
+ layers: string[][][];
23
+ }
24
+
25
+ const IDENTIFIER_PATTERN = /^([a-z0-9_.-]+):([a-z0-9/._-]+)$/;
26
+ const PROPERTY_NAME_PATTERN = /^[a-z0-9_]+$/;
27
+ const PROPERTY_VALUE_PATTERN = /^[a-z0-9_.-]+$/;
28
+ const KEY_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
29
+
30
+ /** 固定轴序;矩阵按 高度 → 南北 → 东西 三层数组读 */
31
+ export const AXIS_ORDER = 'YZX' as const;
32
+
33
+ /** 校验错误包含精确路径和中文说明。 */
34
+ export class BlueprintValidationError extends Error {
35
+ constructor(
36
+ message: string,
37
+ readonly path: string,
38
+ readonly reason = message,
39
+ ) {
40
+ super(`${path}: ${message}`);
41
+ this.name = 'BlueprintValidationError';
42
+ }
43
+ }
44
+
45
+ function fail(path: string, message: string): never {
46
+ throw new BlueprintValidationError(message, path);
47
+ }
48
+
49
+ function isRecord(value: unknown): value is Record<string, unknown> {
50
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
51
+ }
52
+
53
+ /** 稀疏数组的空洞按缺格处理。 */
54
+ function expectDenseArray(value: unknown, path: string): unknown[] {
55
+ if (!Array.isArray(value)) fail(path, '这里应该是一个数组');
56
+ for (let index = 0; index < value.length; index++) {
57
+ if (!(index in value)) fail(`${path}[${index}]`, '数组这一位是空洞,缺一个元素');
58
+ }
59
+ return value;
60
+ }
61
+
62
+ function expectString(value: unknown, path: string): string {
63
+ if (typeof value !== 'string') fail(path, '这里应该是一个字符串');
64
+ return value;
65
+ }
66
+
67
+ function validateSize(value: unknown, path = 'size_xyz'): SizeXYZ {
68
+ const tuple = expectDenseArray(value, path);
69
+ if (tuple.length !== 3) fail(path, `应该正好是 [X,Y,Z] 三个数,给了 ${tuple.length} 个`);
70
+ const result = tuple.map((dimension, index) => {
71
+ if (!Number.isSafeInteger(dimension) || (dimension as number) <= 0) {
72
+ fail(`${path}[${index}]`, '应该是一个正整数');
73
+ }
74
+ return dimension as number;
75
+ });
76
+ return result as SizeXYZ;
77
+ }
78
+
79
+ /** 方块 id:必须带命名空间,全小写(`minecraft:oak_planks`) */
80
+ export function parseBlockId(value: unknown, path = 'block id'): string {
81
+ const id = expectString(value, path);
82
+ if (!IDENTIFIER_PATTERN.test(id)) {
83
+ fail(path, `${JSON.stringify(id)} 不是带命名空间的小写方块 id(应形如 minecraft:oak_planks)`);
84
+ }
85
+ return id;
86
+ }
87
+
88
+ /** 解析完整方块状态串。 */
89
+ export function parseBlockState(value: unknown, path = 'block state'): ParsedBlockState {
90
+ const state = expectString(value, path);
91
+ const bracket = state.indexOf('[');
92
+ const idText = bracket === -1 ? state : state.slice(0, bracket);
93
+ const id = parseBlockId(idText, path);
94
+
95
+ if (bracket === -1) return { id, properties: {} };
96
+ if (!state.endsWith(']') || state.indexOf('[', bracket + 1) !== -1) {
97
+ fail(path, '属性方括号没配对');
98
+ }
99
+
100
+ const propertyText = state.slice(bracket + 1, -1);
101
+ if (propertyText.length === 0) fail(path, '属性方括号里是空的');
102
+
103
+ const properties: Record<string, string> = {};
104
+ for (const assignment of propertyText.split(',')) {
105
+ const separator = assignment.indexOf('=');
106
+ if (
107
+ separator <= 0
108
+ || separator === assignment.length - 1
109
+ || assignment.indexOf('=', separator + 1) !== -1
110
+ ) {
111
+ fail(path, `属性写法不对:${JSON.stringify(assignment)}(应形如 facing=north)`);
112
+ }
113
+ const name = assignment.slice(0, separator);
114
+ const propertyValue = assignment.slice(separator + 1);
115
+ if (!PROPERTY_NAME_PATTERN.test(name)) {
116
+ fail(path, `属性名不合法:${JSON.stringify(name)}`);
117
+ }
118
+ if (!PROPERTY_VALUE_PATTERN.test(propertyValue)) {
119
+ fail(path, `属性 ${JSON.stringify(name)} 的值不合法`);
120
+ }
121
+ if (Object.hasOwn(properties, name)) {
122
+ fail(path, `属性 ${JSON.stringify(name)} 写了两遍`);
123
+ }
124
+ properties[name] = propertyValue;
125
+ }
126
+ return { id, properties };
127
+ }
128
+
129
+ /** 按属性名排序序列化状态串。 */
130
+ export function formatBlockState(state: ParsedBlockState): string {
131
+ const id = parseBlockId(state.id);
132
+ const entries = Object.entries(state.properties)
133
+ .sort(([left], [right]) => left.localeCompare(right));
134
+ if (entries.length === 0) return id;
135
+
136
+ for (const [name, value] of entries) {
137
+ if (!PROPERTY_NAME_PATTERN.test(name)) fail(`properties.${name}`, '属性名不合法');
138
+ if (!PROPERTY_VALUE_PATTERN.test(value)) fail(`properties.${name}`, '属性值不合法');
139
+ }
140
+ return `${id}[${entries.map(([name, value]) => `${name}=${value}`).join(',')}]`;
141
+ }
142
+
143
+ /** 状态串规范化:解析一遍再排序打印,消掉属性顺序差异 */
144
+ export function canonicalizeBlockState(value: unknown, path?: string): string {
145
+ return formatBlockState(parseBlockState(value, path));
146
+ }
147
+
148
+ /** palette 一条 → 规范化状态串;字符串与 `{Name, Properties}` 两种写法都收 */
149
+ export function paletteEntryToState(value: unknown, path = 'palette entry'): string {
150
+ if (typeof value === 'string') return canonicalizeBlockState(value, path);
151
+ if (!isRecord(value)) fail(path, '这里应该是状态串,或者 {Name, Properties} 对象');
152
+
153
+ const id = parseBlockId(value.Name, `${path}.Name`);
154
+ if (value.Properties === undefined) return id;
155
+ if (!isRecord(value.Properties)) fail(`${path}.Properties`, '这里应该是一个对象');
156
+
157
+ const properties: Record<string, string> = {};
158
+ for (const [name, propertyValue] of Object.entries(value.Properties)) {
159
+ if (!PROPERTY_NAME_PATTERN.test(name)) {
160
+ fail(`${path}.Properties.${name}`, '属性名不合法');
161
+ }
162
+ if (typeof propertyValue !== 'string' || !PROPERTY_VALUE_PATTERN.test(propertyValue)) {
163
+ fail(`${path}.Properties.${name}`, '属性值不合法');
164
+ }
165
+ properties[name] = propertyValue;
166
+ }
167
+ return formatBlockState({ id, properties });
168
+ }
169
+
170
+ /** 只带 id 的方块名(丢掉属性);type 比对与直方图都用它 */
171
+ export function blockIdOf(state: string): string {
172
+ const bracket = state.indexOf('[');
173
+ return bracket === -1 ? state : state.slice(0, bracket);
174
+ }
175
+
176
+ /**
177
+ * 不产生放置步骤的状态。`air` 仍属于工地并要求为空;`structure_void` 表示这格
178
+ * 不属于工地,施工与冲突核对都跳过。
179
+ */
180
+ const AIR_IDS = new Set([
181
+ 'minecraft:air',
182
+ 'minecraft:cave_air',
183
+ 'minecraft:void_air',
184
+ 'minecraft:structure_void',
185
+ ]);
186
+
187
+ export function isAirState(state: string): boolean {
188
+ return AIR_IDS.has(blockIdOf(state));
189
+ }
190
+
191
+ /** structure_void 表示这格不属于工地:不放、不挖、也不参与冲突核对。 */
192
+ export function isStructureVoidState(state: string): boolean {
193
+ return blockIdOf(state) === 'minecraft:structure_void';
194
+ }
195
+
196
+ /** 世界侧读回来的名字规范化:`stone` / `minecraft:stone` 都收,统一成带前缀 */
197
+ export function normalizeBlockName(name: string): string {
198
+ const trimmed = name.trim();
199
+ return trimmed.includes(':') ? trimmed : `minecraft:${trimmed}`;
200
+ }
201
+
202
+ function validateGrid(
203
+ value: unknown,
204
+ size: SizeXYZ,
205
+ parseCell: (cell: unknown, path: string) => string,
206
+ ): string[][][] {
207
+ const [sizeX, sizeY, sizeZ] = size;
208
+ const layers = expectDenseArray(value, 'layers');
209
+ if (layers.length !== sizeY) {
210
+ fail('layers', `声明高 ${sizeY} 层,矩阵给了 ${layers.length} 层`);
211
+ }
212
+
213
+ return layers.map((layer, y) => {
214
+ const rows = expectDenseArray(layer, `layers[${y}]`);
215
+ if (rows.length !== sizeZ) {
216
+ fail(`layers[${y}]`, `第 ${y} 层应有 ${sizeZ} 行(Z),给了 ${rows.length} 行`);
217
+ }
218
+ return rows.map((row, z) => {
219
+ const cells = expectDenseArray(row, `layers[${y}][${z}]`);
220
+ if (cells.length !== sizeX) {
221
+ fail(`layers[${y}][${z}]`, `这一行应有 ${sizeX} 格(X),给了 ${cells.length} 格`);
222
+ }
223
+ return cells.map((cell, x) => parseCell(cell, `layers[${y}][${z}][${x}]`));
224
+ });
225
+ });
226
+ }
227
+
228
+ export interface NormalizeOptions {
229
+ /** 给了就核对声明尺寸;不给就以提交的尺寸为准 */
230
+ expectedSize?: SizeXYZ;
231
+ }
232
+
233
+ /** 有 palette 时解析索引矩阵;无 palette 时解析逐格状态串。 */
234
+ export function normalizeBlueprint(
235
+ value: unknown,
236
+ options: NormalizeOptions = {},
237
+ ): NormalizedBlueprint {
238
+ if (!isRecord(value)) fail('submission', '提交应该是一个对象');
239
+
240
+ const size = validateSize(value.size_xyz);
241
+ const siteMode = value.site_mode === undefined
242
+ ? fail('site_mode', '必须选:new(空地新建)或 retrofit(改造前探测)')
243
+ : expectString(value.site_mode, 'site_mode');
244
+ if (siteMode !== 'new' && siteMode !== 'retrofit') {
245
+ fail('site_mode', '只认 new(空地新建)或 retrofit(改造前探测)');
246
+ }
247
+ const { expectedSize } = options;
248
+ if (expectedSize !== undefined) {
249
+ const expected = validateSize(expectedSize, 'expected_size');
250
+ if (!expected.every((dimension, index) => dimension === size[index])) {
251
+ fail('size_xyz', `这一份要求 [${expected.join(',')}],提交写的是 [${size.join(',')}]`);
252
+ }
253
+ }
254
+
255
+ if (value.axis_order !== undefined) {
256
+ const axis = expectString(value.axis_order, 'axis_order');
257
+ if (axis !== AXIS_ORDER) {
258
+ fail('axis_order', `轴序固定写 ${AXIS_ORDER},给的是 ${JSON.stringify(axis)}`);
259
+ }
260
+ }
261
+
262
+ if (value.palette === undefined) {
263
+ return {
264
+ size_xyz: size,
265
+ site_mode: siteMode,
266
+ layers: validateGrid(value.layers, size, canonicalizeBlockState),
267
+ };
268
+ }
269
+
270
+ const rawPalette = expectDenseArray(value.palette, 'palette');
271
+ if (rawPalette.length === 0) fail('palette', '对照表至少要有一条');
272
+ const palette = rawPalette.map((entry, index) =>
273
+ paletteEntryToState(entry, `palette[${index}]`));
274
+
275
+ const layers = validateGrid(value.layers, size, (cell, path) => {
276
+ if (!Number.isSafeInteger(cell)) fail(path, '这一格应该是 palette 的整数索引');
277
+ const index = cell as number;
278
+ if (index < 0 || index >= palette.length) {
279
+ fail(path, `palette 索引 ${index} 越界,对照表只有 0..${palette.length - 1}`);
280
+ }
281
+ return palette[index];
282
+ });
283
+
284
+ return { size_xyz: size, site_mode: siteMode, layers };
285
+ }
286
+
287
+ /** 不抛异常的解析口:错误按精确路径结构化返回,给需要拼回执的调用方 */
288
+ type BlueprintParseResult =
289
+ | { ok: true; blueprint: NormalizedBlueprint }
290
+ | { ok: false; path: string; reason: string };
291
+
292
+ export function tryNormalizeBlueprint(
293
+ value: unknown,
294
+ options: NormalizeOptions = {},
295
+ ): BlueprintParseResult {
296
+ try {
297
+ return { ok: true, blueprint: normalizeBlueprint(value, options) };
298
+ } catch (error) {
299
+ if (error instanceof BlueprintValidationError) {
300
+ return { ok: false, path: error.path, reason: error.reason };
301
+ }
302
+ throw error;
303
+ }
304
+ }
305
+
306
+ /** 运行时使用的蓝图键和显示名。 */
307
+ export interface BlueprintLabel {
308
+ key: string;
309
+ name: string | null;
310
+ }
311
+
312
+ export function validateBlueprintLabel(value: unknown): BlueprintLabel {
313
+ if (!isRecord(value)) fail('submission', '提交应该是一个对象');
314
+ const key = expectString(value.key, 'key').trim();
315
+ if (!KEY_PATTERN.test(key)) {
316
+ fail('key', '键用小写字母数字加连字符,开头是字母或数字,最长 32 个字符');
317
+ }
318
+ const name = value.name === undefined || value.name === null
319
+ ? null
320
+ : expectString(value.name, 'name').trim() || null;
321
+ return { key, name };
322
+ }
323
+
324
+ // ── 多部件方块 ────────────────────────────────────────────────────────────────
325
+
326
+ export type Cardinal = 'north' | 'south' | 'west' | 'east';
327
+
328
+ /** 四个水平朝向的单位向量(X 西→东,Z 北→南,与蓝图轴向一致) */
329
+ export const CARDINAL_VECTORS: Record<Cardinal, readonly [number, number, number]> = {
330
+ north: [0, 0, -1],
331
+ south: [0, 0, 1],
332
+ west: [-1, 0, 0],
333
+ east: [1, 0, 0],
334
+ };
335
+
336
+ function isCardinal(value: string): value is Cardinal {
337
+ return value === 'north' || value === 'south' || value === 'west' || value === 'east';
338
+ }
339
+
340
+ /**
341
+ * 按注册表属性识别多部件方块:half=upper/lower 或 part=head/foot。
342
+ * 从部件不单独生成步骤,mainOffset 指向对应主部件。
343
+ */
344
+ type MultiPartRole =
345
+ | { role: 'single' }
346
+ | { role: 'main'; secondaryOffset: readonly [number, number, number] }
347
+ | { role: 'secondary'; mainOffset: readonly [number, number, number] };
348
+
349
+ export function multiPartRole(state: string): MultiPartRole {
350
+ const parsed = parseBlockState(state);
351
+ const half = parsed.properties.half;
352
+ if (half === 'upper') return { role: 'secondary', mainOffset: [0, -1, 0] };
353
+ if (half === 'lower') return { role: 'main', secondaryOffset: [0, 1, 0] };
354
+
355
+ const part = parsed.properties.part;
356
+ if (part === 'head' || part === 'foot') {
357
+ const facing = parsed.properties.facing;
358
+ if (facing === undefined || !isCardinal(facing)) return { role: 'single' };
359
+ const [dx, dy, dz] = CARDINAL_VECTORS[facing];
360
+ // 取负别落出 -0(它和 0 相等但深比较不认),0 原样返回
361
+ const neg = (value: number): number => (value === 0 ? 0 : -value);
362
+ // 床的 facing 从床脚指向床头
363
+ return part === 'foot'
364
+ ? { role: 'main', secondaryOffset: [dx, dy, dz] }
365
+ : { role: 'secondary', mainOffset: [neg(dx), neg(dy), neg(dz)] };
366
+ }
367
+ return { role: 'single' };
368
+ }
369
+
370
+ // ── 体素指标与逐层图 ──────────────────────────────────────────────────────────
371
+
372
+ interface Bounds3D {
373
+ min: PositionXYZ;
374
+ max: PositionXYZ;
375
+ size: SizeXYZ;
376
+ }
377
+
378
+ export interface VoxelMetrics {
379
+ size_xyz: SizeXYZ;
380
+ totalCells: number;
381
+ airCells: number;
382
+ nonAirCells: number;
383
+ /** 非空气占比,0..1,两位小数 */
384
+ fillRate: number;
385
+ uniqueBlockStates: number;
386
+ uniqueBlockIds: number;
387
+ /** 状态 → 格数,按格数降序 */
388
+ blockStateHistogram: Record<string, number>;
389
+ /** 方块 id → 格数,按格数降序 */
390
+ blockIdHistogram: Record<string, number>;
391
+ /** 非空气格的包围盒;整份都是空气时是 null */
392
+ nonAirBounds: Bounds3D | null;
393
+ }
394
+
395
+ function sortedHistogram(values: string[]): Record<string, number> {
396
+ const counts = new Map<string, number>();
397
+ for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
398
+ return Object.fromEntries(
399
+ [...counts].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])),
400
+ );
401
+ }
402
+
403
+ /** 遍历所有格:回调拿到局部坐标与该格状态,顺序是 y→z→x */
404
+ export function forEachCell(
405
+ blueprint: NormalizedBlueprint,
406
+ visit: (x: number, y: number, z: number, state: string) => void,
407
+ ): void {
408
+ const [sizeX, sizeY, sizeZ] = blueprint.size_xyz;
409
+ for (let y = 0; y < sizeY; y++) {
410
+ for (let z = 0; z < sizeZ; z++) {
411
+ for (let x = 0; x < sizeX; x++) visit(x, y, z, blueprint.layers[y][z][x]);
412
+ }
413
+ }
414
+ }
415
+
416
+ export function voxelMetrics(blueprint: NormalizedBlueprint): VoxelMetrics {
417
+ const states: string[] = [];
418
+ const ids: string[] = [];
419
+ let nonAirCells = 0;
420
+ const min: PositionXYZ = [Infinity, Infinity, Infinity];
421
+ const max: PositionXYZ = [-Infinity, -Infinity, -Infinity];
422
+
423
+ forEachCell(blueprint, (x, y, z, state) => {
424
+ states.push(state);
425
+ ids.push(blockIdOf(state));
426
+ if (isAirState(state)) return;
427
+ nonAirCells++;
428
+ min[0] = Math.min(min[0], x); max[0] = Math.max(max[0], x);
429
+ min[1] = Math.min(min[1], y); max[1] = Math.max(max[1], y);
430
+ min[2] = Math.min(min[2], z); max[2] = Math.max(max[2], z);
431
+ });
432
+
433
+ const totalCells = states.length;
434
+ return {
435
+ size_xyz: [...blueprint.size_xyz],
436
+ totalCells,
437
+ airCells: totalCells - nonAirCells,
438
+ nonAirCells,
439
+ fillRate: totalCells === 0 ? 0 : Math.round((nonAirCells / totalCells) * 100) / 100,
440
+ uniqueBlockStates: new Set(states).size,
441
+ uniqueBlockIds: new Set(ids).size,
442
+ blockStateHistogram: sortedHistogram(states),
443
+ blockIdHistogram: sortedHistogram(ids),
444
+ nonAirBounds: nonAirCells === 0 ? null : {
445
+ min,
446
+ max,
447
+ size: [max[0] - min[0] + 1, max[1] - min[1] + 1, max[2] - min[2] + 1],
448
+ },
449
+ };
450
+ }
451
+
452
+ interface BlueprintLayerMap {
453
+ /** 图例:字符 → 状态与格数;空气固定是 `.` 不进图例 */
454
+ legend: Array<{ char: string; state: string; cells: number }>;
455
+ /** 每层一张图,`rows[z]` 是从北到南第 z 行,行内从西到东 */
456
+ layers: Array<{ y: number; nonAirCells: number; rows: string[] }>;
457
+ }
458
+
459
+ const LEGEND_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
460
+
461
+ /** 供 dryRun 与核验回执使用的逐层字符图;超过 62 种的状态显示为 ?,不进入图例。 */
462
+ export function renderLayerMap(blueprint: NormalizedBlueprint): BlueprintLayerMap {
463
+ const counts = new Map<string, number>();
464
+ forEachCell(blueprint, (_x, _y, _z, state) => {
465
+ if (isAirState(state)) return;
466
+ counts.set(state, (counts.get(state) ?? 0) + 1);
467
+ });
468
+ const ordered = [...counts]
469
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
470
+
471
+ const charOf = new Map<string, string>();
472
+ const legend: BlueprintLayerMap['legend'] = [];
473
+ for (const [state, cells] of ordered) {
474
+ if (legend.length >= LEGEND_CHARS.length) break;
475
+ const char = LEGEND_CHARS[legend.length];
476
+ charOf.set(state, char);
477
+ legend.push({ char, state, cells });
478
+ }
479
+
480
+ const [, sizeY] = blueprint.size_xyz;
481
+ const layers: BlueprintLayerMap['layers'] = [];
482
+ for (let y = 0; y < sizeY; y++) {
483
+ let nonAirCells = 0;
484
+ const rows = blueprint.layers[y].map((row) => row.map((state) => {
485
+ if (isAirState(state)) return '.';
486
+ nonAirCells++;
487
+ return charOf.get(state) ?? '?';
488
+ }).join(''));
489
+ layers.push({ y, nonAirCells, rows });
490
+ }
491
+ return { legend, layers };
492
+ }