@thincoder/core 0.10.4 → 0.10.6

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 (153) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/advisor/compaction.mjs +11 -6
  4. package/advisor/run.mjs +4 -4
  5. package/agent/assemble.mjs +1 -1
  6. package/agent/chat-call.mjs +1 -1
  7. package/agent/child-marks.mjs +2 -2
  8. package/agent/digest-account.mjs +5 -4
  9. package/agent/dispatch-gates.mjs +8 -0
  10. package/agent/dispatch.mjs +39 -18
  11. package/agent/family-tools.mjs +22 -11
  12. package/agent/helpers.mjs +4 -3
  13. package/agent/light-round.mjs +66 -0
  14. package/agent/post-turn.mjs +8 -4
  15. package/agent/run-stages.mjs +5 -3
  16. package/agent/run-start.mjs +3 -2
  17. package/agent/setup-reminders.mjs +1 -1
  18. package/agent/setup.mjs +18 -3
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +8 -5
  21. package/agent/timers.mjs +56 -5
  22. package/agent/turn-loop.mjs +5 -3
  23. package/agent/write-gate.mjs +7 -7
  24. package/agent-tools/advisor-async.mjs +2 -2
  25. package/agent-tools/advisor-settle.mjs +2 -2
  26. package/agent-tools/advisor.mjs +1 -1
  27. package/agent-tools/async-discard.mjs +34 -0
  28. package/agent-tools/async-settle.mjs +3 -2
  29. package/agent-tools/audit-block.mjs +1 -1
  30. package/agent-tools/bash-async.mjs +1 -1
  31. package/agent-tools/batch-lifecycle.mjs +35 -14
  32. package/agent-tools/batch-paths.mjs +4 -4
  33. package/agent-tools/batch-skeleton.mjs +29 -11
  34. package/agent-tools/batch.mjs +28 -15
  35. package/agent-tools/browser-async.mjs +198 -0
  36. package/agent-tools/checkpoint.mjs +1 -1
  37. package/agent-tools/design-slots.mjs +232 -0
  38. package/agent-tools/digest-budget.mjs +1 -1
  39. package/agent-tools/manifest.mjs +193 -0
  40. package/agent-tools/parent-channel.mjs +7 -4
  41. package/agent-tools/review-facts.mjs +13 -13
  42. package/agent-tools/settings.mjs +1 -1
  43. package/agent-tools/skill.mjs +1 -1
  44. package/agent-tools/subagent-async.mjs +21 -12
  45. package/agent-tools/subagent-scheduler.mjs +8 -5
  46. package/agent-tools/subagent-spawn.mjs +29 -55
  47. package/agent-tools/subagent.mjs +21 -8
  48. package/agent-tools/timer.mjs +186 -12
  49. package/agent-tools/zero-write-watch.mjs +2 -2
  50. package/agent-tools.mjs +2 -1
  51. package/agent.mjs +1 -1
  52. package/attachments.mjs +1 -1
  53. package/browser/actions.mjs +163 -0
  54. package/browser/cdp.mjs +161 -0
  55. package/browser/clipboard.mjs +142 -0
  56. package/browser/input-actions.mjs +197 -0
  57. package/browser/input.mjs +256 -0
  58. package/browser/launch.mjs +143 -0
  59. package/browser/queue.mjs +171 -0
  60. package/browser/session.mjs +494 -0
  61. package/browser/snapshot.mjs +270 -0
  62. package/config-io.mjs +18 -13
  63. package/config-migrate.mjs +13 -29
  64. package/config-presets.mjs +33 -29
  65. package/config.mjs +68 -25
  66. package/context-degrade.mjs +1 -1
  67. package/context-echo.mjs +1 -1
  68. package/context-push.mjs +2 -2
  69. package/context.mjs +1 -1
  70. package/conventions.mjs +2 -2
  71. package/declaration.mjs +18 -2
  72. package/file-links.mjs +1 -1
  73. package/file-refs.mjs +1 -1
  74. package/history-window.mjs +1 -1
  75. package/i18n.mjs +2 -0
  76. package/ledger-cmd.mjs +62 -16
  77. package/ledger-db.mjs +6 -0
  78. package/ledger-surface.mjs +49 -5
  79. package/ledger-tools.mjs +16 -8
  80. package/ledger.mjs +9 -16
  81. package/log.mjs +1 -1
  82. package/manifest-discovery.mjs +1 -1
  83. package/manifest-schema.mjs +1 -1
  84. package/mcp/transport-stdio.mjs +1 -1
  85. package/mcp.mjs +1 -1
  86. package/memory/code-index.mjs +1 -1
  87. package/memory/code-search.mjs +1 -1
  88. package/memory/code-sync.mjs +1 -1
  89. package/memory/docs.mjs +6 -2
  90. package/memory/file-list.mjs +1 -1
  91. package/memory/file-walk.mjs +17 -5
  92. package/memory/maintain.mjs +410 -0
  93. package/memory/memory-tool.mjs +21 -6
  94. package/memory/schema.mjs +19 -7
  95. package/memory/sweep.mjs +8 -5
  96. package/memory/sync-tail.mjs +16 -10
  97. package/model-ref.mjs +16 -13
  98. package/model-specs.mjs +21 -9
  99. package/package.json +2 -1
  100. package/peer-instances.mjs +1 -1
  101. package/permission.mjs +33 -0
  102. package/process-probe.mjs +2 -2
  103. package/prompts/advisor-design.md +4 -2
  104. package/prompts/advisor-round1.md +1 -1
  105. package/prompts/advisor-round2.md +1 -1
  106. package/prompts/advisor-round3.md +1 -1
  107. package/prompts/common.md +36 -2
  108. package/prompts/consult-base.md +1 -1
  109. package/prompts/discipline-engineering.md +32 -15
  110. package/prompts/discipline-normal.md +46 -2
  111. package/prompts/persona-eng-designer.md +6 -6
  112. package/prompts/persona-engineering.md +14 -6
  113. package/prompts/persona-normal.md +2 -0
  114. package/prompts/subagent-base.md +3 -0
  115. package/provider/core.mjs +6 -0
  116. package/provider/errors.mjs +5 -3
  117. package/provider/google.mjs +9 -8
  118. package/provider/list-models.mjs +3 -2
  119. package/provider/sse.mjs +3 -0
  120. package/provider-flows.mjs +14 -15
  121. package/proxy-chunked.mjs +102 -0
  122. package/proxy-target.mjs +54 -0
  123. package/proxy-transport.mjs +298 -0
  124. package/proxy.mjs +17 -248
  125. package/session-gc.mjs +2 -2
  126. package/session-lifecycle.mjs +18 -14
  127. package/session-slot-claims.mjs +2 -2
  128. package/session-slots-manifest.mjs +1 -1
  129. package/session-slots.mjs +3 -3
  130. package/team-ledger.mjs +282 -0
  131. package/team.mjs +223 -0
  132. package/tool-docs/bash.md +1 -0
  133. package/tool-docs/batch.md +1 -1
  134. package/tool-docs/browser.md +48 -0
  135. package/tool-docs/ledger.md +3 -3
  136. package/tool-docs/manifest.md +16 -0
  137. package/tool-docs/memory.md +1 -0
  138. package/tool-docs/notify_parent.md +5 -0
  139. package/tool-docs/process.md +4 -4
  140. package/tool-docs/question.md +1 -0
  141. package/tool-docs/subagent.md +4 -3
  142. package/tool-docs/team_ledger.md +9 -0
  143. package/tool-docs/timer.md +6 -0
  144. package/tool-docs/wait_for.md +1 -0
  145. package/tools/browser.mjs +161 -0
  146. package/tools/git.mjs +1 -1
  147. package/tools/index.mjs +3 -0
  148. package/tools/ops.mjs +26 -10
  149. package/tools/repomap.mjs +1 -1
  150. package/tools/write-path.mjs +2 -2
  151. package/traces/trace-cleanup.mjs +1 -1
  152. package/traces/trace-store.mjs +1 -1
  153. package/vision-reader.mjs +10 -16
