@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,408 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Fang contributors — see LICENSE-CORE.
3
+ // dsh-Fang v2.0.0 — 母体侧 Remote 客户端(契约:docs/CONTRACTS-V2.md §5.2、PROTOCOL-V2.md §4)。
4
+ //
5
+ // 母体与分身是两个独立 DSH 进程,控制面只能走分身自己的 HTTP 服务。本模块把
6
+ // 「URL + token + fetch」封成一次函数调用,统一返回 `{ok,status,data|error,kind}`:
7
+ //
8
+ // const remote = createRemote({ url, token, timeoutMs: 8000 })
9
+ // const hello = await remote.hello() // {ok:true, status:200, data:{...}}
10
+ // const read = await remote.command('read-plugin', { pluginId: 'plug' })
11
+ //
12
+ // 硬约束:
13
+ // * 纯模块:零 `node:*` 依赖,只用 Web 标准(URL/AbortController/encodeURIComponent);
14
+ // * 可注入 `fetchImpl`,测试不需要真实 socket;
15
+ // * **永不抛出**:网络错误/超时/非 JSON/非 2xx 全部降级成返回对象;
16
+ // * 超时用 AbortController;`token` 以查询参数形式幂等拼进每个请求。
17
+ import { ADAPTER_OP, ADAPTER_ROUTE } from '../kernel/types.mjs'
18
+
19
+ /** 失败分类词汇(与 CONTRACTS-V2 §5.2 的 kind 一一对应)。 */
20
+ export const REMOTE_FAILURE_KIND = Object.freeze({
21
+ TIMEOUT: 'timeout',
22
+ NETWORK: 'network',
23
+ HTTP: 'http',
24
+ JSON: 'json',
25
+ UNKNOWN: 'unknown',
26
+ })
27
+
28
+ const FAILURE_KINDS = new Set(Object.values(REMOTE_FAILURE_KIND))
29
+
30
+ const msgOf = (e) => String(e && e.message !== undefined ? e.message : e)
31
+
32
+ /** 把 "a b/c" 之类的 token 编码进查询串;任何非字符串都按空串处理。 */
33
+ function encodeToken(token) {
34
+ try {
35
+ return encodeURIComponent(String(token))
36
+ } catch {
37
+ return ''
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 幂等地拼接/替换 `url` 的 `token` 查询参数(契约 §5.2)。
43
+ * - 已有 token 参数 → 原地替换(不会出现两个 token);
44
+ * - 没有 → 追加到查询串末尾;`#hash` 永远保留在最后;
45
+ * - token 为空/省略 → 原样返回(调用方没给 token 时不改动 URL)。
46
+ * @param {string} url
47
+ * @param {string} [token]
48
+ * @returns {string}
49
+ */
50
+ export function withToken(url, token) {
51
+ const base = String(url ?? '')
52
+ if (token === undefined || token === null || token === '') return base
53
+
54
+ const encoded = encodeToken(token)
55
+ if (!encoded) return base
56
+
57
+ const hashAt = base.indexOf('#')
58
+ const beforeHash = hashAt < 0 ? base : base.slice(0, hashAt)
59
+ const hash = hashAt < 0 ? '' : base.slice(hashAt)
60
+
61
+ const queryAt = beforeHash.indexOf('?')
62
+ const pathPart = queryAt < 0 ? beforeHash : beforeHash.slice(0, queryAt)
63
+ const query = queryAt < 0 ? '' : beforeHash.slice(queryAt + 1)
64
+
65
+ const out = []
66
+ let replaced = false
67
+ for (const pair of query === '' ? [] : query.split('&')) {
68
+ if (pair === '') continue
69
+ const eq = pair.indexOf('=')
70
+ const key = eq < 0 ? pair : pair.slice(0, eq)
71
+ if (key === 'token') {
72
+ if (!replaced) {
73
+ out.push(`token=${encoded}`)
74
+ replaced = true
75
+ }
76
+ continue
77
+ }
78
+ out.push(pair)
79
+ }
80
+ if (!replaced) out.push(`token=${encoded}`)
81
+ return `${pathPart}?${out.join('&')}${hash}`
82
+ }
83
+
84
+ /**
85
+ * 取出 URL 里的 token 查询参数(已 decode)。没有/非法时返回 ''。
86
+ * @param {string} url
87
+ * @returns {string}
88
+ */
89
+ export function extractToken(url) {
90
+ const s = String(url ?? '')
91
+ const queryAt = s.indexOf('?')
92
+ if (queryAt < 0) return ''
93
+ const hashAt = s.indexOf('#', queryAt)
94
+ const query = s.slice(queryAt + 1, hashAt < 0 ? undefined : hashAt)
95
+ for (const pair of query.split('&')) {
96
+ if (!pair) continue
97
+ const eq = pair.indexOf('=')
98
+ const key = eq < 0 ? pair : pair.slice(0, eq)
99
+ if (key !== 'token') continue
100
+ const raw = eq < 0 ? '' : pair.slice(eq + 1)
101
+ try {
102
+ return decodeURIComponent(raw)
103
+ } catch {
104
+ return raw
105
+ }
106
+ }
107
+ return ''
108
+ }
109
+
110
+ /** '/fang/'、'fang' → '/fang';空值兜底 '/fang'。 */
111
+ export function normalizePrefix(prefix) {
112
+ const raw = String(prefix ?? '/fang').trim().replace(/\\/g, '/')
113
+ if (!raw) return '/fang'
114
+ const withSlash = raw.startsWith('/') ? raw : `/${raw}`
115
+ const trimmed = withSlash.replace(/\/+$/, '')
116
+ return trimmed || '/fang'
117
+ }
118
+
119
+ /** 把端点路径拼到 base URL 上(丢弃 base 原本的 query/hash,token 由请求阶段重新拼)。 */
120
+ export function joinUrl(base, pathname, prefix = '/fang') {
121
+ const s = String(base ?? '').trim()
122
+ const hashAt = s.indexOf('#')
123
+ let clean = hashAt < 0 ? s : s.slice(0, hashAt)
124
+ const queryAt = clean.indexOf('?')
125
+ if (queryAt >= 0) clean = clean.slice(0, queryAt)
126
+ const root = clean.replace(/\/+$/, '')
127
+ const p = normalizePrefix(prefix)
128
+ const tail = String(pathname ?? '')
129
+ return `${root}${p}${tail.startsWith('/') ? tail : `/${tail}`}`
130
+ }
131
+
132
+ /** 给 URL 追加查询参数(token 之外),idempotent 不重要——每次请求只拼一次。 */
133
+ function withQuery(url, query) {
134
+ const entries = Object.entries(query ?? {}).filter(([, v]) => v !== undefined && v !== null && v !== '')
135
+ if (!entries.length) return url
136
+ const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&')
137
+ return `${url}${url.includes('?') ? '&' : '?'}${qs}`
138
+ }
139
+
140
+ function failure(status, error, kind, extra = null) {
141
+ const out = { ok: false, status: Number(status) || 0, error: String(error ?? 'remote-failed'), kind }
142
+ if (extra && typeof extra === 'object') out.data = extra
143
+ return out
144
+ }
145
+
146
+ /** 错误 → {kind, message};只做分类,不抛。 */
147
+ export function classifyRemoteFailure(err) {
148
+ const message = msgOf(err)
149
+ const explicit = err && typeof err.kind === 'string' ? err.kind : ''
150
+ if (FAILURE_KINDS.has(explicit)) return { kind: explicit, message }
151
+
152
+ const name = String(err?.name ?? '')
153
+ const code = String(err?.code ?? '')
154
+ const status = err?.status ?? err?.statusCode
155
+
156
+ if (name === 'AbortError' || name === 'TimeoutError' || code === 'ABORT_ERR' || code === 'ETIMEDOUT') {
157
+ return { kind: REMOTE_FAILURE_KIND.TIMEOUT, message: message || 'timeout' }
158
+ }
159
+ if (/abort|timed?\s*out|timeout/i.test(message)) {
160
+ return { kind: REMOTE_FAILURE_KIND.TIMEOUT, message: message || 'timeout' }
161
+ }
162
+ if (name === 'SyntaxError' || code === 'JSON_PARSE' || /\bJSON\b|unexpected token/i.test(message)) {
163
+ return { kind: REMOTE_FAILURE_KIND.JSON, message: message || 'invalid json' }
164
+ }
165
+ if (typeof status === 'number' && Number.isFinite(status)) {
166
+ return { kind: REMOTE_FAILURE_KIND.HTTP, message: message || `http-${status}` }
167
+ }
168
+ if (name === 'TypeError' || /fetch failed|network|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|EHOSTUNREACH|socket hang up|UND_ERR/i.test(message)) {
169
+ return { kind: REMOTE_FAILURE_KIND.NETWORK, message: message || 'network-failure' }
170
+ }
171
+ return { kind: REMOTE_FAILURE_KIND.UNKNOWN, message: message || 'unknown-failure' }
172
+ }
173
+
174
+ /** 读响应体:优先 text()(真实 Response 只能读一次),退化到 json();都不行返回 null。 */
175
+ async function readResponseBody(res) {
176
+ if (!res || typeof res !== 'object') return { text: null }
177
+ if (typeof res.text === 'function') {
178
+ try {
179
+ return { text: String(await res.text()) }
180
+ } catch {
181
+ /* 继续尝试 json() */
182
+ }
183
+ }
184
+ if (typeof res.json === 'function') {
185
+ try {
186
+ return { value: await res.json() }
187
+ } catch (e) {
188
+ return { parseError: e }
189
+ }
190
+ }
191
+ return { text: null }
192
+ }
193
+
194
+ function responseIsOk(res, status) {
195
+ if (typeof res?.ok === 'boolean') return res.ok
196
+ return status >= 200 && status < 300
197
+ }
198
+
199
+ function errorFromBody(data, status) {
200
+ if (data && typeof data === 'object') {
201
+ if (typeof data.error === 'string' && data.error) return data.error
202
+ if (typeof data.code === 'string' && data.code) return data.code
203
+ }
204
+ return `http-${status}`
205
+ }
206
+
207
+ /**
208
+ * 创建一个指向某个分身 Adapter 的远端客户端(契约 §5.2)。
209
+ *
210
+ * @param {object} [options]
211
+ * @param {string} [options.url] 分身 web URL(通常来自 `parseWebUrl()`,可自带 `?token=`)
212
+ * @param {string} [options.token] token;省略时从 `url` 中提取
213
+ * @param {string} [options.adapterToken] v3 Bearer token(Forker spawn 时注入;不落 URL)
214
+ * @param {Function} [options.fetchImpl=globalThis.fetch] 可注入的 fetch(测试用)
215
+ * @param {number} [options.timeoutMs=5000] 单次请求超时
216
+ * @param {string} [options.prefix='/fang'] 路由前缀(与 Adapter Config.prefix 一致)
217
+ */
218
+ export function createRemote({
219
+ url,
220
+ token,
221
+ adapterToken,
222
+ fetchImpl = globalThis.fetch,
223
+ timeoutMs = 5000,
224
+ prefix = '/fang',
225
+ } = {}) {
226
+ const targetUrl = String(url ?? '')
227
+ const effectiveToken = token === undefined || token === null ? extractToken(targetUrl) : String(token)
228
+ const effectiveAdapterToken = adapterToken === undefined || adapterToken === null ? '' : String(adapterToken)
229
+ const requestTimeoutMs = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
230
+ ? Math.floor(Number(timeoutMs))
231
+ : 5000
232
+ const routePrefix = normalizePrefix(prefix)
233
+ const impl = typeof fetchImpl === 'function' ? fetchImpl : null
234
+
235
+ /** v3 Bearer 头;没有 token 时不发头(v2 兼容模式)。 */
236
+ const authHeaders = () => (effectiveAdapterToken ? { authorization: `Bearer ${effectiveAdapterToken}` } : {})
237
+
238
+ /**
239
+ * 发出一次请求并归一化返回;任何情况下都 resolve,不 reject。
240
+ * @returns {Promise<{ok:boolean,status:number,data?:any,error?:string,kind?:string}>}
241
+ */
242
+ async function request(method, pathname, { query = null, body = undefined } = {}) {
243
+ if (!impl) return failure(0, 'no-fetch', REMOTE_FAILURE_KIND.NETWORK)
244
+
245
+ // v3.0.5/B4:token 只走 Authorization header,绝不再拼进 URL(防日志/历史/Referer 泄漏)。
246
+ const target = withQuery(joinUrl(targetUrl, pathname, routePrefix), query)
247
+
248
+ const controller = new AbortController()
249
+ let timer = null
250
+ try {
251
+ if (Number.isFinite(requestTimeoutMs) && requestTimeoutMs > 0) {
252
+ timer = setTimeout(() => {
253
+ try {
254
+ controller.abort()
255
+ } catch {
256
+ /* 老运行时没有 AbortController.abort 也不影响分类 */
257
+ }
258
+ }, requestTimeoutMs)
259
+ }
260
+
261
+ const init = { method, signal: controller.signal }
262
+ const headers = authHeaders()
263
+ if (body !== undefined) {
264
+ init.headers = { ...headers, 'content-type': 'application/json; charset=utf-8' }
265
+ try {
266
+ init.body = JSON.stringify(body)
267
+ } catch (e) {
268
+ return failure(0, `serialize-failed: ${msgOf(e)}`, REMOTE_FAILURE_KIND.UNKNOWN)
269
+ }
270
+ } else if (Object.keys(headers).length > 0) {
271
+ init.headers = headers
272
+ }
273
+
274
+ let res
275
+ try {
276
+ res = await impl(target, init)
277
+ } catch (e) {
278
+ const classified = classifyRemoteFailure(e)
279
+ return failure(0, classified.message, classified.kind)
280
+ }
281
+
282
+ const status = Number(res?.status)
283
+ const okFlag = responseIsOk(res, status)
284
+ const { text, value, parseError } = await readResponseBody(res)
285
+ let data = null
286
+ let parseOk = true
287
+ if (value !== undefined) {
288
+ data = value
289
+ } else if (parseError !== undefined) {
290
+ parseOk = false
291
+ } else if (text !== null) {
292
+ if (String(text).trim() === '') data = null
293
+ else {
294
+ try {
295
+ data = JSON.parse(text)
296
+ } catch {
297
+ parseOk = false
298
+ }
299
+ }
300
+ }
301
+
302
+ if (!okFlag) {
303
+ // HTTP 失败优先按 http 归类(body 是否 JSON 不影响语义),但把可读 body 带上便于诊断。
304
+ return failure(status, errorFromBody(data, status), REMOTE_FAILURE_KIND.HTTP, data)
305
+ }
306
+ if (!parseOk) return failure(status, 'invalid-json', REMOTE_FAILURE_KIND.JSON)
307
+ return { ok: true, status, data }
308
+ } finally {
309
+ if (timer !== null) clearTimeout(timer)
310
+ }
311
+ }
312
+
313
+ const remote = {
314
+ url: targetUrl,
315
+ token: effectiveToken,
316
+ prefix: routePrefix,
317
+ /** v3 Bearer token(母体内存持有;为空 = v2 兼容模式)。 */
318
+ adapterToken: effectiveAdapterToken,
319
+
320
+ /** 握手:GET /fang/adapter/hello → {ok, status, data:{ok,role,taskId,version,capabilities}} */
321
+ hello() {
322
+ return request('GET', ADAPTER_ROUTE.HELLO)
323
+ },
324
+
325
+ /** 分身内服务表/缓冲区/待审批/审计头:GET /fang/adapter/status */
326
+ status() {
327
+ return request('GET', ADAPTER_ROUTE.STATUS)
328
+ },
329
+
330
+ /** 分身本地待审批项:GET /fang/adapter/approvals */
331
+ approvals() {
332
+ return request('GET', ADAPTER_ROUTE.APPROVALS)
333
+ },
334
+
335
+ /** 分身审计尾部:GET /fang/adapter/audit?limit=n(默认 100) */
336
+ audit({ limit } = {}) {
337
+ const raw = Number(limit)
338
+ const n = Number.isFinite(raw) ? Math.max(0, Math.floor(raw)) : 100
339
+ return request('GET', ADAPTER_ROUTE.AUDIT, { query: { limit: n } })
340
+ },
341
+
342
+ /** 控制命令:POST /fang/adapter/command {op, payload} */
343
+ command(op, payload = {}) {
344
+ const body = op !== null && typeof op === 'object'
345
+ ? { op: String(op.op ?? ''), payload: op.payload ?? {} }
346
+ : { op: String(op ?? ''), payload: payload ?? {} }
347
+ return request('POST', ADAPTER_ROUTE.COMMAND, { body })
348
+ },
349
+
350
+ /** 控制面心跳:等价于 command('ping'),返回 {ok,status,data:{ok,op,result:{pong:true}}} */
351
+ ping() {
352
+ return request('POST', ADAPTER_ROUTE.COMMAND, { body: { op: ADAPTER_OP.PING, payload: {} } })
353
+ },
354
+
355
+ // ── v3(PROTOCOL-V3 §4):协议协商 / 健康 / 快照 / 幂等命令 ──────────────
356
+
357
+ /** 协议版本协商:GET /fang/adapter/version */
358
+ version() {
359
+ return request('GET', ADAPTER_ROUTE.VERSION)
360
+ },
361
+
362
+ /** 轻量健康检查:GET /fang/adapter/health */
363
+ health() {
364
+ return request('GET', ADAPTER_ROUTE.HEALTH)
365
+ },
366
+
367
+ /** 完整状态快照:GET /fang/adapter/state */
368
+ state() {
369
+ return request('GET', ADAPTER_ROUTE.STATE)
370
+ },
371
+
372
+ /**
373
+ * 幂等命令:POST /fang/adapter/commands
374
+ * @param {{commandId?:string, op?:string, payload?:object, async?:boolean}} envelope
375
+ */
376
+ commands(envelope = {}) {
377
+ const body = envelope && typeof envelope === 'object' ? { ...envelope } : {}
378
+ return request('POST', ADAPTER_ROUTE.COMMANDS, { body })
379
+ },
380
+
381
+ /**
382
+ * 拼出 SSE 订阅 URL(含 DSH ?token= 与 since/session 查询参数)。
383
+ * 实际流读取在 `adapter-client.mjs`;这里只保证 URL 规则与其它请求一致。
384
+ */
385
+ eventsUrl({ since = '0', session = '' } = {}) {
386
+ const base = withToken(joinUrl(targetUrl, ADAPTER_ROUTE.EVENTS, routePrefix), effectiveToken)
387
+ const query = []
388
+ if (since !== undefined && since !== null && since !== '') query.push(`since=${encodeURIComponent(String(since))}`)
389
+ if (session) query.push(`session=${encodeURIComponent(String(session))}`)
390
+ return query.length > 0 ? `${base}${base.includes('?') ? '&' : '?'}${query.join('&')}` : base
391
+ },
392
+
393
+ /** v3 Bearer 头(adapter-client 建 SSE 连接时复用)。 */
394
+ authHeaders,
395
+ }
396
+
397
+ return remote
398
+ }
399
+
400
+ export const _internals = {
401
+ joinUrl,
402
+ withQuery,
403
+ normalizePrefix,
404
+ readResponseBody,
405
+ responseIsOk,
406
+ errorFromBody,
407
+ requestTimeoutMs: (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Math.floor(Number(v)) : 5000),
408
+ }