@twinsearth/fang-dsh-plugin 0.0.0-stage → 3.3.4

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 (144) hide show
  1. package/CLA.md +79 -0
  2. package/LICENSE-COMMERCIAL +78 -0
  3. package/LICENSE-CORE +661 -0
  4. package/LICENSE-SDK +21 -0
  5. package/LICENSES/cordis-MIT.txt +39 -0
  6. package/NOTICE +95 -0
  7. package/README.md +158 -2
  8. package/client.js +7287 -0
  9. package/cordis.patch.yml +163 -0
  10. package/defaults/umbrella-rules.yaml +113 -0
  11. package/docs/CONTRACT-INTEROP.md +77 -0
  12. package/docs/CRASH-SAFETY.md +69 -0
  13. package/docs/DECISION-RELAY.md +46 -0
  14. package/docs/DSH-ARCHITECTURE-MAP.md +73 -0
  15. package/docs/FORK-SEED.md +64 -0
  16. package/docs/MAIN-CITY-GATE.md +58 -0
  17. package/docs/OVERLAY-MASTER-SWITCH.md +35 -0
  18. package/docs/PACKAGE-RENAME-v3.3.3.md +40 -0
  19. package/docs/PM-DECISION-POLICY.md +61 -0
  20. package/docs/WORKFLOW-UI.md +86 -0
  21. package/icon.svg +8 -0
  22. package/locale/en.json +6 -0
  23. package/locale/zh.json +6 -0
  24. package/package.json +71 -4
  25. package/sdk/README.md +65 -0
  26. package/sdk/examples/status-badge/README.md +16 -0
  27. package/sdk/examples/status-badge/client.js +60 -0
  28. package/sdk/examples/status-badge/cordis.patch.yml +7 -0
  29. package/sdk/examples/status-badge/index.js +12 -0
  30. package/sdk/examples/status-badge/package.json +24 -0
  31. package/sdk/templates/plugin/README.md +36 -0
  32. package/sdk/templates/plugin/client.js +35 -0
  33. package/sdk/templates/plugin/cordis.patch.yml +8 -0
  34. package/sdk/templates/plugin/icon.svg +5 -0
  35. package/sdk/templates/plugin/index.js +23 -0
  36. package/sdk/templates/plugin/locale/en.json +6 -0
  37. package/sdk/templates/plugin/package.json +36 -0
  38. package/src/compose.mjs +67 -0
  39. package/src/contract/VENDOR.json +16 -0
  40. package/src/contract/client.js +93 -0
  41. package/src/contract/constants.js +94 -0
  42. package/src/contract/envelope.js +83 -0
  43. package/src/contract/index.js +20 -0
  44. package/src/contract/negotiate.js +106 -0
  45. package/src/contract/relay.js +223 -0
  46. package/src/contract/server.js +77 -0
  47. package/src/contract/transports.js +319 -0
  48. package/src/index.mjs +11 -0
  49. package/src/kernel/core.mjs +100 -0
  50. package/src/kernel/extensions.mjs +61 -0
  51. package/src/kernel/fsx.mjs +349 -0
  52. package/src/kernel/index.mjs +21 -0
  53. package/src/kernel/ledger.mjs +94 -0
  54. package/src/kernel/paths.mjs +182 -0
  55. package/src/kernel/role.mjs +68 -0
  56. package/src/kernel/rounds.mjs +172 -0
  57. package/src/kernel/schema.mjs +5 -0
  58. package/src/kernel/types.mjs +266 -0
  59. package/src/kernel/vendor/cosmokit.mjs +421 -0
  60. package/src/kernel/vendor/schemastery.mjs +638 -0
  61. package/src/lib/adapter-client.mjs +383 -0
  62. package/src/lib/adapter-protocol.mjs +243 -0
  63. package/src/lib/ai-secret-store.mjs +202 -0
  64. package/src/lib/boot-guard.mjs +186 -0
  65. package/src/lib/branch-store.mjs +84 -0
  66. package/src/lib/branch.mjs +74 -0
  67. package/src/lib/bridge-client.mjs +133 -0
  68. package/src/lib/broker-adapter.mjs +67 -0
  69. package/src/lib/broker.mjs +45 -0
  70. package/src/lib/business-producers.mjs +109 -0
  71. package/src/lib/command-cache.mjs +209 -0
  72. package/src/lib/compat.mjs +170 -0
  73. package/src/lib/composer-journal.mjs +128 -0
  74. package/src/lib/container/index.mjs +806 -0
  75. package/src/lib/decision-events.mjs +129 -0
  76. package/src/lib/decision-ingest-client.mjs +41 -0
  77. package/src/lib/decision-outbox.mjs +129 -0
  78. package/src/lib/decision-policy.mjs +52 -0
  79. package/src/lib/default-essence.mjs +61 -0
  80. package/src/lib/desktop-launch.mjs +138 -0
  81. package/src/lib/essence.mjs +1211 -0
  82. package/src/lib/fork-client.mjs +410 -0
  83. package/src/lib/fork-config-lock.mjs +166 -0
  84. package/src/lib/fork-seed.mjs +304 -0
  85. package/src/lib/fork-sessions.mjs +183 -0
  86. package/src/lib/fork-supervision-settings.mjs +58 -0
  87. package/src/lib/fork-supervisor.mjs +122 -0
  88. package/src/lib/fs-guard.mjs +28 -0
  89. package/src/lib/http-auth.mjs +162 -0
  90. package/src/lib/idle-audit.mjs +178 -0
  91. package/src/lib/interaction-poke.mjs +61 -0
  92. package/src/lib/main-gate.mjs +579 -0
  93. package/src/lib/mirror-etag.mjs +92 -0
  94. package/src/lib/mirror-official-source.mjs +93 -0
  95. package/src/lib/mirror.mjs +478 -0
  96. package/src/lib/mother-link.mjs +156 -0
  97. package/src/lib/mutex.mjs +31 -0
  98. package/src/lib/permissions.mjs +115 -0
  99. package/src/lib/pnpm-store.mjs +189 -0
  100. package/src/lib/preferences.mjs +212 -0
  101. package/src/lib/producer-contract.mjs +162 -0
  102. package/src/lib/reflow.mjs +41 -0
  103. package/src/lib/remote.mjs +408 -0
  104. package/src/lib/restart-contract.mjs +752 -0
  105. package/src/lib/roots.mjs +30 -0
  106. package/src/lib/rule-registry.mjs +1127 -0
  107. package/src/lib/runtime-capture.mjs +307 -0
  108. package/src/lib/semaphore.mjs +34 -0
  109. package/src/lib/session-bundle.mjs +138 -0
  110. package/src/lib/session-format.mjs +672 -0
  111. package/src/lib/sse-parser.mjs +102 -0
  112. package/src/lib/ssrf-guard.mjs +170 -0
  113. package/src/lib/status-store.mjs +257 -0
  114. package/src/lib/task-handle.mjs +622 -0
  115. package/src/lib/ui-actions.mjs +55 -0
  116. package/src/lib/umbrella.mjs +1661 -0
  117. package/src/lib/upgrade-coordinator.mjs +107 -0
  118. package/src/lib/upgrade-status.mjs +43 -0
  119. package/src/plugins/adapter.mjs +1468 -0
  120. package/src/plugins/approver.mjs +1210 -0
  121. package/src/plugins/auditor.mjs +330 -0
  122. package/src/plugins/buffer-keeper.mjs +522 -0
  123. package/src/plugins/decision-center.mjs +2581 -0
  124. package/src/plugins/forker.mjs +1610 -0
  125. package/src/plugins/healer.mjs +702 -0
  126. package/src/plugins/hot-reloader.mjs +274 -0
  127. package/src/plugins/imports.mjs +247 -0
  128. package/src/plugins/orchestrator.mjs +3938 -0
  129. package/src/plugins/restart-guard.mjs +1022 -0
  130. package/src/plugins/router.mjs +714 -0
  131. package/src/plugins/scorer.mjs +661 -0
  132. package/src/plugins/session-bridge.mjs +166 -0
  133. package/src/plugins/session-catalog.mjs +771 -0
  134. package/src/plugins/session-router.mjs +125 -0
  135. package/src/plugins/session-sync.mjs +171 -0
  136. package/src/plugins/umbrella.mjs +419 -0
  137. package/tools/boot-guard.mjs +95 -0
  138. package/tools/decision-ingest.mjs +53 -0
  139. package/tools/desktop-launch.mjs +71 -0
  140. package/tools/fangctl.mjs +408 -0
  141. package/tools/gate/main-city.mjs +204 -0
  142. package/tools/mcp/server.mjs +121 -0
  143. package/tools/mcp/tools.mjs +65 -0
  144. package/tools/recover-queue.mjs +133 -0