@@ -0,0 +1,256 @@
1
+ /**
2
+ * browser/input.mjs — 输入引擎(纯函数):键表 ∥ 修饰 / 移位解析 ∥ 序列构造器(键 ∥ 鼠标 ∥ 滚轮 ∥ 拖拽 ∥ 触屏)。
3
+ * 设计权威 = `docs/core/design/BROWSER-TOOL.md` §2.8(键面)∥ §2.7(坐标语义)∥ §6 KD-14 / KD-16;
4
+ * 本档零会话依赖(DAG 叶面——动作模块 import 本档,本档不 import 动作 / 会话 / 传输)。
5
+ *
6
+ * 序列形沿 CDP `Input` 域实证口径(对照 Puppeteer `cdp/Input.ts`):
7
+ * - 有文本产出的键 ⇒ `keyDown` + `text`;无 ⇒ `rawKeyDown`(协议注记:Control + A 一类不发文本);
8
+ * - 任一非 Shift 修饰在位 ⇒ 不发文本(`modifiers & ~8` 口径);
9
+ * - 修饰键按下 / 抬起逐事件演进 `modifiers` 位(按下先置位、抬起先清位——修饰态可被页面观察)。
10
+ * 坐标一律视口 CSS 像素(§2.7)。
11
+ */
12
+
13
+ /** 修饰位(CDP `modifiers` 位域)。 */
14
+ export const MODIFIER_BITS = { Alt: 1, Control: 2, Meta: 4, Shift: 8 }
15
+ export const MODIFIER_ORDER = ["Alt", "Control", "Meta", "Shift"]
16
+
17
+ /** 修饰键本体(按下 / 抬起事件面)。 */
18
+ const MODIFIER_SLOTS = {
19
+ Alt: { key: "Alt", code: "AltLeft", vk: 18, loc: 1 },
20
+ Control: { key: "Control", code: "ControlLeft", vk: 17, loc: 1 },
21
+ Meta: { key: "Meta", code: "MetaLeft", vk: 91, loc: 1 },
22
+ Shift: { key: "Shift", code: "ShiftLeft", vk: 16, loc: 1 },
23
+ }
24
+
25
+ const FN_KEYS = {}
26
+ for (let i = 1; i <= 12; i++) FN_KEYS[`F${i}`] = { key: `F${i}`, code: `F${i}`, vk: 111 + i }
27
+
28
+ /** 命名键表(§2.8):`{ key, code, vk, text?, loc? }`——值 = US 布局传统键码。 */
29
+ export const NAMED_KEYS = {
30
+ Enter: { key: "Enter", code: "Enter", vk: 13, text: "\r" },
31
+ Escape: { key: "Escape", code: "Escape", vk: 27 },
32
+ Tab: { key: "Tab", code: "Tab", vk: 9, text: "\t" },
33
+ Backspace: { key: "Backspace", code: "Backspace", vk: 8 },
34
+ Delete: { key: "Delete", code: "Delete", vk: 46 },
35
+ Space: { key: " ", code: "Space", vk: 32, text: " " },
36
+ Insert: { key: "Insert", code: "Insert", vk: 45 },
37
+ Home: { key: "Home", code: "Home", vk: 36 },
38
+ End: { key: "End", code: "End", vk: 35 },
39
+ PageUp: { key: "PageUp", code: "PageUp", vk: 33 },
40
+ PageDown: { key: "PageDown", code: "PageDown", vk: 34 },
41
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", vk: 38 },
42
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", vk: 40 },
43
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", vk: 37 },
44
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", vk: 39 },
45
+ ...FN_KEYS,
46
+ ...Object.fromEntries(Object.entries(MODIFIER_SLOTS).map(([name, slot]) => [name, { key: slot.key, code: slot.code, vk: slot.vk, loc: slot.loc }])),
47
+ }
48
+
49
+ /** 单字符行表(US 布局):`[基键, 移位键, code, vk]`(`1`→`!`、`-`→`_`……)。 */
50
+ const CHAR_ROWS = [
51
+ ["1", "!", "Digit1", 49], ["2", "@", "Digit2", 50], ["3", "#", "Digit3", 51], ["4", "$", "Digit4", 52],
52
+ ["5", "%", "Digit5", 53], ["6", "^", "Digit6", 54], ["7", "&", "Digit7", 55], ["8", "*", "Digit8", 56],
53
+ ["9", "(", "Digit9", 57], ["0", ")", "Digit0", 48],
54
+ ["-", "_", "Minus", 189], ["=", "+", "Equal", 187],
55
+ ["[", "{", "BracketLeft", 219], ["]", "}", "BracketRight", 221], ["\\", "|", "Backslash", 220],
56
+ [";", ":", "Semicolon", 186], ["'", "\"", "Quote", 222], ["`", "~", "Backquote", 192],
57
+ [",", "<", "Comma", 188], [".", ">", "Period", 190], ["/", "?", "Slash", 191],
58
+ ]
59
+
60
+ /** 单字符 → 键面描述(`shift` = 该字符本身需 Shift;`alt` = 显式 Shift 时的产出形)。 */
61
+ function charSpec(ch) {
62
+ if (ch === " ") return { key: " ", text: " ", code: "Space", vk: 32, shift: false, alt: null }
63
+ if (/^[a-z]$/.test(ch)) {
64
+ const up = ch.toUpperCase()
65
+ return { key: ch, text: ch, code: `Key${up}`, vk: up.charCodeAt(0), shift: false, alt: { key: up, text: up } }
66
+ }
67
+ if (/^[A-Z]$/.test(ch)) return { key: ch, text: ch, code: `Key${ch}`, vk: ch.charCodeAt(0), shift: true, alt: null }
68
+ for (const [base, shifted, code, vk] of CHAR_ROWS) {
69
+ if (ch === base) return { key: ch, text: ch, code, vk, shift: false, alt: { key: shifted, text: shifted } }
70
+ if (ch === shifted) return { key: ch, text: ch, code, vk, shift: true, alt: null }
71
+ }
72
+ return null
73
+ }
74
+
75
+ /** 键解析(§2.8):命名键 ∥ 单字符(US 布局);未列 ⇒ 抛 `unknown key "<k>" — keys: …`(不执行)。 */
76
+ export function resolveKey(key) {
77
+ const raw = String(key ?? "")
78
+ const named = NAMED_KEYS[raw]
79
+ if (named) return { ...named, shift: false, alt: null }
80
+ if (raw.length === 1) {
81
+ const spec = charSpec(raw)
82
+ if (spec) return spec
83
+ }
84
+ throw new Error(`unknown key "${raw}" — keys: ${Object.keys(NAMED_KEYS).join(", ")} — or a single US-layout character (a-z, 0-9, punctuation)`)
85
+ }
86
+
87
+ /** 修饰数组归一(大小写不敏感 → 固定序 Alt/Control/Meta/Shift);未列名 ⇒ 拒(不静默发裸键)。 */
88
+ export function normalizeModifiers(list) {
89
+ const given = Array.isArray(list) ? list : []
90
+ const out = []
91
+ for (const raw of given) {
92
+ const name = MODIFIER_ORDER.find((m) => m.toLowerCase() === String(raw).toLowerCase())
93
+ if (!name) throw new Error(`unknown modifier "${raw}" — modifiers: ${MODIFIER_ORDER.join(", ")}`)
94
+ if (!out.includes(name)) out.push(name)
95
+ }
96
+ return MODIFIER_ORDER.filter((m) => out.includes(m))
97
+ }
98
+
99
+ function keyDownParams(slot, bits, { text = null, autoRepeat = false } = {}) {
100
+ const params = { type: text ? "keyDown" : "rawKeyDown", modifiers: bits, windowsVirtualKeyCode: slot.vk, code: slot.code, key: slot.key }
101
+ if (slot.loc) params.location = slot.loc
102
+ if (text) { params.text = text; params.unmodifiedText = text }
103
+ if (autoRepeat) params.autoRepeat = true
104
+ return { method: "Input.dispatchKeyEvent", params }
105
+ }
106
+
107
+ function keyUpParams(slot, bits) {
108
+ const params = { type: "keyUp", modifiers: bits, windowsVirtualKeyCode: slot.vk, code: slot.code, key: slot.key }
109
+ if (slot.loc) params.location = slot.loc
110
+ return { method: "Input.dispatchKeyEvent", params }
111
+ }
112
+
113
+ /**
114
+ * 按键序列(§2.8):修饰键按下 → 主键(+`autoRepeat`×repeat)→ 主键抬起 → 修饰键抬起。
115
+ * `phase` = down(只按)∥ up(只抬)∥ press(默认 = 按 + 抬);`repeat` 0–100(越界夹取)。
116
+ * @returns {{combo: string, commands: Array<{method: string, params: object}>}}
117
+ */
118
+ export function pressSequence({ key, modifiers = [], phase = "press", repeat = 0 } = {}) {
119
+ const spec = resolveKey(key)
120
+ const mods = normalizeModifiers(modifiers)
121
+ const wanted = Number(repeat)
122
+ const repeatCount = Math.max(0, Math.min(Number.isFinite(wanted) ? Math.floor(wanted) : 0, 100))
123
+ const autoShift = spec.shift && !mods.includes("Shift") // 大写字母 / 移位符号 ⇒ 自动置 Shift 位(§2.8)
124
+ const shift = spec.shift || mods.includes("Shift")
125
+ const main = (shift && spec.alt ? spec.alt : { key: spec.key, text: spec.text })
126
+ const mainSlot = { key: main.key, code: spec.code, vk: spec.vk, loc: spec.loc }
127
+ const text = mods.some((m) => m !== "Shift") ? null : (main.text ?? null) // 非 Shift 修饰 ⇒ 不发文本
128
+ const bitsAll = mods.reduce((acc, m) => acc | MODIFIER_BITS[m], 0) | (autoShift ? MODIFIER_BITS.Shift : 0)
129
+ const commands = []
130
+ if (phase !== "up") {
131
+ let bits = 0
132
+ for (const name of mods) {
133
+ bits |= MODIFIER_BITS[name]
134
+ commands.push(keyDownParams(MODIFIER_SLOTS[name], bits))
135
+ }
136
+ for (let i = 0; i <= repeatCount; i++) commands.push(keyDownParams(mainSlot, bitsAll, { text, autoRepeat: i > 0 }))
137
+ }
138
+ if (phase !== "down") {
139
+ commands.push(keyUpParams(mainSlot, bitsAll))
140
+ let bits = bitsAll
141
+ for (const name of [...mods].reverse()) {
142
+ bits &= ~MODIFIER_BITS[name]
143
+ commands.push(keyUpParams(MODIFIER_SLOTS[name], bits))
144
+ }
145
+ }
146
+ return { combo: [...mods, spec.key].join("+"), repeat: repeatCount, commands }
147
+ }
148
+
149
+ /** 鼠标按钮位(CDP `buttons` 位域)——键序 = 报错枚举序。 */
150
+ export const MOUSE_BUTTONS = { left: 1, middle: 4, right: 2, back: 8, forward: 16 }
151
+
152
+ function mouseEvent(type, x, y, extra = {}) {
153
+ return { method: "Input.dispatchMouseEvent", params: { type, x, y, ...extra } }
154
+ }
155
+
156
+ /** 指针移动序列(hover——仅 `mouseMoved`)。 */
157
+ export function moveSequence({ x, y } = {}) {
158
+ return [mouseEvent("mouseMoved", x, y, { button: "none", buttons: 0 })]
159
+ }
160
+
161
+ /** 鼠标序列(§2.2 mouse):click = 移动 → 按下 → 抬起(double ⇒ clickCount 1、2);down / up = 单边。 */
162
+ export function mouseSequence({ x, y, button = "left", phase = "click", double = false } = {}) {
163
+ const flag = MOUSE_BUTTONS[button]
164
+ if (!flag) throw new Error(`unknown mouse button "${button}" — buttons: ${Object.keys(MOUSE_BUTTONS).join(", ")}`)
165
+ const shape = phase === "down" || phase === "up" ? phase : "click"
166
+ const commands = []
167
+ if (shape !== "up") commands.push(mouseEvent("mouseMoved", x, y, { button: "none", buttons: 0 }))
168
+ if (shape === "down") {
169
+ commands.push(mouseEvent("mousePressed", x, y, { button, buttons: flag, clickCount: 1 }))
170
+ } else if (shape === "up") {
171
+ commands.push(mouseEvent("mouseReleased", x, y, { button, buttons: 0, clickCount: 1 }))
172
+ } else {
173
+ const times = double ? 2 : 1
174
+ for (let i = 1; i <= times; i++) {
175
+ commands.push(mouseEvent("mousePressed", x, y, { button, buttons: flag, clickCount: i }))
176
+ commands.push(mouseEvent("mouseReleased", x, y, { button, buttons: 0, clickCount: i }))
177
+ }
178
+ }
179
+ return commands
180
+ }
181
+
182
+ /** 滚轮序列(§2.2 wheel):落点处一发 `mouseWheel`(`deltaX` / `deltaY` 至少一非零——校验在工具面)。 */
183
+ export function wheelSequence({ x, y, deltaX = 0, deltaY = 0 } = {}) {
184
+ return [mouseEvent("mouseWheel", x, y, { button: "none", buttons: 0, pointerType: "mouse", deltaX, deltaY })]
185
+ }
186
+
187
+ /** 中间步数夹取(§2.2:默认 10 · 2–50)。 */
188
+ export function clampSteps(value, fallback = 10) {
189
+ const n = Number(value)
190
+ const base = Number.isFinite(n) ? Math.floor(n) : fallback
191
+ return Math.max(2, Math.min(base, 50))
192
+ }
193
+
194
+ /** 拖拽序列(KD-16 默认形——鼠标序列):按下 → 移动×steps(末步落于 `to`)→ 抬起(`release:false` ⇒ 只按不放——html5 拦截链用)。 */
195
+ export function dragSequence({ from, to, steps = 10, release = true } = {}) {
196
+ const n = clampSteps(steps)
197
+ const commands = [mouseEvent("mouseMoved", from.x, from.y, { button: "none", buttons: 0 })]
198
+ commands.push(mouseEvent("mousePressed", from.x, from.y, { button: "left", buttons: MOUSE_BUTTONS.left, clickCount: 1 }))
199
+ for (let i = 1; i <= n; i++) {
200
+ const x = from.x + (to.x - from.x) * (i / n)
201
+ const y = from.y + (to.y - from.y) * (i / n)
202
+ commands.push(mouseEvent("mouseMoved", x, y, { button: "left", buttons: MOUSE_BUTTONS.left }))
203
+ }
204
+ if (release) commands.push(mouseEvent("mouseReleased", to.x, to.y, { button: "left", buttons: 0, clickCount: 1 }))
205
+ return commands
206
+ }
207
+
208
+ /** 触屏手势面(§2.2 touch gesture——工具面同表校验)。 */
209
+ export const TOUCH_GESTURES = ["tap", "doubleTap", "swipe", "pinch"]
210
+ /** doubleTap 两次序列之间的间隔(同一双击窗内——双击计数仍由浏览器手势引擎管)。 */
211
+ export const TOUCH_TAP_GAP_MS = 60
212
+
213
+ /** 触屏点形(CDP TouchPoint——坐标取整)。 */
214
+ function touchPoint(px, py) {
215
+ return { x: Math.round(px), y: Math.round(py), radiusX: 0.5, radiusY: 0.5, force: 0.5, id: 1 }
216
+ }
217
+
218
+ /** 单次打字序列(tap):touchStart → touchEnd(同点同 id)——全链 click 实证(KD-14 收正)。 */
219
+ function touchTapCommands(x, y) {
220
+ return [
221
+ { method: "Input.dispatchTouchEvent", params: { type: "touchStart", touchPoints: [touchPoint(x, y)] } },
222
+ { method: "Input.dispatchTouchEvent", params: { type: "touchEnd", touchPoints: [touchPoint(x, y)] } },
223
+ ]
224
+ }
225
+
226
+ /**
227
+ * 触屏序列(KD-14 实证收正):tap / doubleTap 走 `dispatchTouchEvent` 显式序列(全链 mousedown/mouseup/click
228
+ * 实证;synthesizeTapGesture 实测不产 click);pinch 走 `synthesizePinchGesture`(实测有效);
229
+ * swipe 走显式路径(手指路径语义——from → to 插值 ×steps)。
230
+ * doubleTap = 同序列×2 + `TOUCH_TAP_GAP_MS` 间隔(时序在动作层控——双击计数由浏览器管)。
231
+ */
232
+ export function touchSequence({ gesture = "tap", x, y, from, to, steps = 10, scale } = {}) {
233
+ if (gesture === "tap" || gesture === "doubleTap") return touchTapCommands(x, y)
234
+ if (gesture === "pinch") {
235
+ return [{ method: "Input.synthesizePinchGesture", params: { x, y, scaleFactor: scale } }]
236
+ }
237
+ if (!TOUCH_GESTURES.includes(gesture)) throw new Error(`unknown touch gesture "${gesture}" — gestures: ${TOUCH_GESTURES.join(", ")}`)
238
+ const n = clampSteps(steps)
239
+ const commands = [{ method: "Input.dispatchTouchEvent", params: { type: "touchStart", touchPoints: [touchPoint(from.x, from.y)] } }]
240
+ for (let i = 1; i <= n; i++) {
241
+ const px = from.x + (to.x - from.x) * (i / n)
242
+ const py = from.y + (to.y - from.y) * (i / n)
243
+ commands.push({ method: "Input.dispatchTouchEvent", params: { type: "touchMove", touchPoints: [touchPoint(px, py)] } })
244
+ }
245
+ commands.push({ method: "Input.dispatchTouchEvent", params: { type: "touchEnd", touchPoints: [touchPoint(to.x, to.y)] } })
246
+ return commands
247
+ }
248
+
249
+ /** 目标形解析(§2.2:`e<N>` ∥ `"x,y"`);不符 ⇒ null(调用面拒——`must be an element ref (e<N>) or "x,y"`)。 */
250
+ export function parseTargetSpec(spec) {
251
+ const raw = String(spec ?? "").trim()
252
+ if (/^e\d+$/.test(raw)) return { kind: "ref", ref: raw }
253
+ const m = /^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/.exec(raw)
254
+ if (m) return { kind: "point", x: Number(m[1]), y: Number(m[2]) }
255
+ return null
256
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * browser/launch.mjs — 浏览器发现 ∥ profile 锁 ∥ 启动 ∥ 杀树(BROWSER-TOOL.md §4)。
3
+ *
4
+ * 候选表与启动参数沿 `scripts/console-walkthrough.mjs:33-44`(Edge 先于 Chrome)∥ `:123`
5
+ * (`--headless=new` ∥ `--remote-debugging-port=0` ∥ `stdio:"ignore"` ∥ POSIX `detached`)——
6
+ * 本档不 import 该脚本,只内化其形。
7
+ * profile = `~/.thincoder/browser/profile`(`configDir` 同源——`config-io.mjs:32`)= **持久**
8
+ * (F-BT6:有头登录一次 ⇒ 后续含无头复用登录态)。
9
+ *
10
+ * 注入缝(N-BT6):`resolveBrowser({platform, env, pathExists, which})` 与 `acquireProfileLock`
11
+ * 的 `io`——缺省回落真实现,单元测试零真实文件系统。
12
+ */
13
+ import { execFileSync, spawn } from "node:child_process"
14
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
15
+ import { dirname, join } from "node:path"
16
+ import { configDir } from "../config-io.mjs"
17
+
18
+ /** 平台候选表(edge 先于 chrome——walkthrough 实证表)。 */
19
+ export const CANDIDATES = {
20
+ edge: {
21
+ win32: ["C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"],
22
+ darwin: ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
23
+ linux: ["microsoft-edge", "microsoft-edge-stable"],
24
+ },
25
+ chrome: {
26
+ win32: ["C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"],
27
+ darwin: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
28
+ linux: ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"],
29
+ },
30
+ }
31
+
32
+ /** 全候选落空(N-BT5)——装配期不剔除工具,首用显式报此句(§2.6)。 */
33
+ export const BROWSER_NOT_FOUND = "no Chromium-based browser found (Edge/Chrome) — install one or set BROWSER_PATH"
34
+ export const START_TIMEOUT_MS = 12_000
35
+
36
+ const defaultPathExists = (p) => existsSync(p)
37
+ function defaultWhich(name) {
38
+ try { return execFileSync("which", [name], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim() || null }
39
+ catch { return null }
40
+ }
41
+
42
+ /** 浏览器可执行文件解析:`BROWSER_PATH`(exe 路径 ∥ 名字)→ 候选表 edge → chrome;全空 ⇒ 抛钉死句。 */
43
+ export function resolveBrowser({ platform = process.platform, env = process.env, pathExists = defaultPathExists, which = defaultWhich } = {}) {
44
+ const lookup = (candidate) => (candidate.includes("/") || candidate.includes("\\")
45
+ ? (pathExists(candidate) ? candidate : null)
46
+ : which(candidate))
47
+ const override = String(env?.BROWSER_PATH ?? "").trim()
48
+ if (override) {
49
+ const byName = CANDIDATES[override.toLowerCase()]
50
+ for (const candidate of byName ? byName[platform] ?? [] : [override]) {
51
+ const found = lookup(candidate)
52
+ if (found) return found
53
+ }
54
+ // 覆盖值不可用 ⇒ 落回候选表(不静默吞:表亦空时抛钉死句——「设置 BROWSER_PATH」指引仍成立)
55
+ }
56
+ for (const family of Object.values(CANDIDATES)) {
57
+ for (const candidate of family[platform] ?? []) {
58
+ const found = lookup(candidate)
59
+ if (found) return found
60
+ }
61
+ }
62
+ throw new Error(BROWSER_NOT_FOUND)
63
+ }
64
+
65
+ /** 本工具的持久化面(profile ∥ 锁 ∥ 截图目录)。 */
66
+ export function browserPaths(base = configDir) {
67
+ const root = join(base, "browser")
68
+ return { root, profile: join(root, "profile"), lock: join(root, "session.lock"), shots: join(root, "shots") }
69
+ }
70
+
71
+ function defaultPidAlive(pid) {
72
+ try { process.kill(pid, 0); return true }
73
+ catch (e) { return e?.code === "EPERM" } // 存在但无权限 ⇒ 视为活着(保守)
74
+ }
75
+
76
+ /**
77
+ * profile 锁(单会话/进程——KD-5):他进程持有 ⇒ 抛 `browser profile in use by another
78
+ * ThinCoder instance (pid <N>)`;陈旧锁(pid 死)⇒ 接管。
79
+ * `io` = 注入缝(readText ∥ writeText ∥ isAlive ∥ pid)——缺省真文件系统。
80
+ */
81
+ export function acquireProfileLock(lockPath, io = {}) {
82
+ const readText = io.readText ?? ((p) => { try { return readFileSync(p, "utf8") } catch { return "" } })
83
+ const writeText = io.writeText ?? ((p, text) => { mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, text, "utf8") })
84
+ const isAlive = io.isAlive ?? defaultPidAlive
85
+ const pid = io.pid ?? process.pid
86
+ const held = Number(String(readText(lockPath) ?? "").trim())
87
+ if (Number.isInteger(held) && held > 0 && held !== pid && isAlive(held)) {
88
+ throw new Error(`browser profile in use by another ThinCoder instance (pid ${held})`)
89
+ }
90
+ writeText(lockPath, String(pid))
91
+ return { pid }
92
+ }
93
+
94
+ export function releaseProfileLock(lockPath, io = {}) {
95
+ const removeText = io.removeText ?? ((p) => { try { rmSync(p, { force: true }) } catch { /* 已被接管/清除 */ } })
96
+ removeText(lockPath)
97
+ }
98
+
99
+ /**
100
+ * 启动浏览器(spawn + `DevToolsActivePort` 轮询 ≤12s)。
101
+ * 持久 profile ⇒ 启动前清陈旧端口文件(否则旧值被当本次读数——发现即失效)。
102
+ * @returns {Promise<{child: import("node:child_process").ChildProcess, port: number, profile: string}>}
103
+ */
104
+ export async function launchBrowser({ exe, headless = true, profile, timeoutMs = START_TIMEOUT_MS, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
105
+ const args = [
106
+ ...(headless ? ["--headless=new"] : []),
107
+ "--disable-gpu", "--no-first-run", "--hide-scrollbars",
108
+ `--user-data-dir=${profile}`, "--remote-debugging-port=0",
109
+ "--window-size=1440,900", "about:blank",
110
+ ]
111
+ mkdirSync(profile, { recursive: true })
112
+ const portFile = join(profile, "DevToolsActivePort")
113
+ rmSync(portFile, { force: true })
114
+ const child = spawn(exe, args, { stdio: "ignore", detached: process.platform !== "win32" })
115
+ let spawnError = null
116
+ child.on("error", (e) => { spawnError = e }) // 捕获(否则 unhandled 'error' 事件崩进程);下面显式上报
117
+ const deadline = Date.now() + timeoutMs
118
+ while (!existsSync(portFile)) {
119
+ if (spawnError) { killBrowser(child); throw new Error(`browser failed to start: ${spawnError.message} (${exe})`) }
120
+ if (child.exitCode !== null || Date.now() >= deadline) {
121
+ killBrowser(child)
122
+ throw new Error(`browser failed to start: DevToolsActivePort not written within ${Math.round(timeoutMs / 1000)}s (${exe})`)
123
+ }
124
+ await sleep(50)
125
+ }
126
+ const port = Number(String(readFileSync(portFile, "utf8")).split("\n")[0])
127
+ if (!Number.isInteger(port) || port <= 0) {
128
+ killBrowser(child)
129
+ throw new Error(`browser failed to start: DevToolsActivePort is malformed (${exe})`)
130
+ }
131
+ return { child, port, profile }
132
+ }
133
+
134
+ /** 杀整棵进程树(win32 `taskkill /T /F`;POSIX 组杀 + 单杀兜底——walkthrough `:192-193` 同式)。 */
135
+ export function killBrowser(child, { platform = process.platform, exec = execFileSync } = {}) {
136
+ if (!child || typeof child.pid !== "number") return
137
+ if (platform === "win32") {
138
+ try { exec("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" }) } catch { /* 已退出 */ }
139
+ return
140
+ }
141
+ try { process.kill(-child.pid, "SIGKILL") }
142
+ catch { try { child.kill("SIGKILL") } catch { /* 已退出 */ } }
143
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * browser/queue.mjs — 调度策略:串行队列 ∥ 动作预算表 ∥ 动作控制器(超时 / 取消单点)∥ 步名跟踪
3
+ * (设计权威 = `docs/core/design/BROWSER-TOOL.md` §2.9——单时钟动作预算 + 三层帽;KD-18 ∥ KD-19)。
4
+ *
5
+ * 单时钟:预算 deadline 自**入队**起算(队列等待计入预算——有意);到点 ⇒ 控制器 `abort`(超时因由)。
6
+ * 排队期到点 ⇒ 动作从未起执行(回执步名 `queue wait`);动作内到点 ⇒ 步名 = 在飞阶段 / CDP 方法名。
7
+ * 控制器(超时 ∥ 取消共用——KD-19):abort 后在飞操作以同一因由拒绝,其后一切调用**快速失败**
8
+ * (清理路径不得再起命令——防僵尸命令与下一动作交错)。
9
+ * 步名:`ctl.run(name, fn)` 压帧(栈顶 = `stuck in <步名>` 的读数源);`h.call` 帧 = CDP 方法名(session 侧接线)。
10
+ */
11
+ import { WAIT_TIMEOUT_DEFAULT, WAIT_TIMEOUT_MAX } from "./actions.mjs"
12
+
13
+ /** 动作预算表(§2.9——机检):动作 ⇒ 预算 ms。wait 无固定值(谓词帽 + 余量——`budgetFor` 解算)。 */
14
+ export const ACTION_BUDGETS = {
15
+ navigate: 45_000, // 含会话自启(12s 启动帽内)+ 导航 + 就绪轮询 + 快照
16
+ click: 30_000,
17
+ evaluate: 30_000,
18
+ screenshot: 30_000,
19
+ close: 30_000,
20
+ press: 30_000,
21
+ hover: 30_000,
22
+ wheel: 30_000,
23
+ mouse: 30_000,
24
+ drag: 30_000,
25
+ touch: 30_000,
26
+ insert: 30_000,
27
+ clipboard: 30_000,
28
+ type: 15_000, // 单页内快动作
29
+ snapshot: 15_000,
30
+ wait: 45_000, // = 谓词帽缺省 30s + 15s 余量(timeoutMs 给出时按帽 + 余量重解——§2.9 表)
31
+ }
32
+ export const BUDGET_FALLBACK_MS = 30_000 // 未在册动作的兜底预算(防御——全部在册动作已列)
33
+ export const BUDGET_MAX_MS = 120_000 // `timeoutMs` override 硬上限(§2.9)
34
+ export const WAIT_MARGIN_MS = 15_000 // wait 预算余量(谓词帽之上——§2.9 表)
35
+ export const QUEUE_WAIT_STEP = "queue wait"
36
+
37
+ /** 预算解算(§2.9):`timeoutMs` = 动作预算 override(硬上限 120s);wait = 谓词帽 + 15s 余量(语义零变)。 */
38
+ export function budgetFor(action, args = {}) {
39
+ const wanted = Number(args?.timeoutMs)
40
+ const given = Number.isFinite(wanted) && wanted > 0
41
+ if (action === "wait") {
42
+ return (given ? Math.min(wanted, WAIT_TIMEOUT_MAX) : WAIT_TIMEOUT_DEFAULT) + WAIT_MARGIN_MS
43
+ }
44
+ if (given) return Math.min(wanted, BUDGET_MAX_MS)
45
+ return ACTION_BUDGETS[action] ?? BUDGET_FALLBACK_MS
46
+ }
47
+
48
+ /** 单动作控制器:超时 / 取消 / 会话丢失共用一个中止点(一个机制多个因由——KD-19)。 */
49
+ export class ActionController {
50
+ constructor({ action, budgetMs }) {
51
+ this.action = action
52
+ this.budgetMs = budgetMs
53
+ this.aborted = false
54
+ this.reason = null
55
+ this.error = null
56
+ this._deadline = Date.now() + budgetMs
57
+ this._steps = []
58
+ this._abortHooks = []
59
+ this._abortP = new Promise((resolve) => { this._resolveAbort = resolve })
60
+ this._timer = setTimeout(() => this.abort("timeout"), budgetMs)
61
+ }
62
+
63
+ /** 剩余预算(ms)——动作内调用帽的下界参照(§2.9 层一:动作内调用随控制器)。 */
64
+ remaining() {
65
+ return Math.max(1, this._deadline - Date.now())
66
+ }
67
+
68
+ /** 快速失败(§2.9):abort 之后一切后续调用即刻拒绝(清理路径不得再起命令)。 */
69
+ failIfAborted() {
70
+ if (this.aborted) throw this.error
71
+ }
72
+
73
+ /** 步名帧 + 中止守卫:帧存活期栈顶 = `stuck in <name>` 读数源;work 随中止以同一因由拒绝。 */
74
+ run(name, work) {
75
+ this.failIfAborted()
76
+ this._steps.push(name)
77
+ let out
78
+ try { out = work() } catch (e) { this._pop(name); throw e }
79
+ return this.guard(Promise.resolve(out)).finally(() => this._pop(name))
80
+ }
81
+
82
+ /** 中止守卫:在飞工作与中止竞速(未中止 ⇒ 原样返回)。 */
83
+ guard(work) {
84
+ if (this.aborted) return Promise.reject(this.error)
85
+ return Promise.race([work, this._abortP.then(() => Promise.reject(this.error))])
86
+ }
87
+
88
+ /** 受控等待(§2.9 层一:轮询 sleep 同受控)。 */
89
+ async sleep(ms) {
90
+ let timer = null
91
+ try {
92
+ await this.guard(new Promise((resolve) => { timer = setTimeout(resolve, ms) }))
93
+ } finally {
94
+ if (timer) clearTimeout(timer)
95
+ }
96
+ }
97
+
98
+ /** 订阅一次中止(发出即退订);返回退订函数。 */
99
+ onAbort(fn) {
100
+ if (this.aborted) { fn(); return () => {} }
101
+ this._abortHooks.push(fn)
102
+ return () => { this._abortHooks = this._abortHooks.filter((hook) => hook !== fn) }
103
+ }
104
+
105
+ /** 中止单点:因由 = timeout ∥ cancelled ∥ session-lost(错误句分形——§2.9 / §2.10)。 */
106
+ abort(reason, error = null) {
107
+ if (this.aborted) return this.error
108
+ this.aborted = true
109
+ this.reason = reason
110
+ this.error = error ?? this._makeError(reason)
111
+ if (this._timer) { clearTimeout(this._timer); this._timer = null }
112
+ this._resolveAbort()
113
+ for (const hook of this._abortHooks.splice(0)) { try { hook() } catch { /* 订阅者自担 */ } }
114
+ return this.error
115
+ }
116
+
117
+ /** 取消(§2.11 kill 单点——与超时共用控制器)。 */
118
+ cancel() {
119
+ return this.abort("cancelled")
120
+ }
121
+
122
+ /** 动作结束(清钟——不中止)。 */
123
+ finish() {
124
+ if (this._timer) { clearTimeout(this._timer); this._timer = null }
125
+ }
126
+
127
+ _pop(name) {
128
+ const at = this._steps.lastIndexOf(name)
129
+ if (at >= 0) this._steps.splice(at, 1)
130
+ }
131
+
132
+ _makeError(reason) {
133
+ const step = this._steps.at(-1) ?? QUEUE_WAIT_STEP
134
+ const message = reason === "cancelled"
135
+ ? `${this.action} cancelled (stuck in ${step})`
136
+ : `${this.action} timed out after ${this.budgetMs}ms (stuck in ${step}) — retry the action, or run \`close\` to reset the session`
137
+ const error = new Error(message)
138
+ error.browserAbort = true
139
+ error.abortReason = reason
140
+ return error
141
+ }
142
+ }
143
+
144
+ /** 串行队列(§2.4:批并行调用按调用序排队——单页不引入并发)。 */
145
+ export function createActionQueue() {
146
+ let tail = Promise.resolve()
147
+ const chain = (task) => {
148
+ const next = tail.then(task)
149
+ tail = next.then(() => { /* 保持链活 */ }, () => { /* 失败不断链 */ })
150
+ return next
151
+ }
152
+ return {
153
+ /** 入队动作:`run(ctl)` 起执行;返回终态(回执 ∥ 拒绝)。排队中到点 ⇒ 即时以同失败形拒绝
154
+ * (`queue wait`——动作从未起执行);起执行后由任务自身守卫裁决 —— 回执/拒绝晚于动作收尾
155
+ * (调用方拿到终态时会话侧清理已完成,不留残余控制器)。
156
+ * signal(批 browser-async-fix 最小增补——可选取消柄):abort ⇒ `cancel()`(queued 丢队 ∥ running 展开);缺省 null 旧调用零变。 */
157
+ enqueue(action, args, run, signal = null) {
158
+ const ctl = new ActionController({ action, budgetMs: budgetFor(action, args) })
159
+ if (signal?.aborted) ctl.cancel()
160
+ else signal?.addEventListener("abort", () => ctl.cancel(), { once: true })
161
+ let started = false
162
+ const task = chain(() => { started = true; return run(ctl) }).finally(() => ctl.finish())
163
+ return new Promise((resolve, reject) => {
164
+ task.then(resolve, reject)
165
+ ctl.onAbort(() => { if (!started) reject(ctl.error) })
166
+ })
167
+ },
168
+ /** 队列直通(无预算语义——空闲关等动作外路径)。 */
169
+ chain,
170
+ }
171
+ }