cortico 0.1.0 → 0.1.2

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 (112) hide show
  1. package/README.md +7 -14
  2. package/package.json +1 -3
  3. package/src/boot.ts +18 -0
  4. package/src/bot.ts +41 -6
  5. package/src/core/README.md +4 -6
  6. package/src/core/blobs.ts +23 -2
  7. package/src/core/config-schema.ts +1 -1
  8. package/src/core/config.ts +2 -16
  9. package/src/core/core.ts +15 -5
  10. package/src/core/loop.ts +55 -27
  11. package/src/core/session.ts +3 -2
  12. package/src/core/timers.ts +8 -6
  13. package/src/core/types.ts +19 -3
  14. package/src/core/util.ts +17 -1
  15. package/src/deploy.ts +4 -1
  16. package/src/extensions/README.md +6 -4
  17. package/src/extensions/dry-mount.ts +23 -2
  18. package/src/extensions/manifest.ts +21 -11
  19. package/src/extensions.ts +147 -23
  20. package/src/launcher.ts +23 -9
  21. package/src/protocol/open-responses/context-log.ts +37 -9
  22. package/src/providers/README.md +32 -8
  23. package/src/providers/base.ts +3 -1
  24. package/src/providers/configuration.ts +2 -1
  25. package/src/providers/console/hub.ts +270 -0
  26. package/src/providers/console/settings.ts +8 -18
  27. package/src/providers/console/types.ts +5 -0
  28. package/src/providers/hub-api.ts +3 -0
  29. package/src/providers/llamacpp/config.ts +8 -6
  30. package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
  31. package/src/providers/llamacpp/console/server.ts +3 -1
  32. package/src/providers/llamacpp/index.ts +3 -1
  33. package/src/providers/llamacpp/native.ts +10 -5
  34. package/src/providers/llamacpp/strings.ts +6 -6
  35. package/src/providers/name.ts +8 -0
  36. package/src/providers/openai-responses-compat/config.ts +13 -0
  37. package/src/providers/openai-responses-compat/console/client.ts +5 -0
  38. package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
  39. package/src/providers/openai-responses-compat/console/server.ts +127 -0
  40. package/src/providers/openai-responses-compat/index.ts +38 -13
  41. package/src/providers/openai-responses-compat/native.ts +9 -4
  42. package/src/providers/openai-responses-compat/strings.ts +62 -1
  43. package/src/providers/registry.ts +5 -0
  44. package/src/providers/transport/responses-input.ts +59 -10
  45. package/src/web/README.md +11 -6
  46. package/src/web/auth.ts +80 -0
  47. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
  48. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
  49. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
  50. package/src/web/client/console-pages/host.ts +24 -7
  51. package/src/web/client/core/api.ts +5 -1
  52. package/src/web/client/core/router.ts +15 -0
  53. package/src/web/client/features/core/strings.ts +2 -2
  54. package/src/web/client/features/extensions/index.ts +276 -41
  55. package/src/web/client/features/extensions/strings.ts +84 -10
  56. package/src/web/client/features/feature.ts +2 -2
  57. package/src/web/client/features/live/diagnostics.ts +32 -0
  58. package/src/web/client/features/live/index.ts +34 -11
  59. package/src/web/client/features/live/onboarding.ts +4 -1
  60. package/src/web/client/features/live/protocol.ts +1 -0
  61. package/src/web/client/features/live/strings.ts +20 -14
  62. package/src/web/client/features/live/timeline.ts +2 -1
  63. package/src/web/client/features/providers/detail.ts +262 -0
  64. package/src/web/client/features/providers/drafts.ts +23 -0
  65. package/src/web/client/features/providers/index.ts +173 -87
  66. package/src/web/client/features/providers/strings.ts +44 -17
  67. package/src/web/client/features/providers/types.ts +15 -0
  68. package/src/web/client/features/settings/general.ts +13 -0
  69. package/src/web/client/features/settings/index.ts +1 -0
  70. package/src/web/client/features/settings/strings.ts +6 -0
  71. package/src/web/client/features/worlds/index.ts +1 -0
  72. package/src/web/client/main.ts +4 -0
  73. package/src/web/client/shell/index.ts +28 -48
  74. package/src/web/client/shell/strings.ts +0 -22
  75. package/src/web/client/ui/icons.ts +14 -1
  76. package/src/web/client/ui/prompt-input.tsx +17 -5
  77. package/src/web/client/ui/strings.ts +0 -2
  78. package/src/web/console-pages.ts +1 -1
  79. package/src/web/diagnostics.ts +133 -0
  80. package/src/web/public/login.html +67 -0
  81. package/src/web/public/styles.css +143 -26
  82. package/src/web/server.ts +233 -19
  83. package/src/web/shared/client-panel.ts +4 -1
  84. package/src/web/shared/console-protocol.ts +4 -1
  85. package/src/worlds/bilibili/README.md +1 -1
  86. package/src/worlds/bilibili/overlay/server.ts +4 -2
  87. package/src/worlds/minecraft/ADAPT.md +66 -0
  88. package/src/worlds/minecraft/README.md +69 -11
  89. package/src/worlds/minecraft/cell-facts.ts +226 -0
  90. package/src/worlds/minecraft/chests.ts +5 -0
  91. package/src/worlds/minecraft/containers.ts +325 -0
  92. package/src/worlds/minecraft/entity-facts.ts +31 -5
  93. package/src/worlds/minecraft/executor.ts +283 -10348
  94. package/src/worlds/minecraft/inventory.ts +268 -0
  95. package/src/worlds/minecraft/melee.ts +419 -0
  96. package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
  97. package/src/worlds/minecraft/placed-ledger.ts +152 -0
  98. package/src/worlds/minecraft/placement.ts +1038 -0
  99. package/src/worlds/minecraft/receipt.ts +340 -0
  100. package/src/worlds/minecraft/skill-context.ts +358 -0
  101. package/src/worlds/minecraft/skills-build.ts +1169 -0
  102. package/src/worlds/minecraft/skills-container.ts +1343 -0
  103. package/src/worlds/minecraft/skills-craft.ts +333 -0
  104. package/src/worlds/minecraft/skills-dig.ts +624 -0
  105. package/src/worlds/minecraft/skills-gather.ts +1230 -0
  106. package/src/worlds/minecraft/skills-interact.ts +1559 -0
  107. package/src/worlds/minecraft/tools.ts +331 -0
  108. package/src/worlds/minecraft/travel.ts +763 -0
  109. package/src/worlds/minecraft/until.ts +75 -0
  110. package/src/worlds/qq/normalize.ts +14 -0
  111. package/src/worlds/qq/world.ts +53 -7
  112. package/src/worlds/terminal/world.ts +5 -3