@@ -0,0 +1,383 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Fang contributors — see LICENSE-CORE.
3
+ // dsh-Fang v2.6.1 — 母体侧 Adapter v3 客户端(契约:docs/PROTOCOL-V3.md §5)。
4
+ //
5
+ // 把「协议协商 + 状态快照 + SSE 事件流 + 断线重连 + cursor 续传 + 幂等命令」封成一个
6
+ // ForkClient。母体(Orchestrator / 未来 Forker 监督器)只接触方法,不碰 HTTP/SSE 细节。
7
+ //
8
+ // 失败语义(刻意与 remote.mjs 一致):**永不抛出**,全部降级为 `{ok:false, ...}`;
9
+ // 网络/超时类失败按指数退避无限重连,401(token 错误)是致命失败、不重试。
10
+ import { RECONNECT_DEFAULTS, createSseParser, negotiateProtocol, newCommandId } from './adapter-protocol.mjs'
11
+ import { classifyRemoteFailure, createRemote } from './remote.mjs'
12
+
13
+ const msgOf = (e) => String(e && e.message !== undefined ? e.message : e)
14
+
15
+ const positiveMs = (value, fallback) => {
16
+ const n = Number(value)
17
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback
18
+ }
19
+
20
+ /**
21
+ * @param {object} [options]
22
+ * @param {string} [options.url] 分身 web URL(可自带 `?token=`)
23
+ * @param {string} [options.token] DSH web token(也用于 SSE 的 ?token=)
24
+ * @param {string} [options.adapterToken] v3 Bearer token(不落 URL)
25
+ * @param {Function} [options.fetchImpl] 可注入 fetch(测试用)
26
+ * @param {number} [options.timeoutMs]
27
+ * @param {string} [options.prefix]
28
+ * @param {number} [options.minProtocol]
29
+ * @param {number} [options.maxProtocol]
30
+ * @param {{baseMs?:number,maxMs?:number}} [options.reconnect]
31
+ * @param {boolean} [options.autoReconnect=true] 事件流断开后是否自动重连
32
+ */
33
+ export function createAdapterClient(options = {}) {
34
+ const {
35
+ url,
36
+ token,
37
+ adapterToken,
38
+ fetchImpl = globalThis.fetch,
39
+ timeoutMs = 8000,
40
+ prefix = '/fang',
41
+ minProtocol,
42
+ maxProtocol,
43
+ reconnect = {},
44
+ autoReconnect = true,
45
+ } = options
46
+
47
+ const remote = createRemote({ url, token, adapterToken, fetchImpl, timeoutMs, prefix })
48
+ const impl = typeof fetchImpl === 'function' ? fetchImpl : null
49
+ const limits = {
50
+ ...(Number.isInteger(minProtocol) ? { min: minProtocol } : {}),
51
+ ...(Number.isInteger(maxProtocol) ? { max: maxProtocol } : {}),
52
+ }
53
+ const backoff = {
54
+ baseMs: positiveMs(reconnect?.baseMs, RECONNECT_DEFAULTS.baseMs),
55
+ maxMs: positiveMs(reconnect?.maxMs, RECONNECT_DEFAULTS.maxMs),
56
+ }
57
+
58
+ const listeners = {
59
+ event: new Set(),
60
+ state: new Set(),
61
+ reconnect: new Set(),
62
+ disconnect: new Set(),
63
+ }
64
+
65
+ let mode = 'idle'
66
+ let protocol = 0
67
+ let sessionId = ''
68
+ let lastCursor = '0'
69
+ let lastState = null
70
+ let lastVersionInfo = null
71
+ let sessionMismatch = null
72
+ let protocolRange = { min: undefined, max: undefined }
73
+ let lastError = ''
74
+ let stopped = true
75
+ let controller = null
76
+ let pumpPromise = null
77
+
78
+ const emit = (kind, payload) => {
79
+ for (const fn of listeners[kind]) {
80
+ try {
81
+ fn(payload)
82
+ } catch {
83
+ /* 订阅者异常不得打断连接 */
84
+ }
85
+ }
86
+ }
87
+
88
+ const on = (kind) => (fn) => {
89
+ if (typeof fn !== 'function') return () => {}
90
+ listeners[kind].add(fn)
91
+ return () => listeners[kind].delete(fn)
92
+ }
93
+
94
+ const pendingWaits = new Map()
95
+
96
+ const wait = (ms) => new Promise((resolve) => {
97
+ const timer = setTimeout(() => {
98
+ pendingWaits.delete(timer)
99
+ resolve()
100
+ }, ms)
101
+ pendingWaits.set(timer, resolve)
102
+ })
103
+
104
+ const backoffFor = (attempt) => Math.min(backoff.maxMs, backoff.baseMs * 2 ** Math.min(attempt, 5))
105
+
106
+ const headers = () => remote.authHeaders()
107
+
108
+ /** 用快照把 cursor/session 对齐(410 cursor-expired / 409 session-changed 后调用)。 */
109
+ async function resyncFromState(reason) {
110
+ lastCursor = '0'
111
+ const snapshot = await remote.state()
112
+ if (snapshot.ok && snapshot.data) {
113
+ lastState = snapshot.data
114
+ if (snapshot.data.sessionId) sessionId = String(snapshot.data.sessionId)
115
+ lastCursor = String(snapshot.data.lastCursor ?? '0')
116
+ emit('state', lastState)
117
+ }
118
+ emit('reconnect', { resetCursor: true, reason, sessionId, lastCursor })
119
+ return snapshot
120
+ }
121
+
122
+ /** 打开一次 SSE 连接:返回可判定结果,不抛。 */
123
+ async function openOnce() {
124
+ if (!impl) return { ok: false, code: 'no-fetch', fatal: true }
125
+ controller = new AbortController()
126
+ const target = remote.eventsUrl({ since: lastCursor, session: sessionId })
127
+ let res = null
128
+ try {
129
+ res = await impl(target, { method: 'GET', headers: headers(), signal: controller.signal })
130
+ } catch (e) {
131
+ return { ok: false, code: classifyRemoteFailure(e).kind, error: msgOf(e), retryable: true }
132
+ }
133
+ const status = Number(res?.status)
134
+ if (status === 401) return { ok: false, code: 'unauthorized', status, fatal: true }
135
+ if (status === 409) {
136
+ await resyncFromState('session-changed')
137
+ return { ok: false, code: 'session-changed', status, resync: true, retryable: true }
138
+ }
139
+ if (status === 410) {
140
+ await resyncFromState('cursor-expired')
141
+ return { ok: false, code: 'cursor-expired', status, resync: true, retryable: true }
142
+ }
143
+ if (!(status >= 200 && status < 300)) return { ok: false, code: `http-${status}`, status, retryable: true }
144
+ return { ok: true, res }
145
+ }
146
+
147
+ /** 读 SSE 流直到结束;解析出的每条事件更新 cursor 并回调 onEvent。 */
148
+ async function pump(res) {
149
+ const parser = createSseParser()
150
+ const body = res?.body
151
+ const onFrame = (frame) => {
152
+ if (!frame || !frame.data) return
153
+ let envelope = null
154
+ try {
155
+ envelope = JSON.parse(frame.data)
156
+ } catch {
157
+ return
158
+ }
159
+ const cursor = frame.id || (envelope && envelope.cursor) || ''
160
+ if (cursor) lastCursor = String(cursor)
161
+ if (envelope && envelope.event) emit('event', envelope)
162
+ }
163
+ try {
164
+ if (body && typeof body.getReader === 'function') {
165
+ const reader = body.getReader()
166
+ const decoder = new TextDecoder()
167
+ for (;;) {
168
+ const chunk = await reader.read()
169
+ if (chunk.done) return { ok: true }
170
+ if (chunk.value !== undefined) {
171
+ for (const frame of parser.push(decoder.decode(chunk.value, { stream: true }))) onFrame(frame)
172
+ }
173
+ }
174
+ }
175
+ if (body && typeof body[Symbol.asyncIterator] === 'function') {
176
+ const decoder = new TextDecoder()
177
+ for await (const value of body) {
178
+ for (const frame of parser.push(decoder.decode(value, { stream: true }))) onFrame(frame)
179
+ }
180
+ return { ok: true }
181
+ }
182
+ return { ok: false, code: 'no-stream-body' }
183
+ } catch (e) {
184
+ return { ok: false, code: classifyRemoteFailure(e).kind, error: msgOf(e) }
185
+ }
186
+ }
187
+
188
+ /** 断线后的重连循环:指数退避;401 致命;409/410 先快照对齐再立即重试。 */
189
+ async function reconnectLoop() {
190
+ let attempt = 0
191
+ while (!stopped) {
192
+ const opened = await openOnce()
193
+ if (stopped) return
194
+ if (opened.fatal) {
195
+ mode = 'failed'
196
+ lastError = opened.code
197
+ emit('disconnect', { reason: opened.code, fatal: true })
198
+ return
199
+ }
200
+ if (opened.ok) {
201
+ attempt = 0
202
+ mode = 'streaming'
203
+ emit('reconnect', { resetCursor: false, reason: 'resumed', sessionId, lastCursor })
204
+ const result = await pump(opened.res)
205
+ if (stopped) return
206
+ mode = 'reconnecting'
207
+ emit('disconnect', { reason: result.code ?? 'stream-closed' })
208
+ if (!autoReconnect) {
209
+ mode = 'idle'
210
+ return
211
+ }
212
+ await wait(backoffFor(attempt++))
213
+ continue
214
+ }
215
+ mode = 'reconnecting'
216
+ lastError = opened.code
217
+ emit('disconnect', { reason: opened.code, retryable: true })
218
+ if (opened.resync) {
219
+ attempt = 0
220
+ await wait(backoffFor(0))
221
+ continue
222
+ }
223
+ await wait(backoffFor(attempt++))
224
+ }
225
+ }
226
+
227
+ /**
228
+ * 建立会话:版本协商 → 首帧快照 → 打开事件流。
229
+ * 结果可判定:`{ok:true, protocol, sessionId, lastCursor}` 或 `{ok:false, code, fatal|retryable}`。
230
+ * 首连失败但可重试时仍会启动后台重连(设计上母体不放弃分身,只等在 onDisconnect 里上报)。
231
+ */
232
+ async function connect() {
233
+ stopped = false
234
+ mode = 'negotiating'
235
+ const versionRes = await remote.version()
236
+ if (!versionRes.ok) {
237
+ const fatal = Number(versionRes.status) === 401
238
+ mode = fatal ? 'failed' : 'reconnecting'
239
+ lastError = versionRes.error ?? versionRes.kind ?? 'version-failed'
240
+ emit('disconnect', { reason: lastError, fatal })
241
+ return { ok: false, code: lastError, status: versionRes.status ?? 0, fatal }
242
+ }
243
+
244
+ const negotiated = negotiateProtocol(versionRes.data?.protocol, limits)
245
+ if (!negotiated.ok) {
246
+ mode = 'failed'
247
+ lastError = negotiated.reason
248
+ emit('disconnect', { reason: negotiated.reason, fatal: true })
249
+ return { ok: false, code: negotiated.reason, protocol: versionRes.data?.protocol }
250
+ }
251
+ protocol = negotiated.protocol
252
+ protocolRange = { min: negotiated.min, max: negotiated.max }
253
+ lastVersionInfo = versionRes.data && typeof versionRes.data === 'object' ? versionRes.data : { protocol: versionRes.data ?? null }
254
+ sessionId = String(versionRes.data?.sessionId ?? '')
255
+
256
+ const snapshot = await remote.state()
257
+ if (!snapshot.ok) {
258
+ const fatal = Number(snapshot.status) === 401
259
+ mode = fatal ? 'failed' : 'reconnecting'
260
+ lastError = snapshot.error ?? snapshot.kind ?? 'state-failed'
261
+ emit('disconnect', { reason: lastError, fatal })
262
+ return { ok: false, code: lastError, status: snapshot.status ?? 0, fatal }
263
+ }
264
+ lastState = snapshot.data ?? null
265
+ if (lastState?.sessionId) sessionId = String(lastState.sessionId)
266
+ if (lastState?.lastCursor !== undefined) lastCursor = String(lastState.lastCursor)
267
+ // 一致性诊断:version 与 state 之间 Adapter 若重启,sessionId 会变化。
268
+ // 此时 state 是最新真相,继续用 state.sessionId 连接;只记录差异供排障(不重试三步)。
269
+ const versionSession = String(lastVersionInfo?.sessionId ?? '')
270
+ const stateSession = String(lastState?.sessionId ?? '')
271
+ sessionMismatch = versionSession && stateSession && versionSession !== stateSession
272
+ ? { versionSession, stateSession, at: Date.now() }
273
+ : null
274
+ emit('state', lastState)
275
+
276
+ const opened = await openOnce()
277
+ if (stopped) return { ok: false, code: 'stopped' }
278
+ if (!opened.ok) {
279
+ if (opened.fatal) {
280
+ mode = 'failed'
281
+ lastError = opened.code
282
+ emit('disconnect', { reason: opened.code, fatal: true })
283
+ return { ok: false, code: opened.code, fatal: true }
284
+ }
285
+ mode = 'reconnecting'
286
+ if (autoReconnect) void reconnectLoop()
287
+ return { ok: false, code: opened.code, retryable: true, sessionId, lastCursor }
288
+ }
289
+
290
+ mode = 'streaming'
291
+ pumpPromise = pump(opened.res).then((result) => {
292
+ if (stopped) return result
293
+ mode = autoReconnect ? 'reconnecting' : 'idle'
294
+ emit('disconnect', { reason: result.code ?? 'stream-closed' })
295
+ if (autoReconnect) void reconnectLoop()
296
+ return result
297
+ })
298
+ return { ok: true, protocol, sessionId, state: lastState, lastCursor, version: lastVersionInfo, sessionMismatch }
299
+ }
300
+
301
+ /** 停止事件流(幂等)。未到期的退避计时器会被立即唤醒,让重连循环看到 stopped 退出。 */
302
+ function stop() {
303
+ stopped = true
304
+ try {
305
+ controller?.abort()
306
+ } catch {
307
+ /* 老运行时没有 abort 也不影响逻辑 */
308
+ }
309
+ controller = null
310
+ for (const [timer, resolve] of pendingWaits) {
311
+ clearTimeout(timer)
312
+ resolve()
313
+ }
314
+ pendingWaits.clear()
315
+ mode = 'idle'
316
+ }
317
+
318
+ /**
319
+ * 幂等命令:生成 commandId,网络/超时失败时用**同一个** commandId 重试一次。
320
+ * 服务端命中 CommandCache 会返回 `deduplicated:true`,不会重复执行副作用。
321
+ */
322
+ async function command(op, payload = {}, { commandId = newCommandId(), async: isAsync = false, retries = 1 } = {}) {
323
+ const envelope = { commandId, op: String(op ?? ''), payload: payload ?? {} }
324
+ if (isAsync) envelope.async = true
325
+ let result = await remote.commands(envelope)
326
+ let attempt = 0
327
+ while (!result.ok && attempt < retries && (result.kind === 'network' || result.kind === 'timeout')) {
328
+ await wait(backoffFor(attempt++))
329
+ result = await remote.commands(envelope)
330
+ }
331
+ return result
332
+ }
333
+
334
+ return {
335
+ ...remote,
336
+ connect,
337
+ stop,
338
+ command,
339
+ onEvent: on('event'),
340
+ onState: on('state'),
341
+ onReconnect: on('reconnect'),
342
+ onDisconnect: on('disconnect'),
343
+ get mode() {
344
+ return mode
345
+ },
346
+ get protocol() {
347
+ return protocol
348
+ },
349
+ get sessionId() {
350
+ return sessionId
351
+ },
352
+ get lastCursor() {
353
+ return lastCursor
354
+ },
355
+ get lastState() {
356
+ return lastState
357
+ },
358
+ /** 最近一次 /adapter/version 的原始响应(protocol / version / sessionId…),供诊断。 */
359
+ get lastVersionInfo() {
360
+ return lastVersionInfo
361
+ },
362
+ /** version 与 state 的 sessionId 不一致时记录(Adapter 在两次请求之间重启过)。 */
363
+ get sessionMismatch() {
364
+ return sessionMismatch
365
+ },
366
+ /** 本客户端实际用于协商的协议区间(min/max),供诊断与上层审计。 */
367
+ get protocolRange() {
368
+ return { ...protocolRange }
369
+ },
370
+ get lastError() {
371
+ return lastError
372
+ },
373
+ get streaming() {
374
+ return mode === 'streaming'
375
+ },
376
+ /** 等待首帧事件流结束(测试/编排器优雅停止时用)。 */
377
+ get pumpDone() {
378
+ return pumpPromise ?? Promise.resolve(null)
379
+ },
380
+ }
381
+ }
382
+
383
+ export const _internals = { positiveMs }
@@ -0,0 +1,243 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Fang contributors — see LICENSE-CORE.
3
+ // dsh-Fang v2.6.1 — Adapter 协议的可共享部分(契约:docs/PROTOCOL-V3.md)。
4
+ //
5
+ // 这里只放**纯函数与纯状态**:cursor 编解码、事件环形缓冲、命令幂等缓存、
6
+ // 协议协商、Bearer 解析、SSE 帧编解码、错误码 → HTTP 状态码。
7
+ // 不 import `node:*`,因此分身侧(adapter.mjs)与母体侧(adapter-client.mjs)
8
+ // 可以共用同一份语义,测试也不需要真实 socket。
9
+ import {
10
+ ADAPTER_ERROR_STATUS,
11
+ ADAPTER_PROTOCOL_MIN,
12
+ ADAPTER_PROTOCOL_VERSION,
13
+ } from '../kernel/types.mjs'
14
+
15
+ /** 事件环形缓冲默认容量:母体断线后能追多久(1000 条 ≈ 数十分钟的常规事件)。 */
16
+ export const DEFAULT_EVENT_CAPACITY = 1000
17
+ /** 命令幂等缓存默认容量(LRU)。 */
18
+ export const DEFAULT_COMMAND_CACHE_SIZE = 500
19
+ /** SSE 心跳间隔:注释帧,不占 cursor(应小于常见代理 60s idle 超时)。 */
20
+ export const SSE_HEARTBEAT_MS = 15000
21
+ /** 母体侧重连退避默认值(毫秒)。 */
22
+ export const RECONNECT_DEFAULTS = Object.freeze({ baseMs: 500, maxMs: 8000 })
23
+
24
+ const MAX_UINT64 = 18446744073709551615n
25
+ const DECIMAL_RE = /^(0|[1-9][0-9]{0,19})$/
26
+ const COMMAND_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/
27
+
28
+ /**
29
+ * cursor 是单调递增 uint64,JSON 里用十进制字符串表示(bigint 不能 JSON 序列化)。
30
+ * @param {unknown} value
31
+ * @returns {bigint|null} 非法输入返回 null(调用方映射 400 cursor-invalid)
32
+ */
33
+ export function parseCursor(value) {
34
+ if (typeof value === 'bigint') return value >= 0n && value <= MAX_UINT64 ? value : null
35
+ if (typeof value === 'number') {
36
+ if (!Number.isSafeInteger(value) || value < 0) return null
37
+ return BigInt(value)
38
+ }
39
+ if (typeof value !== 'string') return null
40
+ const text = value.trim()
41
+ if (!DECIMAL_RE.test(text)) return null
42
+ try {
43
+ const n = BigInt(text)
44
+ return n <= MAX_UINT64 ? n : null
45
+ } catch {
46
+ return null
47
+ }
48
+ }
49
+
50
+ /** @param {bigint|number|string} value @returns {string} */
51
+ export function formatCursor(value) {
52
+ try {
53
+ const n = typeof value === 'bigint' ? value : BigInt(value)
54
+ return n < 0n ? '0' : n.toString()
55
+ } catch {
56
+ return '0'
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 分身侧事件环形缓冲。append 返回完整事件信封;since 支持断线续传。
62
+ *
63
+ * cursor 语义(PROTOCOL-V3 §7):
64
+ * * `since=0` → 从头回放当前缓冲;
65
+ * * `since` 落在缓冲内 → 返回之后的事件,gap=false;
66
+ * * `since` 早于最旧一条 → gap=true(调用方回 410,让母体重新拉快照);
67
+ * * `since` 超前(分身重启 / 伪造)→ gap=true,reason='cursor-ahead'。
68
+ */
69
+ export class EventLog {
70
+ constructor({ capacity = DEFAULT_EVENT_CAPACITY, sessionId = '', now = () => Date.now() } = {}) {
71
+ const size = Math.floor(Number(capacity))
72
+ this.capacity = Number.isFinite(size) && size > 0 ? size : DEFAULT_EVENT_CAPACITY
73
+ this.sessionId = String(sessionId ?? '')
74
+ this.entries = []
75
+ this.next = 1n
76
+ this.now = typeof now === 'function' ? now : () => Date.now()
77
+ }
78
+
79
+ get size() {
80
+ return this.entries.length
81
+ }
82
+
83
+ /** 最后一条事件的 cursor(空缓冲返回 '0')。 */
84
+ get lastCursor() {
85
+ return this.entries.length > 0 ? this.entries[this.entries.length - 1].cursor : '0'
86
+ }
87
+
88
+ /** 下一条事件将拿到的 cursor(不会回退)。 */
89
+ get nextCursor() {
90
+ return formatCursor(this.next)
91
+ }
92
+
93
+ /**
94
+ * 追加一条事件。
95
+ * @param {object} event
96
+ * @returns {{cursor:string, sessionId:string, at:number, event:object}}
97
+ */
98
+ append(event) {
99
+ const cursor = this.next++
100
+ const body = event && typeof event === 'object' ? event : { kind: 'error', code: 'bad-event' }
101
+ const envelope = {
102
+ cursor: formatCursor(cursor),
103
+ sessionId: this.sessionId,
104
+ at: this.now(),
105
+ event: body,
106
+ }
107
+ this.entries.push(envelope)
108
+ if (this.entries.length > this.capacity) {
109
+ this.entries.splice(0, this.entries.length - this.capacity)
110
+ }
111
+ return envelope
112
+ }
113
+
114
+ /**
115
+ * 取 `cursor` 之后的事件。
116
+ * @param {unknown} cursor
117
+ * @returns {{ok:boolean, code?:string, events:object[], gap:boolean, reason?:string,
118
+ * lastCursor:string, nextCursor:string, sessionId:string}}
119
+ */
120
+ since(cursor) {
121
+ const parsed = parseCursor(cursor)
122
+ if (parsed === null) {
123
+ return {
124
+ ok: false,
125
+ code: 'cursor-invalid',
126
+ events: [],
127
+ gap: false,
128
+ lastCursor: this.lastCursor,
129
+ nextCursor: this.nextCursor,
130
+ sessionId: this.sessionId,
131
+ }
132
+ }
133
+ const base = { ok: true, events: [], gap: false, lastCursor: this.lastCursor, nextCursor: this.nextCursor, sessionId: this.sessionId }
134
+ if (this.entries.length === 0) return base
135
+ if (parsed >= this.next) return { ...base, gap: true, reason: 'cursor-ahead' }
136
+ const oldest = parseCursor(this.entries[0].cursor)
137
+ const events = this.entries.filter((entry) => parseCursor(entry.cursor) > parsed)
138
+ return { ...base, events, gap: oldest !== null && parsed + 1n < oldest }
139
+ }
140
+
141
+ clear() {
142
+ this.entries = []
143
+ }
144
+ }
145
+
146
+ // 命令幂等缓存已独立到 `command-cache.mjs`(v2.6.3 起:in-flight 合并 + 失败契约);
147
+ // 这里保留 re-export,避免 Adapter 与旧测试的 import 路径断裂。
148
+ export { CommandCache, COMMAND_CACHE_DEFAULT_CAPACITY } from './command-cache.mjs'
149
+
150
+ /**
151
+ * 版本协商。父体拿到分身 /adapter/version 的 protocol 后调用。
152
+ * @param {unknown} remote
153
+ * @param {{min?:number, max?:number}} [options]
154
+ * @returns {{ok:boolean, protocol?:number, reason?:string, remote?:unknown, min:number, max:number}}
155
+ */
156
+ export function negotiateProtocol(remote, { min = ADAPTER_PROTOCOL_MIN, max = ADAPTER_PROTOCOL_VERSION } = {}) {
157
+ const raw = Number(remote)
158
+ if (!Number.isInteger(raw) || raw < 1) {
159
+ return { ok: false, reason: 'protocol-invalid', remote, min, max }
160
+ }
161
+ if (raw < min) return { ok: false, reason: 'protocol-too-old', protocol: raw, remote: raw, min, max }
162
+ if (raw > max) return { ok: false, reason: 'protocol-too-new', protocol: raw, remote: raw, min, max }
163
+ return { ok: true, protocol: raw, min, max }
164
+ }
165
+
166
+ /** 从 Authorization 头取 Bearer token(大小写不敏感,空值返回 '')。 */
167
+ export function parseBearer(header) {
168
+ const text = String(header ?? '').trim()
169
+ if (!text) return ''
170
+ const match = /^Bearer\s+(\S+)$/i.exec(text)
171
+ return match ? match[1] : ''
172
+ }
173
+
174
+ /**
175
+ * 校验并归一化 v3 命令信封。
176
+ * 接受 `{commandId, op, payload, async}`,也接受嵌套 `{commandId, command:{kind, payload}}`。
177
+ * @returns {{ok:true, commandId:string, op:string, payload:object, async:boolean} |
178
+ * {ok:false, code:'command-id-invalid'|'missing-op', error:string, commandId:string}}
179
+ */
180
+ export function normalizeCommandEnvelope(input) {
181
+ const raw = input && typeof input === 'object' ? input : {}
182
+ const command = raw.command && typeof raw.command === 'object' ? raw.command : null
183
+ const commandId = raw.commandId === undefined || raw.commandId === null ? '' : String(raw.commandId).trim()
184
+ if (commandId && !COMMAND_ID_RE.test(commandId)) {
185
+ return { ok: false, code: 'command-id-invalid', error: 'command-id-invalid', commandId }
186
+ }
187
+ const op = String(raw.op ?? command?.op ?? command?.kind ?? '').trim()
188
+ if (!op) return { ok: false, code: 'missing-op', error: 'missing-op', commandId }
189
+ const payloadRaw = raw.payload ?? command?.payload
190
+ const payload = payloadRaw && typeof payloadRaw === 'object' ? payloadRaw : {}
191
+ const async = raw.async === true || command?.async === true || payload.async === true
192
+ return { ok: true, commandId, op, payload, async }
193
+ }
194
+
195
+ /** 构造失败信封(所有 Adapter 端点共用)。 */
196
+ export function adapterError(code, message, extra = null) {
197
+ const envelope = { ok: false, code: String(code ?? 'internal-error'), error: String(message ?? code ?? 'internal-error') }
198
+ if (extra && typeof extra === 'object') Object.assign(envelope, extra)
199
+ return envelope
200
+ }
201
+
202
+ /** 错误码 → HTTP 状态码(未登记 = 500)。 */
203
+ export function statusForAdapterError(code) {
204
+ return ADAPTER_ERROR_STATUS[String(code ?? '')] ?? 500
205
+ }
206
+
207
+ /**
208
+ * 一条 SSE 帧。data 是事件信封的 JSON(JSON 会转义换行,因此永远是单行)。
209
+ * @param {{cursor:string, sessionId:string, at:number, event:object}} envelope
210
+ * @param {string} [eventName]
211
+ */
212
+ export function sseFrame(envelope, eventName = 'adapter') {
213
+ const data = JSON.stringify(envelope)
214
+ return `id: ${envelope.cursor}\nevent: ${eventName}\ndata: ${data}\n\n`
215
+ }
216
+
217
+ /** SSE 注释帧(心跳/连接确认),不占 cursor。 */
218
+ export function sseComment(text = 'heartbeat') {
219
+ return `: ${text}\n\n`
220
+ }
221
+
222
+ /** 连接建立时建议客户端重连间隔。 */
223
+ export const SSE_RETRY_FRAME = 'retry: 2000\n\n'
224
+
225
+ /**
226
+ * 增量 SSE 解析器(母体侧):把任意 chunk 切片还原成完整帧。
227
+ * v2.6.5 起独立成 `sse-parser.mjs` 的 `SseParser` 类(便于模糊测试);
228
+ * `flush()` 按 SSE 规范丢弃未完成的残帧。
229
+ * @returns {{push:(chunk:unknown)=>object[], flush:()=>object[]}}
230
+ */
231
+ export { SseParser, createSseParser } from './sse-parser.mjs'
232
+
233
+ /** 生成一个命令幂等键(crypto.randomUUID 可用时优先)。 */
234
+ export function newCommandId() {
235
+ try {
236
+ if (globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
237
+ return `cmd_${globalThis.crypto.randomUUID()}`
238
+ }
239
+ } catch {
240
+ /* 老运行时退化到时间戳 + 随机串 */
241
+ }
242
+ return `cmd_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`
243
+ }