@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,161 @@
1
+ /**
2
+ * browser/cdp.mjs — CDP 传输客户端(零第三方——N-BT1:Node ≥ 24 原生 WebSocket)。
3
+ *
4
+ * 形沿 `scripts/console-walkthrough.mjs:133-141` 的实证客户端(id 派发 ∥ 事件派发)——本文档把它
5
+ * 拆成类以承载多动作会话(connect / call / on / close),并加 HTTP 面 `/json/new` 取标签页。
6
+ * `WebSocketImpl` = 注入缝(缺省 `globalThis.WebSocket`;单元测试注假传输——缺省回落真实现,`??`)。
7
+ *
8
+ * 错误语义:CDP 回 `{ error: { message } }` ⇒ 拒以 `Error(message)`(BROWSER-TOOL.md §5)。
9
+ * 帽(§2.9 三层帽——层二 / 层三):`call` 缺省帽 15s(任何未显式配帽的调用也不悬挂);`connectCdp` open 帽
10
+ * 10s;`/json/new` fetch 帽 5s——动作内调用另随动作控制器(session 侧接线,预算先行)。
11
+ * 断连面:WS `onclose` / `onerror` ⇒ 拒在飞命令**并通知订阅者**(session 据此复位会话态——§2.10 源②)。
12
+ */
13
+
14
+ /** `call` 缺省兜底帽(§2.9 层三——未显式配帽的调用同样有界)。 */
15
+ export const DEFAULT_CALL_TIMEOUT_MS = 15_000
16
+ /** `connectCdp` open 帽(§2.9 层二)。 */
17
+ export const CONNECT_TIMEOUT_MS = 10_000
18
+ /** `/json/new` fetch 帽(§2.9 层二)。 */
19
+ export const NEWTAB_TIMEOUT_MS = 5_000
20
+
21
+ /** 与浏览器标签页的一条 CDP 连接。 */
22
+ export class CdpClient {
23
+ constructor(ws) {
24
+ this._ws = ws
25
+ this._nextId = 0
26
+ this._pending = new Map()
27
+ this._listeners = new Map()
28
+ this._disconnects = []
29
+ this.closed = false
30
+ ws.onmessage = (ev) => this._onMessage(ev)
31
+ ws.onclose = () => this._die(new Error("CDP connection closed"))
32
+ ws.onerror = () => this._die(new Error("CDP connection error"))
33
+ }
34
+
35
+ _onMessage(ev) {
36
+ let msg
37
+ try { msg = JSON.parse(ev.data) } catch { return } // 非 JSON 帧(调试器噪声)忽略
38
+ if (msg.id !== undefined && this._pending.has(msg.id)) {
39
+ const p = this._pending.get(msg.id)
40
+ this._pending.delete(msg.id)
41
+ if (msg.error) p.reject(new Error(msg.error.message ?? JSON.stringify(msg.error)))
42
+ else p.resolve(msg.result ?? {})
43
+ return
44
+ }
45
+ const handlers = msg.method ? this._listeners.get(msg.method) : null
46
+ if (!handlers) return
47
+ for (const handler of handlers) {
48
+ try { handler(msg.params ?? {}, msg.method) } catch { /* 订阅者自担——一条订阅抛错不打断派发 */ }
49
+ }
50
+ }
51
+
52
+ /** 连接断(WS close/error)单点:通知订阅者(先)⇒ 拒在飞命令(同一因由实例——§2.10)。 */
53
+ _die(error) {
54
+ if (this.closed) return
55
+ this.closed = true
56
+ for (const handler of this._disconnects) {
57
+ try { handler(error) } catch { /* 订阅者自担 */ }
58
+ }
59
+ this._abortAll(error)
60
+ }
61
+
62
+ _abortAll(error) {
63
+ for (const pending of this._pending.values()) pending.reject(error)
64
+ this._pending.clear()
65
+ }
66
+
67
+ /** 订阅断连(会话健康接线——§2.10 源②);返回退订函数。 */
68
+ onDisconnect(handler) {
69
+ this._disconnects.push(handler)
70
+ return () => { this._disconnects = this._disconnects.filter((h) => h !== handler) }
71
+ }
72
+
73
+ /** 发一条 CDP 命令,解析其 result(失败拒以 `Error`);缺省帽 15s(§2.9 层三)。 */
74
+ call(method, params = {}, { timeoutMs = DEFAULT_CALL_TIMEOUT_MS } = {}) {
75
+ if (this.closed) return Promise.reject(new Error("CDP client is closed"))
76
+ const id = ++this._nextId
77
+ return new Promise((resolve, reject) => {
78
+ const timer = setTimeout(() => {
79
+ this._pending.delete(id)
80
+ const error = new Error(`${method} did not respond within ${timeoutMs}ms`)
81
+ error.cdpTimeout = true
82
+ reject(error)
83
+ }, timeoutMs)
84
+ timer.unref?.() // 挂死型调用不吊住宿主进程(命令真返回时在 settle 里清钟)
85
+ const settle = (fn, value) => { clearTimeout(timer); this._pending.delete(id); fn(value) }
86
+ this._pending.set(id, { resolve: (v) => settle(resolve, v), reject: (e) => settle(reject, e) })
87
+ try { this._ws.send(JSON.stringify({ id, method, params })) }
88
+ catch (e) { settle(reject, e) }
89
+ })
90
+ }
91
+
92
+ /** 订阅 CDP 事件(无 id 帧);返回退订函数。 */
93
+ on(method, handler) {
94
+ const list = this._listeners.get(method) ?? []
95
+ list.push(handler)
96
+ this._listeners.set(method, list)
97
+ return () => this._listeners.set(method, (this._listeners.get(method) ?? []).filter((h) => h !== handler))
98
+ }
99
+
100
+ close() {
101
+ if (this.closed) return
102
+ this.closed = true
103
+ this._abortAll(new Error("CDP client closed"))
104
+ try { this._ws.close() } catch { /* 已断开 */ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * 连标签页调试地址(open 帽 10s——§2.9 层二)。
110
+ * @param {string} wsUrl `webSocketDebuggerUrl`
111
+ * @param {{WebSocketImpl?: Function, openTimeoutMs?: number}} [opts] 注入缝 ∥ open 帽
112
+ */
113
+ export async function connectCdp(wsUrl, { WebSocketImpl, openTimeoutMs = CONNECT_TIMEOUT_MS } = {}) {
114
+ const Impl = WebSocketImpl ?? globalThis.WebSocket
115
+ if (typeof Impl !== "function") {
116
+ throw new Error("no WebSocket implementation available — Node >= 24 required (CDP transport is stdlib-only)")
117
+ }
118
+ const ws = new Impl(wsUrl)
119
+ await new Promise((resolve, reject) => {
120
+ let settled = false
121
+ const finish = (fn, value) => {
122
+ if (settled) return
123
+ settled = true
124
+ clearTimeout(timer)
125
+ fn(value)
126
+ }
127
+ const timer = setTimeout(
128
+ () => finish(reject, new Error(`failed to connect to CDP at ${wsUrl} (no response within ${openTimeoutMs}ms)`)),
129
+ openTimeoutMs,
130
+ )
131
+ timer.unref?.()
132
+ ws.onopen = () => finish(resolve)
133
+ ws.onerror = () => finish(reject, new Error(`failed to connect to CDP at ${wsUrl}`))
134
+ })
135
+ return new CdpClient(ws)
136
+ }
137
+
138
+ /** 开一张标签页并取其调试地址(`PUT /json/new`,GET 兜底——walkthrough `:131-132` 同式;fetch 帽 5s)。 */
139
+ export async function newTabWebSocketUrl(port, { host = "127.0.0.1", fetchImpl = globalThis.fetch, timeoutMs = NEWTAB_TIMEOUT_MS } = {}) {
140
+ const url = `http://${host}:${port}/json/new?about:blank`
141
+ let timer = null
142
+ const deadline = new Promise((_, reject) => {
143
+ timer = setTimeout(
144
+ () => reject(new Error(`browser did not answer /json/new within ${timeoutMs}ms (port ${port})`)),
145
+ timeoutMs,
146
+ )
147
+ timer.unref?.()
148
+ })
149
+ const fetchTab = async (init) => (await fetchImpl(url, init)).json()
150
+ try {
151
+ let tab
152
+ try { tab = await Promise.race([fetchTab({ method: "PUT" }), deadline]) }
153
+ catch { tab = await Promise.race([fetchTab(), deadline]) } // GET 兜底(同帽——到点即拒,不悬挂)
154
+ if (!tab?.webSocketDebuggerUrl) {
155
+ throw new Error(`browser did not return a debuggable tab from /json/new (port ${port})`)
156
+ }
157
+ return tab.webSocketDebuggerUrl
158
+ } finally {
159
+ if (timer) clearTimeout(timer)
160
+ }
161
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * browser/clipboard.mjs — 剪贴板动作(read / write / copy / paste)∥ Browser 域授权 ∥ 页侧表达式
3
+ * (BROWSER-TOOL.md §3.5 ∥ §2.2 clipboard ∥ N-BT9)。
4
+ *
5
+ * 路径(KD-13):CDP 无剪贴板直控方法 ⇒ 页面 API(`navigator.clipboard`)+ Browser 域按 origin 惰性授予;
6
+ * `write` 页面拒 / 非安全上下文 ⇒ 回落 `document.execCommand('copy')` 隐藏 textarea 面(再败才报错)。
7
+ * copy / paste = 真加速键(平台分支:darwin ⇒ Meta+C/V;win32 / linux ⇒ Ctrl+C/V——受信按键直驱浏览器复制 / 粘贴)。
8
+ * 隐私(N-BT9):read 回执上限 8000 字符 + 截断标记;write 回执不回显文本;剪贴板内容不落盘、不写日志(本档零 fs 面)。
9
+ */
10
+ import { pressSequence } from "./input.mjs"
11
+ import { truncateText } from "./snapshot.mjs"
12
+
13
+ export const CLIPBOARD_MAX_CHARS = 8_000
14
+ export const CLIPBOARD_OPS = ["read", "write", "copy", "paste"]
15
+ /** `Browser.setPermission` 取的描述符名(实测在册可用——PermissionDescriptor ≠ PermissionType)。 */
16
+ export const CLIPBOARD_PERMISSION_DESCRIPTORS = ["clipboard-read", "clipboard-write"]
17
+ /** `Browser.grantPermissions` 取的权限枚举(PermissionType——回落面,两法同名不同名面,实测在案)。 */
18
+ export const CLIPBOARD_PERMISSIONS = ["clipboardReadWrite", "clipboardSanitizedWrite"]
19
+
20
+ const mark = (kind) => `/*thincoder-browser:${kind}*/`
21
+
22
+ /** 授权缓存(§3.5:会话内按 origin 缓存——close / 换 origin 重授;键 = 连接身份)。 */
23
+ let grantCache = { client: null, origins: new Set() }
24
+ /** 无头焦点模拟缓存(§3.5:会话内一次性——仅剪贴板动作触发,不进其他动作路径)。 */
25
+ let focusCache = { client: null, done: false }
26
+
27
+ /** 无头焦点适配:`document.hasFocus()` 为假 ⇒ `Emulation.setFocusEmulationEnabled`(一次/会话)。 */
28
+ async function ensureFocusEmulation(h) {
29
+ const client = h.connection()
30
+ if (client && focusCache.client === client && focusCache.done) return
31
+ let hasFocus = true
32
+ try { hasFocus = (await h.evalRaw("document.hasFocus()")) === true } catch { hasFocus = true } // 页不可读 ⇒ 不模拟
33
+ if (!hasFocus) await h.call("Emulation.setFocusEmulationEnabled", { enabled: true })
34
+ focusCache = { client, done: true }
35
+ }
36
+
37
+ /**
38
+ * 按当前页 origin 惰性授予(§3.5):`setPermission` 两权(描述符名)→ 方法缺 / 拒 ⇒ `grantPermissions`
39
+ * 回落(PermissionType 枚举)→ 再败明示错。实测(本机 Edge):`setPermission` 只认描述符名
40
+ * (`clipboard-read` / `clipboard-write`);PermissionType 名在该方法上被拒(版本 / 名面漂移——回落层兜住)。
41
+ */
42
+ async function ensureClipboardPermission(h, origin) {
43
+ const client = h.connection()
44
+ if (grantCache.client !== client) grantCache = { client, origins: new Set() }
45
+ if (grantCache.origins.has(origin)) return
46
+ try {
47
+ for (const name of CLIPBOARD_PERMISSION_DESCRIPTORS) {
48
+ await h.call("Browser.setPermission", { permission: { name }, setting: "granted", origin })
49
+ }
50
+ } catch (e) {
51
+ try {
52
+ await h.call("Browser.grantPermissions", { permissions: CLIPBOARD_PERMISSIONS, origin })
53
+ } catch (e2) {
54
+ throw h.pageError(`clipboard permission failed — setPermission: ${e?.message ?? e}; grantPermissions fallback: ${e2?.message ?? e2}`)
55
+ }
56
+ }
57
+ grantCache.origins.add(origin)
58
+ }
59
+
60
+ /** 当前页 origin(授权键);无真实页 origin(about:blank 等)⇒ 明示拒(不静默降级)。 */
61
+ async function pageOrigin(h) {
62
+ const info = await h.pageInfo()
63
+ let origin = null
64
+ try { origin = new URL(info.url).origin } catch { origin = null }
65
+ if (!origin || origin === "null") throw h.pageError(`clipboard requires an http/https page origin — current page is "${info.url}"`)
66
+ return origin
67
+ }
68
+
69
+ /** 页侧写表达式:`navigator.clipboard.writeText` → 失败回落 `document.execCommand('copy')` 隐藏 textarea。 */
70
+ function clipboardWriteExpression(text) {
71
+ return `${mark("clipboard-write")}(async function () {
72
+ var text = ${JSON.stringify(text)}
73
+ try {
74
+ if (navigator.clipboard && navigator.clipboard.writeText) {
75
+ await navigator.clipboard.writeText(text)
76
+ return { ok: true, via: 'navigator.clipboard' }
77
+ }
78
+ } catch (e) { /* 页面拒 / 非安全上下文 ⇒ 走 execCommand 回落 */ }
79
+ try {
80
+ var ta = document.createElement('textarea')
81
+ ta.value = text
82
+ ta.setAttribute('readonly', '')
83
+ ta.style.position = 'fixed'
84
+ ta.style.opacity = '0'
85
+ document.body.appendChild(ta)
86
+ ta.select()
87
+ var ok = false
88
+ try { ok = document.execCommand('copy') } finally { document.body.removeChild(ta) }
89
+ return ok ? { ok: true, via: 'execCommand' } : { ok: false, message: 'document.execCommand("copy") returned false' }
90
+ } catch (e2) { return { ok: false, message: String((e2 && e2.message) || e2) } }
91
+ })()`
92
+ }
93
+
94
+ /** 页侧读表达式:`navigator.clipboard.readText()`(安全上下文限定——非安全上下文给引导句)。 */
95
+ function clipboardReadExpression() {
96
+ return `${mark("clipboard-read")}(async function () {
97
+ try {
98
+ if (!navigator.clipboard || !navigator.clipboard.readText) {
99
+ return { ok: false, message: 'navigator.clipboard.readText is unavailable — a secure context (https or localhost) is required' }
100
+ }
101
+ var text = await navigator.clipboard.readText()
102
+ return { ok: true, text: String(text == null ? '' : text) }
103
+ } catch (e) { return { ok: false, message: String((e && e.message) || e) } }
104
+ })()`
105
+ }
106
+
107
+ /** 加速键序列(§3.5 平台分支):darwin ⇒ Meta+C/V;win32 / linux ⇒ Ctrl+C/V。 */
108
+ export function acceleratorSequence(op, platform = process.platform) {
109
+ const modifier = platform === "darwin" ? "Meta" : "Control"
110
+ const key = op === "copy" ? "c" : "v"
111
+ return pressSequence({ key, modifiers: [modifier] }).commands
112
+ }
113
+
114
+ /** clipboard(§2.2):op ∈ read / write / copy / paste;write 必填 text;copy / paste 可携 ref 聚焦。 */
115
+ async function actClipboard(args, ctx, h) {
116
+ const op = String(args.op ?? "")
117
+ if (!CLIPBOARD_OPS.includes(op)) throw new Error(`unknown clipboard op "${op}" — ops: ${CLIPBOARD_OPS.join(", ")}`)
118
+ await h.ensureSession(args)
119
+ await ensureFocusEmulation(h)
120
+ const origin = await pageOrigin(h)
121
+ await ensureClipboardPermission(h, origin)
122
+ if (op === "write") {
123
+ const text = String(args.text)
124
+ const res = await h.evalRaw(clipboardWriteExpression(text))
125
+ if (!res?.ok) throw h.pageError(`clipboard write failed: ${res?.message ?? "the page rejected the write"}`)
126
+ return `[clipboard] write ← ${text.length} chars` // 不回显文本(N-BT9)
127
+ }
128
+ if (op === "read") {
129
+ const res = await h.evalRaw(clipboardReadExpression())
130
+ if (!res?.ok) throw h.pageError(`clipboard read failed: ${res?.message ?? "the page rejected the read"}`)
131
+ const text = String(res.text ?? "")
132
+ const body = text ? `\n${truncateText(text, CLIPBOARD_MAX_CHARS, `clipboard reads are capped at ${CLIPBOARD_MAX_CHARS} chars`)}` : ""
133
+ return `[clipboard] read (${text.length} chars)${body}`
134
+ }
135
+ let target = null
136
+ if (typeof args.ref === "string" && args.ref !== "") target = await h.focusRef(args.ref)
137
+ for (const cmd of acceleratorSequence(op)) await h.call(cmd.method, cmd.params)
138
+ return `[clipboard] ${op}${target ? ` → ${target.label}` : ""}`
139
+ }
140
+
141
+ /** 剪贴板分发表(会话档并入总表——§5)。 */
142
+ export const CLIPBOARD_ACTIONS = { clipboard: actClipboard }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * browser/input-actions.mjs — 新七动作实现(press / hover / wheel / mouse / drag / touch / insert)
3
+ * + 目标解析(ref → 真点 ∥ x,y)(BROWSER-TOOL.md §2.2 ∥ §2.7 ∥ §6 KD-14 / KD-16 / KD-17)。
4
+ *
5
+ * 会话能力一律经句柄取(`h.call` / `h.evalRaw` / `h.ensureSession` / `h.resolveRef` / `h.focusRef` /
6
+ * `h.once` / `h.pageError`)——本档不 import `session.mjs`(DAG 单向,§5 拆分决定)。
7
+ * 一切 ref 寻址的输入动作经 `pointExpression` 自动滚动到视(F-BT14——视口外目标同样可驱)。
8
+ */
9
+ import { clampSteps, dragSequence, mouseSequence, moveSequence, parseTargetSpec, pressSequence, TOUCH_GESTURES, TOUCH_TAP_GAP_MS, touchSequence, wheelSequence } from "./input.mjs"
10
+ import { pointExpression } from "./snapshot.mjs"
11
+
12
+ export const WHEEL_SETTLE_MS = 800 // 卷动稳定读数上限(§2.2 wheel 判据)
13
+ export const IME_COMMIT_BREATH_MS = 50 // 组合与提交之间留一拍(中间态可观察——KD-17)
14
+ export const DRAG_INTERCEPT_TIMEOUT_MS = 5_000 // html5 拖拽拦截等待(无拦截事件 ⇒ 明示拒——不悬挂)
15
+
16
+ async function sendAll(h, commands) {
17
+ for (const cmd of commands) await h.call(cmd.method, cmd.params)
18
+ }
19
+
20
+ /** 视口中心(wheel 落点缺省——§2.2)。 */
21
+ async function viewportCenter(h) {
22
+ const v = await h.evalRaw("({ x: Math.round(innerWidth / 2), y: Math.round(innerHeight / 2) })")
23
+ const x = Number(v?.x ?? 0)
24
+ const y = Number(v?.y ?? 0)
25
+ return { x, y, ref: null, name: null, label: `${x},${y}` }
26
+ }
27
+
28
+ /** 目标解析(§2.7):ref ⇒ 表查 + `pointExpression`(滚动到视 + 中心点);x,y ⇒ 直读;缺省 ⇒ fallback。 */
29
+ async function targetOf(args, h, fallback = null) {
30
+ const hasRef = typeof args?.ref === "string" && args.ref !== ""
31
+ const hasXY = args?.x !== undefined && args?.y !== undefined && Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y))
32
+ if (hasRef) {
33
+ const entry = h.resolveRef(args.ref)
34
+ const p = await h.evalRaw(pointExpression(entry.selector, entry.tag))
35
+ if (!p?.found || p.tag) throw h.pageError(`ref ${args.ref} is stale (was "${entry.name}") — run snapshot again`)
36
+ return { x: Number(p.x), y: Number(p.y), ref: args.ref, name: entry.name, label: `${args.ref} "${entry.name}"` }
37
+ }
38
+ if (hasXY) return { x: Number(args.x), y: Number(args.y), ref: null, name: null, label: `${Number(args.x)},${Number(args.y)}` }
39
+ if (fallback) return fallback(h)
40
+ throw new Error(`${args?.action ?? "action"} requires ref or x,y`)
41
+ }
42
+
43
+ /** `e<N>` ∥ `"x,y"` 形目标(drag / touch swipe 的 from、to——§2.2)。 */
44
+ async function specTarget(spec, h, action, param) {
45
+ const parsed = parseTargetSpec(spec)
46
+ if (!parsed) throw new Error(`${action} ${param} must be an element ref (e<N>) or "x,y"`)
47
+ if (parsed.kind === "ref") return targetOf({ ref: parsed.ref }, h)
48
+ return { x: parsed.x, y: parsed.y, ref: null, name: null, label: `${parsed.x},${parsed.y}` }
49
+ }
50
+
51
+ /** press(§2.2):真按键链 + 携 ref 先聚焦(§2.7);未知键 ⇒ 未起会话先拒(不执行)。 */
52
+ async function actPress(args, ctx, h) {
53
+ const seq = pressSequence({ key: args.key, modifiers: args.modifiers, phase: args.phase, repeat: args.repeat })
54
+ await h.ensureSession(args)
55
+ let target = ""
56
+ if (typeof args.ref === "string" && args.ref !== "") {
57
+ const f = await h.focusRef(args.ref)
58
+ target = ` → ${f.label}`
59
+ }
60
+ await sendAll(h, seq.commands)
61
+ const repeat = seq.repeat > 0 ? ` ×${seq.repeat}` : ""
62
+ const phase = args.phase === "down" || args.phase === "up" ? ` (${args.phase})` : ""
63
+ return `[press] ${seq.combo}${repeat}${phase}${target}`
64
+ }
65
+
66
+ /** hover(§2.2):指针移动(无激活)。 */
67
+ async function actHover(args, ctx, h) {
68
+ await h.ensureSession(args)
69
+ const t = await targetOf(args, h)
70
+ await sendAll(h, moveSequence({ x: t.x, y: t.y }))
71
+ return `[hover] ${t.label}`
72
+ }
73
+
74
+ /** 卷动稳定读数(≤800ms——两次连读相等即定);标 `scroll settle` 步名(§2.9 卡点可诊断)。 */
75
+ async function settleScroll(h, maxMs = WHEEL_SETTLE_MS) {
76
+ return h.step("scroll settle", async () => {
77
+ const deadline = Date.now() + maxMs
78
+ let last = null
79
+ for (;;) {
80
+ const v = await h.evalRaw("({ x: Math.round(window.scrollX), y: Math.round(window.scrollY) })")
81
+ const reading = { x: Number(v?.x ?? 0), y: Number(v?.y ?? 0) }
82
+ if (last && last.x === reading.x && last.y === reading.y) return reading
83
+ if (Date.now() >= deadline) return reading
84
+ last = reading
85
+ await h.sleep(100)
86
+ }
87
+ })
88
+ }
89
+
90
+ /** wheel(§2.2):落点缺省 = 视口中心;回执 = 卷动后 window 读数。 */
91
+ async function actWheel(args, ctx, h) {
92
+ await h.ensureSession(args)
93
+ const deltaX = Number(args.deltaX ?? 0) || 0
94
+ const deltaY = Number(args.deltaY ?? 0) || 0
95
+ const t = await targetOf(args, h, viewportCenter)
96
+ await sendAll(h, wheelSequence({ x: t.x, y: t.y, deltaX, deltaY }))
97
+ const win = await settleScroll(h)
98
+ return `[wheel] Δ(${deltaX},${deltaY}) at ${Math.round(t.x)},${Math.round(t.y)} — window (${win.x},${win.y})`
99
+ }
100
+
101
+ /** mouse(§2.2):真指针(命中测试生效);形 = click(默认)∥ down ∥ up;double ⇒ clickCount 1、2。 */
102
+ async function actMouse(args, ctx, h) {
103
+ await h.ensureSession(args)
104
+ const t = await targetOf(args, h)
105
+ const phase = args.phase === "down" || args.phase === "up" ? args.phase : "click"
106
+ const double = args.double === true
107
+ const button = String(args.button ?? "left")
108
+ await sendAll(h, mouseSequence({ x: t.x, y: t.y, button, phase, double }))
109
+ const shape = phase === "click" ? (double ? "doubleClick" : "click") : phase
110
+ return `[mouse] ${button} ${shape} ${t.label}`
111
+ }
112
+
113
+ /** HTML5 原生拖放(KD-16,实验):拦截 → `dragIntercepted` 取 DragData → 事件三连 → 投递 → 抬起。 */
114
+ async function html5Drag(h, from, to, steps) {
115
+ await h.call("Input.setInterceptDrags", { enabled: true })
116
+ try {
117
+ const intercepted = h.once("Input.dragIntercepted", DRAG_INTERCEPT_TIMEOUT_MS)
118
+ // 占位消费者:下方 sendAll 抛出时该 promise 无人 await——无此挂载即 unhandled 拒绝(宿主进程级 fatal)。
119
+ intercepted.catch(() => {})
120
+ await sendAll(h, dragSequence({ from, to, steps, release: false }))
121
+ let data
122
+ try { data = (await intercepted).data } catch (e) {
123
+ if (e?.browserAbort || e?.sessionLost) throw e // 预算中止 / 外部关闭 ⇒ 原样上报(不冒名「未拦截」)
124
+ throw new Error("html5 drag was not intercepted — the source may not be a native draggable; retry without html5")
125
+ }
126
+ await h.call("Input.dispatchDragEvent", { type: "dragEnter", x: to.x, y: to.y, data })
127
+ await h.call("Input.dispatchDragEvent", { type: "dragOver", x: to.x, y: to.y, data })
128
+ await h.call("Input.dispatchDragEvent", { type: "drop", x: to.x, y: to.y, data })
129
+ await sendAll(h, mouseSequence({ x: to.x, y: to.y, phase: "up" }))
130
+ } finally {
131
+ try { await h.call("Input.setInterceptDrags", { enabled: false }) } catch { /* 会话已断——拦截位随进程 */ }
132
+ }
133
+ }
134
+
135
+ /** drag(§2.2):默认鼠标序列(按下 → 移动×steps → 抬起);`html5:true` = 拦截链(KD-16)。 */
136
+ async function actDrag(args, ctx, h) {
137
+ await h.ensureSession(args)
138
+ const from = await specTarget(args.from, h, "drag", "from")
139
+ const to = await specTarget(args.to, h, "drag", "to")
140
+ const steps = clampSteps(args.steps)
141
+ if (args.html5 === true) await html5Drag(h, from, to, steps)
142
+ else await sendAll(h, dragSequence({ from, to, steps }))
143
+ return `[drag] ${from.label} → ${to.label}${args.html5 === true ? " (html5)" : ""}`
144
+ }
145
+
146
+ /** touch(§2.2):tap / doubleTap / pinch(落点 + scale)∥ swipe(from → to);KD-14 分工在引擎档。 */
147
+ async function actTouch(args, ctx, h) {
148
+ const gesture = String(args.gesture ?? "tap")
149
+ if (!TOUCH_GESTURES.includes(gesture)) throw new Error(`unknown touch gesture "${gesture}" — gestures: ${TOUCH_GESTURES.join(", ")}`)
150
+ await h.ensureSession(args)
151
+ if (gesture === "swipe") {
152
+ const from = await specTarget(args.from, h, "touch", "from")
153
+ const to = await specTarget(args.to, h, "touch", "to")
154
+ await sendAll(h, touchSequence({ gesture, from, to, steps: clampSteps(args.steps) }))
155
+ return `[touch] swipe ${from.label} → ${to.label}`
156
+ }
157
+ const t = await targetOf(args, h)
158
+ if (gesture === "pinch") {
159
+ const scale = Number(args.scale)
160
+ await sendAll(h, touchSequence({ gesture, x: t.x, y: t.y, scale }))
161
+ return `[touch] pinch ×${scale} at ${Math.round(t.x)},${Math.round(t.y)}`
162
+ }
163
+ // tap / doubleTap:显式序列;doubleTap = 两轮 + 双击窗内间隔(KD-14 收正)
164
+ const rounds = gesture === "doubleTap" ? 2 : 1
165
+ for (let i = 0; i < rounds; i++) {
166
+ if (i > 0) await h.sleep(TOUCH_TAP_GAP_MS)
167
+ await sendAll(h, touchSequence({ gesture, x: t.x, y: t.y }))
168
+ }
169
+ return `[touch] ${gesture} ${t.label}`
170
+ }
171
+
172
+ /** insert(§2.2):纯文本插入(不按键——长文本 / emoji 路径);`ime` = 组合候选 → `insertText` 提交(KD-17)。 */
173
+ async function actInsert(args, ctx, h) {
174
+ const text = String(args.text)
175
+ const ime = typeof args.ime === "string" && args.ime !== "" ? args.ime : null
176
+ await h.ensureSession(args)
177
+ let target = null
178
+ if (typeof args.ref === "string" && args.ref !== "") target = await h.focusRef(args.ref)
179
+ if (ime) {
180
+ await h.call("Input.imeSetComposition", { text: ime, selectionStart: ime.length, selectionEnd: ime.length })
181
+ await h.sleep(IME_COMMIT_BREATH_MS)
182
+ }
183
+ await h.call("Input.insertText", { text })
184
+ const chars = `← ${text.length} chars${ime ? " (ime)" : ""}`
185
+ return target ? `[insert] ${target.label} ${chars}` : `[insert] (focused) ${chars}`
186
+ }
187
+
188
+ /** 新七动作分发表(会话档并入总表——§5)。 */
189
+ export const INPUT_ACTIONS = {
190
+ press: actPress,
191
+ hover: actHover,
192
+ wheel: actWheel,
193
+ mouse: actMouse,
194
+ drag: actDrag,
195
+ touch: actTouch,
196
+ insert: actInsert,
197
+ }