@@ -0,0 +1,340 @@
1
+ /**
2
+ * 回执的说法:这一步是什么、受阻那一刻的现场、核验结论怎么念。
3
+ *
4
+ * 只出文本,不改世界也不发包;两处(bagNow、noDropMaterials)读背包是为了把读数写进那句话。
5
+ * 判据在各技能族,这里只负责措辞。
6
+ */
7
+ import type { Bot } from 'mineflayer';
8
+ import { zhDimension, zhEntity, zhName } from './names.ts';
9
+ import { INVENTORY_SLOTS } from './inventory.ts';
10
+ import { DIRECTION_ZH, narrateInventory, type Direction, type ItemStack } from './terrain.ts';
11
+ import { SkillBlocked } from './skill-context.ts';
12
+ import { NEAR_DEFAULT, type Expectation, type SkillCall } from './skills.ts';
13
+ import { FACE_ZH, SHAPE_ZH } from './cell-facts.ts';
14
+ import { DRINKABLES, readEnchants } from './item-facts.ts';
15
+ import { equipDestOf } from './tools.ts';
16
+ import { type Anchor, type BoxFill, type ShapeName } from './geometry.ts';
17
+ import { LIQUIDS, type RegionReading } from './cell-facts.ts';
18
+ import { minHarvestTool } from './tools.ts';
19
+
20
+ /**
21
+ * mineflayer/pathfinder 的英文报错翻成中文再进事件:上下文里除方块/物品 id 之外
22
+ * 不该混进英文,而反复出现的报错原文是最大的一处渗入。
23
+ * 认不出的报错原样保留(排查后再补映射)。
24
+ */
25
+ export function zhErrorText(msg: string): string {
26
+ if (/path was stopped/i.test(msg)) return '寻路半途被叫停';
27
+ // A* 超时只表示限时内未搜完,不能据此断言目标太远或无路。
28
+ if (/took to+ long to decide/i.test(msg)) return '限时内没算完';
29
+ if (/goal was changed/i.test(msg)) return '目标中途被更换';
30
+ if (/no path to the goal/i.test(msg)) return '找不到可行路线';
31
+ if (/digging aborted/i.test(msg)) return '挖到一半被打断了';
32
+ if (/timeout|timed? out/i.test(msg)) return '超时';
33
+ if (/must be holding an item to place/i.test(msg)) return '手上没有东西可放';
34
+ // mineflayer-fixes 的英文错误供寻路器兼容 catch 识别;此处转换为回执文案。
35
+ {
36
+ const still = /no block has been placed\s*:\s*the block is still (\w+)/i.exec(msg);
37
+ if (still) {
38
+ return still[1] === 'air' || still[1] === 'undefined'
39
+ ? '放下去了但那一格还是空的(服务端没接受这次放置)'
40
+ : `放不上:那一格还是${zhName(still[1])}`;
41
+ }
42
+ }
43
+ // 主物品栏共 36 格:9 格快捷栏和 27 格背包。
44
+ if (/unable to withdraw.*inventory is full/i.test(msg)) return `背包 ${INVENTORY_SLOTS} 格全满了,取不出来`;
45
+ if (/destination full/i.test(msg)) return '那一边没空位了(箱子满了或包满了)';
46
+ if (/can't find .* in slots/i.test(msg)) return '窗口里已经找不到这样东西了';
47
+ if (/fishing cancelled/i.test(msg)) return '浮标没了,这竿作废(被收走或钩到了别处)';
48
+ return msg;
49
+ }
50
+
51
+ /** 受阻是谁说的:非 SkillBlocked 的一律算机器自己的问题 */
52
+ export function blockedSourceOf(err: unknown): 'server' | 'local' {
53
+ return err instanceof SkillBlocked ? err.source : 'local';
54
+ }
55
+
56
+ /** build 的「她自己点格子」两形态(贴面 / 锚点);蓝图形态另走 skillBuildBlueprint */
57
+ export type PlaceCall = Extract<SkillCall, { skill: 'build'; material: string }>;
58
+ /** build 的蓝图形态 */
59
+ export type BlueprintCall = Extract<SkillCall, { skill: 'build'; blueprint: string }>;
60
+
61
+ /** heldItem 由调用方提供,供 use 未指定 item 时呈现实际手持物。 */
62
+ export function describeSkill(c: SkillCall, heldItem?: string | null): string {
63
+ const tool = 'tool' in c && c.tool
64
+ ? `(${c.tool === 'fastest' ? '最快工具' : `指定${zhName(c.tool)}`})`
65
+ : '';
66
+ switch (c.skill) {
67
+ case 'goto': {
68
+ const where = c.groundY ? `(${c.at[0]},${c.at[2]}) 的地表` : `坐标 ${anchorsText([c.at])}`;
69
+ const dimension = c.dimension ? `[${zhDimension(c.dimension)}] ` : '';
70
+ return c.dryRun ? `探路到 ${dimension}${where}` : `去${dimension}${where}`;
71
+ }
72
+ case 'transit': return `穿过 ${anchorsText([c.at])} 的下界传送门`;
73
+ case 'goto_player': return `去 ${c.name} 身边`;
74
+ case 'follow': return `跟着 ${c.name}`;
75
+ case 'find': return c.direction
76
+ ? `朝${DIRECTION_ZH[c.direction]}找${zhThing(c.target)}(最多 ${c.distance} 格${untilText(c.until)})`
77
+ : `在周围 ${c.distance} 格内找${zhThing(c.target)}`;
78
+ case 'flee': return `远离敌对生物(拉开 ${c.distance} 格)`;
79
+ case 'surface': return '脱离水体或向上到露天';
80
+ case 'collect': return `采集 ${c.count} 个${zhName(c.block)}${c.buried ? '(可挖过去)' : ''}${tool}`;
81
+ case 'fish': return `钓一竿${c.at ? `(在 ${anchorsText([c.at])})` : ''}`;
82
+ case 'build': {
83
+ // 一步可以下 16 处;任务名每份快照都要重发一遍,列全就是 16 份坐标的常驻开销
84
+ const head = c.dryRun ? '试算:' : '';
85
+ const more = (n: number): string => (n > LABEL_SPOTS ? `等 ${n} 处` : '');
86
+ if ('blueprint' in c) {
87
+ const where = c.at ? `,锚点 ${anchorsText([c.at])}` : '';
88
+ const stop = c.stopAfter === undefined ? '' : `,施工到第 ${c.stopAfter} 层`;
89
+ return `${head}按蓝图「${c.blueprint}」施工${where}${stop}`;
90
+ }
91
+ if ('on' in c) {
92
+ const where = c.on.slice(0, LABEL_SPOTS)
93
+ .map((o) => `${anchorsText([o.at])} 的${FACE_ZH[o.face]}面`).join('、');
94
+ return `${head}把${zhName(c.material)}贴着${where}${more(c.on.length)}放`;
95
+ }
96
+ if (!c.shape) {
97
+ return `${head}把${zhName(c.material)}放到 ${anchorsText(c.anchors.slice(0, LABEL_SPOTS))}${more(c.anchors.length)}`;
98
+ }
99
+ return `${head}沿${SHAPE_ZH[c.shape]}搭${fillText(c)}${zhName(c.material)} ${anchorsText(c.anchors)}`;
100
+ }
101
+ case 'excavate': return `${c.dryRun ? '试算:' : ''}挖开${fillText(c)}${SHAPE_ZH[c.shape]} ${anchorsText(c.anchors)}${tool}`;
102
+ case 'tunnel':
103
+ return `${c.dryRun ? '试算:' : ''}挖${c.spiral ? '螺旋楼梯' : '通道'}到 ${anchorsText([c.at])}` +
104
+ `${c.until && c.until.length > 0 ? `(${untilText(c.until).replace(/^,/, '')})` : ''}${tool}`;
105
+ case 'probe': return `探查${fillText(c)}${SHAPE_ZH[c.shape]} ${anchorsText(c.anchors)}`;
106
+ case 'use': {
107
+ const what = c.item ? `用${zhName(c.item)}` : heldItem ? `用${zhName(heldItem)}` : '空手';
108
+ const n = (c.times ?? 1) > 1 ? ` ${c.times} 次` : '';
109
+ if (c.target) {
110
+ if (c.index === undefined) return `${what}右键${zhEntity(c.target)}${n}`;
111
+ return `按${zhEntity(c.target)}报价 ${c.index} 号成交${n}`;
112
+ }
113
+ if (c.text !== undefined && c.at) {
114
+ return `在 ${anchorsText([c.at])} 的告示牌${c.back ? '背面' : ''}上写 ${signLinesText(c.text)}`;
115
+ }
116
+ if (c.at) return `${what}右键 ${anchorsText([c.at])}${n}`;
117
+ return `${what}右键${n}`;
118
+ }
119
+ case 'craft':
120
+ return c.grid
121
+ ? `按自己摆的格子合成 ${c.count} 次(${gridText(c.grid)})`
122
+ : `合成 ${c.count} 个${zhName(c.item ?? '')}`;
123
+ case 'smelt':
124
+ return `烧 ${c.count} 个${zhName(c.input)}(烧${zhName(c.fuel)})`;
125
+ case 'brew':
126
+ return `酿 ${c.count} 瓶${zhName(c.bottle)}(加${zhName(c.input)})`;
127
+ case 'enchant':
128
+ return c.index === undefined
129
+ ? `看${zhName(c.item)}在 ${anchorsText([c.at])} 的附魔报价`
130
+ : `给${zhName(c.item)}按第 ${c.index} 档附魔`;
131
+ case 'eat': return DRINKABLES[c.item] ? `喝${DRINKABLES[c.item]!.label}` : `吃${zhName(c.item)}`;
132
+ case 'ride': {
133
+ if (c.off) return '从坐骑上下来';
134
+ if (c.target && c.to) return `骑${zhEntity(c.target)}去 ${anchorsText([c.to])}`;
135
+ if (c.target) return `骑上${zhEntity(c.target)}`;
136
+ return `驾着坐骑去 ${anchorsText([c.to!])}`;
137
+ }
138
+ case 'anvil':
139
+ return c.op === 'rename'
140
+ ? `铁砧:给${zhName(c.item)}改名「${c.name}」`
141
+ : `铁砧:把${zhName(c.item)}和${zhName(c.with ?? '')}合一起`;
142
+ case 'grindstone':
143
+ return `砂轮:磨${zhName(c.item)}${c.with ? `+${zhName(c.with)}` : ''}`;
144
+ case 'attack': return `攻击${zhEntity(c.target)}${c.mode && c.mode !== 'auto' ? `(${c.mode})` : ''}`;
145
+ case 'equip': {
146
+ if (!c.item) return '把主手腾空';
147
+ return equipDestOf(c.item) === 'hand' ? `拿出${zhName(c.item)}` : `穿上${zhName(c.item)}`;
148
+ }
149
+ case 'pickup': return c.item ? `捡起附近的${zhName(c.item)}` : '捡起附近的掉落物';
150
+ case 'toss':
151
+ return `扔掉 ${c.count} 个${zhName(c.item)}${c.at ? `,朝 ${anchorsText([c.at])}` : ''}`;
152
+ case 'lead': {
153
+ if (c.off) return '松开牵着的活物';
154
+ const who = c.target ? zhEntity(c.target) : '牵着的活物';
155
+ const tie = c.tie ? `,系到 ${anchorsText([c.tie])} 的栅栏上` : '';
156
+ if (c.to) return `用拴绳把${who}牵去 ${anchorsText([c.to])}${tie}`;
157
+ return c.tie ? `把${who}${tie.slice(1)}` : `用拴绳拴住${who}`;
158
+ }
159
+ case 'stow': return `把 ${c.count} 个${zhName(c.item)}存进箱子`;
160
+ case 'take': {
161
+ if (!c.at) return `从箱子取出 ${c.count ?? 1} 个${zhName(c.item!)}`;
162
+ const spot = anchorsText([c.at]);
163
+ return c.item ? `从 ${spot} 的容器取出 ${c.count ?? 1} 个${zhName(c.item)}` : `掏空 ${spot} 的容器`;
164
+ }
165
+ case 'chat': return `说: ${c.text}`;
166
+ }
167
+ }
168
+
169
+ /** `until` 早停名单进任务描述的那半句;没声明就一个字都不加 */
170
+ export function untilText(until: readonly string[] | undefined): string {
171
+ if (!until || until.length === 0) return '';
172
+ return `,碰到${until.map((n) => (n.startsWith('#') ? n : zhName(n))).join('/')}就停`;
173
+ }
174
+
175
+ /** 她自己摆的合成格进任务描述的样子:按行写,空位写「·」 */
176
+ export function gridText(grid: string[][]): string {
177
+ return grid.map((row) => row.map((n) => (n ? zhName(n) : '·')).join(' ')).join(' / ');
178
+ }
179
+
180
+ export const FILL_ZH: Record<BoxFill, string> = { solid: '实心', outline: '空壳', edges: '框架' };
181
+
182
+ /** 长方体的 fill 写入任务描述;其他形状返回空串。 */
183
+ export function fillText(c: { shape?: ShapeName; fill?: BoxFill }): string {
184
+ return c.shape === 'box' ? FILL_ZH[c.fill ?? 'solid'] : '';
185
+ }
186
+
187
+ /** 任务名里最多列几处放置;再多只报处数(名字随每份快照重发) */
188
+ export const LABEL_SPOTS = 3;
189
+
190
+ /** 锚点序列进任务描述的样子:她写的原样;解析成哪一格由执行回执报 */
191
+ export function anchorsText(anchors: readonly Anchor[]): string {
192
+ return anchors.map((a) => `(${a.join(',')})`).join('→');
193
+ }
194
+
195
+ /** 方块/物品名优先,实体名兜底:find 的 target 两类都收 */
196
+ export function zhThing(name: string): string {
197
+ const asItem = zhName(name);
198
+ return asItem !== name ? asItem : zhEntity(name);
199
+ }
200
+
201
+ /**
202
+ * 期望进回执的样子。判据一律写成对世界的陈述,不带人称——这一句会出现在
203
+ * 每一步的回执里,读的人要能不看调用就知道拿什么在量。
204
+ */
205
+ export function describeExpect(e: Expectation): string {
206
+ if ('has' in e) return `背包内${zhName(e.has.item)} ≥${e.has.count}`;
207
+ if ('near' in e) return `距 (${e.near.join(',')}) ${e.within ?? NEAR_DEFAULT} 格内`;
208
+ if ('holding' in e) return `主手持有${zhName(e.holding.item)}`;
209
+ return `(${e.at.join(',')}) 为${zhName(e.block)}`;
210
+ }
211
+
212
+ /** expect 的评估结果:达成与否 + 两档实测值(短的进回执回显,长的进受阻说明) */
213
+ export interface ExpectVerdict {
214
+ met: boolean;
215
+ actual: string;
216
+ /** 回显用的极短读数(一个数、一个物名、一个距离),与 describeExpect 同一量纲 */
217
+ measured: string;
218
+ /** 这一判是按**这一步的增量**下的(采集):读数与措辞都不是存量口径 */
219
+ gain?: boolean;
220
+ }
221
+
222
+ /**
223
+ * 核验达成与落空均逐步回执;readAt 是该步执行核验的实测时刻。
224
+ * 任务终态可能晚于核验,回执重放原读数,不重读世界。
225
+ */
226
+ export function verdictNote(e: Expectation, v: ExpectVerdict, readAt?: string): string {
227
+ const what = v.gain && 'has' in e ? `这一步进包${zhName(e.has.item)} ≥${e.has.count}` : describeExpect(e);
228
+ return `该步按「${what}」核验:${v.met ? '达成' : '落空'}`
229
+ + `(实测 ${v.measured}${readAt ? `,读于 ${readAt}` : ''})`;
230
+ }
231
+
232
+ /** 动作受阻而目标已满足时,并列报告两项事实;既有存量不能证明本步产出。 */
233
+ export function blockedText(
234
+ call: SkillCall,
235
+ reason: string,
236
+ expect: Expectation | null | undefined,
237
+ verdict: ExpectVerdict | null,
238
+ /** 受阻那一刻手里真正拿着什么;只用来给 use 的头部换主语(见 describeSkill) */
239
+ heldItem?: string | null,
240
+ /** 这份实测是什么时候读的(见 verdictNote) */
241
+ readAt?: string,
242
+ ): string {
243
+ const head = `${describeSkill(call, heldItem)}没做成(${reason})`;
244
+ if (!verdict || !expect) return head;
245
+ if (!verdict.met) return `${head};${verdictNote(expect, verdict, readAt)}`;
246
+ // 紧跟失败原因说明期望是否已满足,并区分已有存量与本步增量。
247
+ if ('has' in expect) {
248
+ if (verdict.gain) {
249
+ return `${head};不过这一趟进包 ${verdict.measured} 个${zhName(expect.has.item)},` +
250
+ `够这一步要的 ${expect.has.count} 个了`;
251
+ }
252
+ return `${head};不过包里现在有 ${verdict.measured} 个${zhName(expect.has.item)},` +
253
+ `已经够这一步要的 ${expect.has.count} 个了 —— 没做成的是这一趟的动作,不是东西不够`;
254
+ }
255
+ return `${head};不过「${describeExpect(expect)}」这个条件现在本来就是满足的`
256
+ + `(实测 ${verdict.measured}${readAt ? `,读于 ${readAt}` : ''})`;
257
+ }
258
+
259
+ /**
260
+ * 这一步是不是卡在「东西」上。判据是受阻/缺口那句话里的固定字样,而这些字样全是
261
+ * 执行器自己写死的措辞:`包里没有X`、`包里凑不齐`、`包里的货不够`、`包里没有任何食物`、
262
+ * `背包 36 格全满了`、`包满了,没处放`,以及核验句里的 `背包内X ≥N`。
263
+ * 走不过去、那一格不是箱子这类非物品受阻不在内。
264
+ */
265
+ export function blockedOnItems(why: string): boolean {
266
+ return ['包里', '包满了', '全满了', '背包内'].some((mark) => why.includes(mark));
267
+ }
268
+
269
+ /** 物品类受阻时附当前全量背包,与快照共用渲染,只报告事实。 */
270
+ export function bagNow(bot: Bot): string {
271
+ const items: ItemStack[] = bot.inventory.items().map((it) => {
272
+ const ench = readEnchants(it as never, bot.registry as never);
273
+ return { name: it.name, count: it.count, ...(ench.length > 0 ? { enchantments: ench } : {}) };
274
+ });
275
+ return `\n[背包] ${items.length > 0 ? narrateInventory(items) : '空的'}`;
276
+ }
277
+
278
+ /** 空位跌到这个数(含)就该知道包快满了 */
279
+ export const BAG_LOW_FREE = 5;
280
+
281
+ /** 立牌/挂牌/墙上牌,十几种木头各一套,统一按后缀认。 */
282
+ export const SIGN_RE = /(^|_)(wall_)?(hanging_)?sign$/;
283
+
284
+ export const SIGN_LINE_MARKS = ['①', '②', '③', '④'];
285
+
286
+ /** 逐行显示牌面文字并注明行数;任务描述与回读共用格式。 */
287
+ export function signLinesText(text: string): string {
288
+ const ls = text.split('\n');
289
+ return `${ls.length} 行:${ls.map((l, i) => `${SIGN_LINE_MARKS[i] ?? `(${i + 1})`}${l === '' ? '(空行)' : l}`).join(' ')}`;
290
+ }
291
+
292
+ /** 耗时的人读写法:不到一秒给一位小数,不到一分钟报秒,再长报「3m20s」 */
293
+ export function fmtDur(ms: number): string {
294
+ if (ms < 1000) return `${(ms / 1000).toFixed(1)}s`;
295
+ const s = Math.round(ms / 1000);
296
+ if (s < 60) return `${s}s`;
297
+ return `${Math.floor(s / 60)}m${s % 60 > 0 ? `${s % 60}s` : ''}`;
298
+ }
299
+
300
+ export function contentsText(items: ItemStack[]): string {
301
+ if (items.length === 0) return '空的';
302
+ return items
303
+ .slice()
304
+ .sort((a, b) => b.count - a.count)
305
+ .map((i) => `${zhName(i.name)}×${i.count}`)
306
+ .join('、');
307
+ }
308
+
309
+ /**
310
+ * 现有的家伙什(含空手)挖了也不掉东西的材质,各自点名原版要哪一级。
311
+ * 试算是"出发前"字面意义上的那一刻,挖掘等级这条事实本该在这里就说清。
312
+ */
313
+ export function noDropMaterials(bot: Bot, reading: RegionReading): string[] {
314
+ const toolTypes: Array<number | null> = [null, ...bot.inventory.items().map((i) => i.type)];
315
+ const out: string[] = [];
316
+ for (const [name, e] of reading.counts) {
317
+ if (LIQUIDS.has(name)) continue;
318
+ if (typeof e.sample.canHarvest !== 'function') continue;
319
+ if (toolTypes.some((t) => e.sample.canHarvest(t))) continue;
320
+ const need = minHarvestTool(bot, name);
321
+ out.push(need ? `${zhName(name)}(要${zhName(need)}及以上)` : zhName(name));
322
+ }
323
+ return out;
324
+ }
325
+
326
+ /** 材质构成一句话:量大在前,矿石带最近坐标 */
327
+ export function compositionText(reading: RegionReading): string {
328
+ const parts = [...reading.counts.entries()]
329
+ .sort((a, b) => b[1].n - a[1].n)
330
+ .slice(0, 10)
331
+ .map(([name, e]) => {
332
+ const spot = name.endsWith('_ore') ? `(最近的在 (${e.nearest.x}, ${e.nearest.y}, ${e.nearest.z}))` : '';
333
+ return `${zhName(name)}×${e.n}${spot}`;
334
+ });
335
+ const rest = reading.counts.size - Math.min(reading.counts.size, 10);
336
+ if (rest > 0) parts.push(`另有 ${rest} 种少量`);
337
+ if (reading.air.length > 0) parts.push(`空气×${reading.air.length}`);
338
+ return parts.length > 0 ? parts.join('、') : '什么都没有';
339
+ }
340
+