@twinsearth/fang-dsh-plugin 0.0.0-stage → 3.3.5
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.
- package/CLA.md +79 -0
- package/LICENSE-COMMERCIAL +78 -0
- package/LICENSE-CORE +661 -0
- package/LICENSE-SDK +21 -0
- package/LICENSES/cordis-MIT.txt +39 -0
- package/NOTICE +95 -0
- package/README.md +158 -2
- package/client.js +7287 -0
- package/cordis.patch.yml +163 -0
- package/defaults/umbrella-rules.yaml +113 -0
- package/docs/APPROVAL-DRILL.md +42 -0
- package/docs/CONTRACT-INTEROP.md +77 -0
- package/docs/CRASH-SAFETY.md +69 -0
- package/docs/DECISION-RELAY.md +46 -0
- package/docs/DSH-ARCHITECTURE-MAP.md +73 -0
- package/docs/FORK-SEED.md +64 -0
- package/docs/MAIN-CITY-GATE.md +58 -0
- package/docs/OVERLAY-MASTER-SWITCH.md +35 -0
- package/docs/PACKAGE-RENAME-v3.3.3.md +40 -0
- package/docs/PM-DECISION-POLICY.md +61 -0
- package/docs/WORKFLOW-UI.md +86 -0
- package/icon.svg +8 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +71 -4
- package/sdk/README.md +65 -0
- package/sdk/examples/status-badge/README.md +16 -0
- package/sdk/examples/status-badge/client.js +60 -0
- package/sdk/examples/status-badge/cordis.patch.yml +7 -0
- package/sdk/examples/status-badge/index.js +12 -0
- package/sdk/examples/status-badge/package.json +24 -0
- package/sdk/templates/plugin/README.md +36 -0
- package/sdk/templates/plugin/client.js +35 -0
- package/sdk/templates/plugin/cordis.patch.yml +8 -0
- package/sdk/templates/plugin/icon.svg +5 -0
- package/sdk/templates/plugin/index.js +23 -0
- package/sdk/templates/plugin/locale/en.json +6 -0
- package/sdk/templates/plugin/package.json +36 -0
- package/src/compose.mjs +67 -0
- package/src/contract/VENDOR.json +16 -0
- package/src/contract/client.js +93 -0
- package/src/contract/constants.js +94 -0
- package/src/contract/envelope.js +83 -0
- package/src/contract/index.js +20 -0
- package/src/contract/negotiate.js +106 -0
- package/src/contract/relay.js +223 -0
- package/src/contract/server.js +77 -0
- package/src/contract/transports.js +319 -0
- package/src/index.mjs +11 -0
- package/src/kernel/core.mjs +100 -0
- package/src/kernel/extensions.mjs +61 -0
- package/src/kernel/fsx.mjs +349 -0
- package/src/kernel/index.mjs +21 -0
- package/src/kernel/ledger.mjs +94 -0
- package/src/kernel/paths.mjs +182 -0
- package/src/kernel/role.mjs +68 -0
- package/src/kernel/rounds.mjs +172 -0
- package/src/kernel/schema.mjs +5 -0
- package/src/kernel/types.mjs +266 -0
- package/src/kernel/vendor/cosmokit.mjs +421 -0
- package/src/kernel/vendor/schemastery.mjs +638 -0
- package/src/lib/adapter-client.mjs +383 -0
- package/src/lib/adapter-protocol.mjs +243 -0
- package/src/lib/ai-secret-store.mjs +202 -0
- package/src/lib/approval-drill.mjs +111 -0
- package/src/lib/boot-guard.mjs +186 -0
- package/src/lib/branch-store.mjs +84 -0
- package/src/lib/branch.mjs +74 -0
- package/src/lib/bridge-client.mjs +133 -0
- package/src/lib/broker-adapter.mjs +67 -0
- package/src/lib/broker.mjs +45 -0
- package/src/lib/business-producers.mjs +109 -0
- package/src/lib/command-cache.mjs +209 -0
- package/src/lib/compat.mjs +170 -0
- package/src/lib/composer-journal.mjs +128 -0
- package/src/lib/container/index.mjs +806 -0
- package/src/lib/decision-events.mjs +129 -0
- package/src/lib/decision-ingest-client.mjs +41 -0
- package/src/lib/decision-outbox.mjs +129 -0
- package/src/lib/decision-policy.mjs +52 -0
- package/src/lib/default-essence.mjs +61 -0
- package/src/lib/desktop-launch.mjs +138 -0
- package/src/lib/essence.mjs +1211 -0
- package/src/lib/fork-client.mjs +410 -0
- package/src/lib/fork-config-lock.mjs +166 -0
- package/src/lib/fork-seed.mjs +304 -0
- package/src/lib/fork-sessions.mjs +183 -0
- package/src/lib/fork-supervision-settings.mjs +58 -0
- package/src/lib/fork-supervisor.mjs +122 -0
- package/src/lib/fs-guard.mjs +28 -0
- package/src/lib/http-auth.mjs +162 -0
- package/src/lib/idle-audit.mjs +178 -0
- package/src/lib/interaction-poke.mjs +61 -0
- package/src/lib/main-gate.mjs +582 -0
- package/src/lib/mirror-etag.mjs +92 -0
- package/src/lib/mirror-official-source.mjs +93 -0
- package/src/lib/mirror.mjs +478 -0
- package/src/lib/mother-link.mjs +156 -0
- package/src/lib/mutex.mjs +31 -0
- package/src/lib/permissions.mjs +115 -0
- package/src/lib/pnpm-store.mjs +189 -0
- package/src/lib/preferences.mjs +212 -0
- package/src/lib/producer-contract.mjs +162 -0
- package/src/lib/reflow.mjs +41 -0
- package/src/lib/remote.mjs +408 -0
- package/src/lib/restart-contract.mjs +752 -0
- package/src/lib/roots.mjs +30 -0
- package/src/lib/rule-registry.mjs +1127 -0
- package/src/lib/runtime-capture.mjs +307 -0
- package/src/lib/semaphore.mjs +34 -0
- package/src/lib/session-bundle.mjs +138 -0
- package/src/lib/session-format.mjs +672 -0
- package/src/lib/sse-parser.mjs +102 -0
- package/src/lib/ssrf-guard.mjs +170 -0
- package/src/lib/status-store.mjs +257 -0
- package/src/lib/task-handle.mjs +622 -0
- package/src/lib/ui-actions.mjs +55 -0
- package/src/lib/umbrella.mjs +1661 -0
- package/src/lib/upgrade-coordinator.mjs +107 -0
- package/src/lib/upgrade-status.mjs +43 -0
- package/src/plugins/adapter.mjs +1468 -0
- package/src/plugins/approver.mjs +1210 -0
- package/src/plugins/auditor.mjs +330 -0
- package/src/plugins/buffer-keeper.mjs +522 -0
- package/src/plugins/decision-center.mjs +2581 -0
- package/src/plugins/forker.mjs +1610 -0
- package/src/plugins/healer.mjs +702 -0
- package/src/plugins/hot-reloader.mjs +274 -0
- package/src/plugins/imports.mjs +247 -0
- package/src/plugins/orchestrator.mjs +3979 -0
- package/src/plugins/restart-guard.mjs +1022 -0
- package/src/plugins/router.mjs +714 -0
- package/src/plugins/scorer.mjs +661 -0
- package/src/plugins/session-bridge.mjs +166 -0
- package/src/plugins/session-catalog.mjs +771 -0
- package/src/plugins/session-router.mjs +125 -0
- package/src/plugins/session-sync.mjs +171 -0
- package/src/plugins/umbrella.mjs +419 -0
- package/tools/approval-drill.mjs +21 -0
- package/tools/boot-guard.mjs +95 -0
- package/tools/decision-ingest.mjs +53 -0
- package/tools/desktop-launch.mjs +71 -0
- package/tools/fangctl.mjs +425 -0
- package/tools/gate/main-city.mjs +204 -0
- package/tools/mcp/server.mjs +121 -0
- package/tools/mcp/tools.mjs +65 -0
- 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
|
+
}
|