@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.
- 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/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/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 +579 -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 +3938 -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/boot-guard.mjs +95 -0
- package/tools/decision-ingest.mjs +53 -0
- package/tools/desktop-launch.mjs +71 -0
- package/tools/fangctl.mjs +408 -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,8 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Bundle patch for the plugin template: one loader row inserted at install time.
|
|
3
|
+
# Rename the id / package name when you copy this template.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: my-plugin
|
|
6
|
+
name: '@your-scope/dsh-plugin-template'
|
|
7
|
+
config:
|
|
8
|
+
greeting: 'hello from my plugin'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="my-plugin">
|
|
2
|
+
<rect x="4" y="4" width="56" height="56" rx="12" fill="#0f172a"/>
|
|
3
|
+
<circle cx="32" cy="30" r="12" fill="none" stroke="#22c55e" stroke-width="4"/>
|
|
4
|
+
<path d="M20 48h24" stroke="#94a3b8" stroke-width="4" stroke-linecap="round"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// dsh-Fang / DSH plugin template — Host half.
|
|
3
|
+
//
|
|
4
|
+
// A Cordis plugin module: export name / inject / (optional Config) / apply.
|
|
5
|
+
// Copy this file, change the names, and install the directory as a bundle:
|
|
6
|
+
// plugin_manager install_bundle(target="E:\\path\\to\\my-plugin")
|
|
7
|
+
//
|
|
8
|
+
// The optional `Config` two lines below needs a schema library. If you want a configurable
|
|
9
|
+
// plugin, copy `schemastery.mjs` + `cosmokit.mjs` (MIT, upstream cordis) into a `vendor/`
|
|
10
|
+
// folder next to this file and `export const Config = z.object({...})`, exactly like the
|
|
11
|
+
// dsh-Fang core does; otherwise a plugin with no Config is perfectly valid.
|
|
12
|
+
export const name = 'my-plugin'
|
|
13
|
+
export const inject = []
|
|
14
|
+
|
|
15
|
+
const DEFAULTS = { greeting: 'hello from my plugin' }
|
|
16
|
+
|
|
17
|
+
export function apply(ctx, config) {
|
|
18
|
+
const cfg = { ...DEFAULTS, ...(config ?? {}) }
|
|
19
|
+
ctx.effect(() => ctx.provide('myPlugin', {
|
|
20
|
+
greeting: () => cfg.greeting,
|
|
21
|
+
status: () => ({ ok: true, at: new Date().toISOString() }),
|
|
22
|
+
}))
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@your-scope/dsh-plugin-template",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Minimal dsh-Fang / DSH plugin template (MIT): host half + optional client half.",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": {
|
|
10
|
+
"browser": "./client.js",
|
|
11
|
+
"default": "./client.js"
|
|
12
|
+
},
|
|
13
|
+
"./locale/*.json": "./locale/*.json",
|
|
14
|
+
"./icon": "./icon.svg"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"index.js",
|
|
18
|
+
"client.js",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"locale/*.json",
|
|
21
|
+
"icon.svg"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dsh": {
|
|
25
|
+
"bundle": {
|
|
26
|
+
"patch": "./cordis.patch.yml"
|
|
27
|
+
},
|
|
28
|
+
"client": {
|
|
29
|
+
"platform": "web",
|
|
30
|
+
"immediately": true,
|
|
31
|
+
"inject": [
|
|
32
|
+
"@deepseek-ai/dsh-client-connection"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/compose.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang contributors — see LICENSE-CORE.
|
|
3
|
+
// dsh-Fang compose mode — mounts the orchestrator and every sub-plugin as nested
|
|
4
|
+
// fibers of one loader row. This is the fallback composition used when the split-row
|
|
5
|
+
// patch (`./cordis.patch.yml`) cannot resolve subpath entries; it is behaviourally
|
|
6
|
+
// identical because every sub-plugin is the same module the split rows would load.
|
|
7
|
+
import { z } from './kernel/index.mjs'
|
|
8
|
+
import * as auditor from './plugins/auditor.mjs'
|
|
9
|
+
import * as forker from './plugins/forker.mjs'
|
|
10
|
+
import * as bufferKeeper from './plugins/buffer-keeper.mjs'
|
|
11
|
+
import * as scorer from './plugins/scorer.mjs'
|
|
12
|
+
import * as umbrella from './plugins/umbrella.mjs'
|
|
13
|
+
import * as healer from './plugins/healer.mjs'
|
|
14
|
+
import * as router from './plugins/router.mjs'
|
|
15
|
+
import * as approver from './plugins/approver.mjs'
|
|
16
|
+
import * as hotReloader from './plugins/hot-reloader.mjs'
|
|
17
|
+
import * as restartGuard from './plugins/restart-guard.mjs'
|
|
18
|
+
import * as adapter from './plugins/adapter.mjs'
|
|
19
|
+
import * as orchestrator from './plugins/orchestrator.mjs'
|
|
20
|
+
import * as sessionBridge from './plugins/session-bridge.mjs'
|
|
21
|
+
import * as sessionSync from './plugins/session-sync.mjs'
|
|
22
|
+
import * as sessionRouter from './plugins/session-router.mjs'
|
|
23
|
+
import * as decisionCenter from './plugins/decision-center.mjs'
|
|
24
|
+
|
|
25
|
+
export const name = 'fang-compose'
|
|
26
|
+
export const inject = []
|
|
27
|
+
|
|
28
|
+
export const Config = z.object({
|
|
29
|
+
auditor: z.object({}).default({}).description('审计子插件配置'),
|
|
30
|
+
forker: z.object({}).default({}).description('派生器配置'),
|
|
31
|
+
bufferKeeper: z.object({}).default({}).description('缓冲管理器配置'),
|
|
32
|
+
scorer: z.object({}).default({}).description('评分配置'),
|
|
33
|
+
umbrella: z.object({}).default({}).description('保护伞(动作灯色 + 决策)配置'),
|
|
34
|
+
healer: z.object({}).default({}).description('修复子插件配置'),
|
|
35
|
+
router: z.object({}).default({}).description('路由决策配置'),
|
|
36
|
+
approver: z.object({}).default({}).description('审批子插件配置'),
|
|
37
|
+
hotReloader: z.object({}).default({}).description('热更新引擎配置'),
|
|
38
|
+
restartGuard: z.object({}).default({}).description('红线守卫配置'),
|
|
39
|
+
adapter: z.object({}).default({}).description('分身侧 Adapter 配置'),
|
|
40
|
+
orchestrator: z.object({}).default({}).description('编排器配置'),
|
|
41
|
+
sessionBridge: z.object({}).default({}).description('桥接(实时驱动 PC 会话)配置'),
|
|
42
|
+
sessionSync: z.object({}).default({}).description('同步(勾选会话搬运)配置'),
|
|
43
|
+
sessionRouter: z.object({}).default({}).description('混合路由配置'),
|
|
44
|
+
decisionCenter: z.object({}).default({}).description('决策中心配置'),
|
|
45
|
+
}).description('dsh-Fang 单行装载模式配置')
|
|
46
|
+
|
|
47
|
+
export function apply(ctx, config) {
|
|
48
|
+
const cfg = config ?? {}
|
|
49
|
+
ctx.plugin(auditor, cfg.auditor ?? {})
|
|
50
|
+
ctx.plugin(forker, cfg.forker ?? {})
|
|
51
|
+
ctx.plugin(bufferKeeper, cfg.bufferKeeper ?? {})
|
|
52
|
+
ctx.plugin(scorer, cfg.scorer ?? {})
|
|
53
|
+
ctx.plugin(umbrella, cfg.umbrella ?? {})
|
|
54
|
+
ctx.plugin(healer, cfg.healer ?? {})
|
|
55
|
+
ctx.plugin(router, cfg.router ?? {})
|
|
56
|
+
ctx.plugin(approver, cfg.approver ?? {})
|
|
57
|
+
ctx.plugin(hotReloader, cfg.hotReloader ?? {})
|
|
58
|
+
ctx.plugin(restartGuard, cfg.restartGuard ?? {})
|
|
59
|
+
ctx.plugin(adapter, cfg.adapter ?? {})
|
|
60
|
+
ctx.plugin(orchestrator, cfg.orchestrator ?? {})
|
|
61
|
+
ctx.plugin(sessionBridge, cfg.sessionBridge ?? {})
|
|
62
|
+
ctx.plugin(sessionSync, cfg.sessionSync ?? {})
|
|
63
|
+
ctx.plugin(sessionRouter, cfg.sessionRouter ?? {})
|
|
64
|
+
ctx.plugin(decisionCenter, cfg.decisionCenter ?? {})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const _modules = { auditor, forker, bufferKeeper, scorer, umbrella, healer, router, approver, hotReloader, restartGuard, adapter, orchestrator, sessionBridge, sessionSync, sessionRouter, decisionCenter }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "@twinsearth/dsh-fang-contract",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"syncedAt": "2026-11-13T10:20:51.087Z",
|
|
5
|
+
"fileCount": 8,
|
|
6
|
+
"sha256": {
|
|
7
|
+
"client.js": "b1c48a56c64e7058f40a6f1e3239f9e81619381c121fc3d2e1f5418b6e8c6b26",
|
|
8
|
+
"constants.js": "ffb1924b79bc742a69c592ad2f3d6e943edc98a81cf6adebc73162832e7ccdbc",
|
|
9
|
+
"envelope.js": "68e8725a4b7d5c01e49b1364a201121278d3d45dac6e99dc0ee53f98232d380a",
|
|
10
|
+
"index.js": "67b5d687d4f8c47fd4d573fe5540362014ed0617e7df3533936e613a439b655c",
|
|
11
|
+
"negotiate.js": "8a699b287b740a7d478c40981c0ae7915a6522ff856f3bc0b19f0cc077587c43",
|
|
12
|
+
"relay.js": "63e86b65ff865db4c189e906bd44a771c0a712ddcddff88bdaae7e55534748ce",
|
|
13
|
+
"server.js": "5e7c884639fc365833f6fdca91f276c34db2b1d1a6938790c7e9b71c127e6c17",
|
|
14
|
+
"transports.js": "6712aabbe702ae559ff50027a3469c0ce031092e6b0a61fec51cfe80b69d0a6c"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_TRANSPORT_ORDER, METHODS } from './constants.js'
|
|
5
|
+
import {
|
|
6
|
+
createFileTransport,
|
|
7
|
+
createHttpTransport,
|
|
8
|
+
createLegacyTransport,
|
|
9
|
+
createLocalTransport,
|
|
10
|
+
createTransportRouter,
|
|
11
|
+
} from './transports.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 客户端门面:一次配置,按 local → http → file → legacy 自动降级。
|
|
15
|
+
*
|
|
16
|
+
* @param {{
|
|
17
|
+
* ctx?: object,
|
|
18
|
+
* config?: {httpBaseUrl?:string, httpToken?:string, fileDir?:string, order?:string[], timeoutMs?:number},
|
|
19
|
+
* fetchImpl?: typeof fetch,
|
|
20
|
+
* logger?: object,
|
|
21
|
+
* instanceId?: string,
|
|
22
|
+
* }} options
|
|
23
|
+
*/
|
|
24
|
+
export function createContractClient(options = {}) {
|
|
25
|
+
const { ctx = null, config = {}, fetchImpl = fetch, logger = null, instanceId = 'contract-client' } = options
|
|
26
|
+
const transports = []
|
|
27
|
+
if (ctx) transports.push(createLocalTransport({ ctx }))
|
|
28
|
+
if (config.httpBaseUrl) transports.push(createHttpTransport({
|
|
29
|
+
baseUrl: config.httpBaseUrl,
|
|
30
|
+
token: config.httpToken,
|
|
31
|
+
headers: config.httpHeaders,
|
|
32
|
+
fetchImpl,
|
|
33
|
+
timeoutMs: config.timeoutMs,
|
|
34
|
+
pathPrefix: config.pathPrefix,
|
|
35
|
+
}))
|
|
36
|
+
if (config.fileDir) transports.push(createFileTransport({
|
|
37
|
+
dir: config.fileDir,
|
|
38
|
+
pollMs: config.filePollMs,
|
|
39
|
+
timeoutMs: config.fileTimeoutMs,
|
|
40
|
+
}))
|
|
41
|
+
if (ctx) transports.push(createLegacyTransport({ ctx }))
|
|
42
|
+
|
|
43
|
+
const order = Array.isArray(config.order) && config.order.length > 0 ? config.order : DEFAULT_TRANSPORT_ORDER
|
|
44
|
+
const router = createTransportRouter({
|
|
45
|
+
transports,
|
|
46
|
+
order,
|
|
47
|
+
localHello: null,
|
|
48
|
+
logger,
|
|
49
|
+
})
|
|
50
|
+
let lastHello = null
|
|
51
|
+
let helloPromise = null
|
|
52
|
+
|
|
53
|
+
const client = {
|
|
54
|
+
instanceId,
|
|
55
|
+
transports: () => router.transports(),
|
|
56
|
+
describe: () => ({
|
|
57
|
+
instanceId,
|
|
58
|
+
routes: router.describe(),
|
|
59
|
+
hello: lastHello,
|
|
60
|
+
}),
|
|
61
|
+
attempts: () => router.attempts(),
|
|
62
|
+
async hello(force = false) {
|
|
63
|
+
if (!force && lastHello) return lastHello
|
|
64
|
+
if (!force && helloPromise) return helloPromise
|
|
65
|
+
helloPromise = (async () => {
|
|
66
|
+
const result = await router.hello()
|
|
67
|
+
lastHello = result
|
|
68
|
+
return result
|
|
69
|
+
})().finally(() => { helloPromise = null })
|
|
70
|
+
return helloPromise
|
|
71
|
+
},
|
|
72
|
+
async invoke(method, params, invokeOptions = {}) {
|
|
73
|
+
const result = await router.invoke(method, params, { timeoutMs: config.timeoutMs, ...invokeOptions })
|
|
74
|
+
return result
|
|
75
|
+
},
|
|
76
|
+
async health() { return await client.invoke(METHODS.HEALTH, null) },
|
|
77
|
+
async spawnFork(params, invokeOptions = {}) { return await client.invoke(METHODS.FORK_SPAWN, params, invokeOptions) },
|
|
78
|
+
async stopFork(params, invokeOptions = {}) { return await client.invoke(METHODS.FORK_STOP, params, invokeOptions) },
|
|
79
|
+
async listForks(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.FORK_LIST, params, invokeOptions) },
|
|
80
|
+
async createDecision(params, invokeOptions = {}) { return await client.invoke(METHODS.DECISION_CREATE, params, invokeOptions) },
|
|
81
|
+
async resolveDecision(params, invokeOptions = {}) { return await client.invoke(METHODS.DECISION_RESOLVE, params, invokeOptions) },
|
|
82
|
+
async listSessions(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.SESSION_LIST, params, invokeOptions) },
|
|
83
|
+
async orchestratorStatus(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.ORCHESTRATOR_STATUS, params, invokeOptions) },
|
|
84
|
+
/** contract 1.1:批量投递回流事件(按 event.id 幂等;返回回执与游标)。 */
|
|
85
|
+
async publishRelay(events, invokeOptions = {}) {
|
|
86
|
+
const params = Array.isArray(events) ? { events } : (events && typeof events === 'object' ? events : { events: [] })
|
|
87
|
+
return await client.invoke(METHODS.RELAY_PUBLISH, params, invokeOptions)
|
|
88
|
+
},
|
|
89
|
+
/** contract 1.1:读取对端回流状态(对端自报 peer/cursor/累计计数)。 */
|
|
90
|
+
async relayStatus(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.RELAY_STATUS, params, invokeOptions) },
|
|
91
|
+
}
|
|
92
|
+
return client
|
|
93
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 契约常量与兼容规则(contract SDK 1.x)。
|
|
6
|
+
*
|
|
7
|
+
* 稳定承诺:
|
|
8
|
+
* * MAJOR 相同的版本保证“旧字段可读、旧方法可调、未知字段忽略”;
|
|
9
|
+
* * 新增能力只通过 METHODS/CAPABILITIES 扩展,不改变既有方法语义;
|
|
10
|
+
* * 删除/改义必须升 MAJOR,并保留至少一个 MINOR 周期的双写(method@1 与 method@2)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const CONTRACT_PACKAGE = '@twinsearth/dsh-fang-contract'
|
|
14
|
+
export const CONTRACT_VERSION = '1.1.0'
|
|
15
|
+
export const PROTOCOL = 'fang-master'
|
|
16
|
+
export const PROTOCOL_VERSION = 1
|
|
17
|
+
export const MIN_COMPATIBLE_MAJOR = 1
|
|
18
|
+
|
|
19
|
+
/** 稳定方法名(新方法只能追加)。 */
|
|
20
|
+
export const METHODS = Object.freeze({
|
|
21
|
+
HELLO: 'contract.hello',
|
|
22
|
+
HEALTH: 'contract.health',
|
|
23
|
+
FORK_SPAWN: 'fang.fork.spawn',
|
|
24
|
+
FORK_STOP: 'fang.fork.stop',
|
|
25
|
+
FORK_LIST: 'fang.fork.list',
|
|
26
|
+
DECISION_CREATE: 'fang.decision.create',
|
|
27
|
+
DECISION_RESOLVE: 'fang.decision.resolve',
|
|
28
|
+
SESSION_LIST: 'fang.session.list',
|
|
29
|
+
ORCHESTRATOR_STATUS: 'fang.orchestrator.status',
|
|
30
|
+
RELAY_PUBLISH: 'fang.relay.publish',
|
|
31
|
+
RELAY_STATUS: 'fang.relay.status',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/** 方法 → 契约版本(用于协商后的方法裁剪)。 */
|
|
35
|
+
export const METHOD_VERSION = Object.freeze({
|
|
36
|
+
[METHODS.HELLO]: '1.0',
|
|
37
|
+
[METHODS.HEALTH]: '1.0',
|
|
38
|
+
[METHODS.FORK_SPAWN]: '1.0',
|
|
39
|
+
[METHODS.FORK_STOP]: '1.0',
|
|
40
|
+
[METHODS.FORK_LIST]: '1.0',
|
|
41
|
+
[METHODS.DECISION_CREATE]: '1.0',
|
|
42
|
+
[METHODS.DECISION_RESOLVE]: '1.0',
|
|
43
|
+
[METHODS.SESSION_LIST]: '1.0',
|
|
44
|
+
[METHODS.ORCHESTRATOR_STATUS]: '1.0',
|
|
45
|
+
[METHODS.RELAY_PUBLISH]: '1.1',
|
|
46
|
+
[METHODS.RELAY_STATUS]: '1.1',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const CAPABILITIES = Object.freeze([
|
|
50
|
+
'sync', 'async', 'idempotent', 'stream-events', 'file-outbox', 'event-relay',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
export const ROLES = Object.freeze(['mother', 'essence', 'fork'])
|
|
54
|
+
|
|
55
|
+
export const ERROR_CODES = Object.freeze({
|
|
56
|
+
INVALID_ENVELOPE: 'invalid-envelope',
|
|
57
|
+
UNSUPPORTED_METHOD: 'unsupported-method',
|
|
58
|
+
UNSUPPORTED_VERSION: 'unsupported-version',
|
|
59
|
+
INCOMPATIBLE: 'contract-incompatible',
|
|
60
|
+
TRANSPORT_UNAVAILABLE: 'transport-unavailable',
|
|
61
|
+
TRANSPORT_TIMEOUT: 'transport-timeout',
|
|
62
|
+
TRANSPORT_HTTP_ERROR: 'transport-http-error',
|
|
63
|
+
REMOTE_ERROR: 'remote-error',
|
|
64
|
+
ALL_TRANSPORTS_FAILED: 'all-transports-failed',
|
|
65
|
+
HANDLER_THREW: 'handler-threw',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 兜底顺序(从快到慢):
|
|
70
|
+
* local —— 同一 Host 的 DSH 服务(零网络、首选)
|
|
71
|
+
* http —— 母体/原身 HTTP 契约端点
|
|
72
|
+
* file —— 同一文件系统的 outbox/inbox(HTTP 不可用/被防火墙拦)
|
|
73
|
+
* legacy —— 直接调用 3.1.x 的旧 DSH 服务键(接口包被替换/新端点不存在时)
|
|
74
|
+
*/
|
|
75
|
+
export const DEFAULT_TRANSPORT_ORDER = Object.freeze(['local', 'http', 'file', 'legacy'])
|
|
76
|
+
|
|
77
|
+
/** 3.1.x 及更早的旧服务键(只读兼容;不新增语义)。 */
|
|
78
|
+
export const LEGACY_SERVICE_KEYS = Object.freeze({
|
|
79
|
+
forker: 'fangForker',
|
|
80
|
+
decisionCenter: 'fangDecisionCenter',
|
|
81
|
+
sessionCatalog: 'fangSessionCatalog',
|
|
82
|
+
orchestrator: 'fang',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 回流接口的硬上限(1.1)。超限是**协议错误**而不是静默截断:
|
|
87
|
+
* 调用方必须自己分批,服务端不得悄悄丢事件(回流要可对账)。
|
|
88
|
+
*/
|
|
89
|
+
export const RELAY_LIMITS = Object.freeze({
|
|
90
|
+
maxBatch: 128,
|
|
91
|
+
maxEventBytes: 32_000,
|
|
92
|
+
maxPayloadBytes: 512_000,
|
|
93
|
+
maxRecords: 5_000,
|
|
94
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
|
|
3
|
+
|
|
4
|
+
import crypto from 'node:crypto'
|
|
5
|
+
|
|
6
|
+
import { ERROR_CODES, PROTOCOL, PROTOCOL_VERSION } from './constants.js'
|
|
7
|
+
|
|
8
|
+
export function newId(prefix = 'msg') {
|
|
9
|
+
return `${prefix}_${crypto.randomUUID()}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** 创建请求信封;未知字段由接收方忽略(兼容规则)。 */
|
|
13
|
+
export function createEnvelope({ method, params = null, id, source = '', instanceId = '', timeoutMs = 30_000, meta = {} } = {}) {
|
|
14
|
+
if (!method || typeof method !== 'string') throw new Error('method is required')
|
|
15
|
+
return {
|
|
16
|
+
protocol: PROTOCOL,
|
|
17
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
18
|
+
id: String(id ?? newId('req')),
|
|
19
|
+
method: String(method),
|
|
20
|
+
params: params ?? null,
|
|
21
|
+
meta: {
|
|
22
|
+
at: Date.now(),
|
|
23
|
+
deadline: Date.now() + Math.max(1, Number(timeoutMs) || 30_000),
|
|
24
|
+
source: String(source ?? ''),
|
|
25
|
+
instanceId: String(instanceId ?? ''),
|
|
26
|
+
...(meta && typeof meta === 'object' ? meta : {}),
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 校验请求/响应信封;返回 {ok, errors, envelope}。 */
|
|
32
|
+
export function validateEnvelope(value, { response = false } = {}) {
|
|
33
|
+
const errors = []
|
|
34
|
+
const raw = value && typeof value === 'object' ? value : {}
|
|
35
|
+
if (raw.protocol !== PROTOCOL) errors.push(`protocol-invalid:${String(raw.protocol)}`)
|
|
36
|
+
if (raw.protocolVersion !== PROTOCOL_VERSION) errors.push(`protocol-version-invalid:${String(raw.protocolVersion)}`)
|
|
37
|
+
if (!raw.id) errors.push('id-required')
|
|
38
|
+
if (response) {
|
|
39
|
+
if (typeof raw.ok !== 'boolean') errors.push('ok-required')
|
|
40
|
+
if (raw.ok === false && !raw.error?.code) errors.push('error-code-required')
|
|
41
|
+
} else if (!raw.method) {
|
|
42
|
+
errors.push('method-required')
|
|
43
|
+
}
|
|
44
|
+
return { ok: errors.length === 0, errors, envelope: raw }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createResponse({ id, result, error, meta = {} } = {}) {
|
|
48
|
+
const base = {
|
|
49
|
+
protocol: PROTOCOL,
|
|
50
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
51
|
+
id: String(id ?? newId('res')),
|
|
52
|
+
ok: !error,
|
|
53
|
+
meta: { at: Date.now(), ...(meta && typeof meta === 'object' ? meta : {}) },
|
|
54
|
+
}
|
|
55
|
+
if (error) return { ...base, error: normalizeError(error) }
|
|
56
|
+
return { ...base, result: result ?? null }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizeError(error, fallbackCode = ERROR_CODES.REMOTE_ERROR) {
|
|
60
|
+
if (!error) return { code: fallbackCode, message: String(fallbackCode), retryable: false }
|
|
61
|
+
if (typeof error === 'string') return { code: fallbackCode, message: error, retryable: false }
|
|
62
|
+
return {
|
|
63
|
+
code: String(error.code ?? fallbackCode),
|
|
64
|
+
message: String(error.message ?? error.error ?? fallbackCode),
|
|
65
|
+
retryable: error.retryable === true,
|
|
66
|
+
...(error.detail === undefined ? {} : { detail: error.detail }),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ContractTransportError extends Error {
|
|
71
|
+
constructor(code, message, { transport = '', detail = null, retryable = false } = {}) {
|
|
72
|
+
super(String(message ?? code))
|
|
73
|
+
this.name = 'ContractTransportError'
|
|
74
|
+
this.code = String(code)
|
|
75
|
+
this.transport = String(transport)
|
|
76
|
+
this.detail = detail
|
|
77
|
+
this.retryable = retryable === true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isCompatibleResponse(value) {
|
|
82
|
+
return validateEnvelope(value, { response: true }).ok
|
|
83
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @twinsearth/dsh-fang-contract
|
|
6
|
+
*
|
|
7
|
+
* 瓮城(dsh-Fang)↔ 房主(dsh-Master)的独立版本化互联 SDK。
|
|
8
|
+
* 设计目标:
|
|
9
|
+
* 1. 双方独立升级:语义化版本 + hello 协商 + 未知字段忽略 + 方法版本裁剪;
|
|
10
|
+
* 2. 互联互通保底:local → http → file → legacy 四级传输自动降级;
|
|
11
|
+
* 3. 接口包可独立替换:更新本包即升级接口架构,旧方法在 MAJOR 内保持可调用。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export * from './constants.js'
|
|
15
|
+
export * from './envelope.js'
|
|
16
|
+
export * from './negotiate.js'
|
|
17
|
+
export * from './server.js'
|
|
18
|
+
export * from './relay.js'
|
|
19
|
+
export * from './transports.js'
|
|
20
|
+
export * from './client.js'
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CAPABILITIES,
|
|
6
|
+
CONTRACT_VERSION,
|
|
7
|
+
ERROR_CODES,
|
|
8
|
+
METHOD_VERSION,
|
|
9
|
+
METHODS,
|
|
10
|
+
MIN_COMPATIBLE_MAJOR,
|
|
11
|
+
PROTOCOL,
|
|
12
|
+
ROLES,
|
|
13
|
+
} from './constants.js'
|
|
14
|
+
|
|
15
|
+
export function parseVersion(value) {
|
|
16
|
+
const text = String(value ?? '').trim()
|
|
17
|
+
const match = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(text)
|
|
18
|
+
if (!match) return null
|
|
19
|
+
return {
|
|
20
|
+
major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3] ?? 0), text,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function compareVersions(a, b) {
|
|
25
|
+
const pa = parseVersion(a)
|
|
26
|
+
const pb = parseVersion(b)
|
|
27
|
+
if (!pa || !pb) return 0
|
|
28
|
+
if (pa.major !== pb.major) return pa.major - pb.major
|
|
29
|
+
if (pa.minor !== pb.minor) return pa.minor - pb.minor
|
|
30
|
+
return pa.patch - pb.patch
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 标准 hello payload(服务端暴露;客户端据此协商)。 */
|
|
34
|
+
export function helloPayload({
|
|
35
|
+
role = 'mother',
|
|
36
|
+
instanceId = '',
|
|
37
|
+
version = CONTRACT_VERSION,
|
|
38
|
+
methods = Object.values(METHODS),
|
|
39
|
+
capabilities = CAPABILITIES,
|
|
40
|
+
transports = [],
|
|
41
|
+
startedAt = Date.now(),
|
|
42
|
+
} = {}) {
|
|
43
|
+
return {
|
|
44
|
+
protocol: PROTOCOL,
|
|
45
|
+
contractVersion: String(version),
|
|
46
|
+
role: ROLES.includes(String(role)) ? String(role) : 'fork',
|
|
47
|
+
instanceId: String(instanceId ?? ''),
|
|
48
|
+
startedAt: Number(startedAt) || Date.now(),
|
|
49
|
+
methods: [...new Set(methods.map(String))],
|
|
50
|
+
capabilities: [...new Set(capabilities.map(String))],
|
|
51
|
+
transports: [...new Set(transports.map(String))],
|
|
52
|
+
features: {
|
|
53
|
+
idempotency: true,
|
|
54
|
+
envelopes: true,
|
|
55
|
+
fileOutbox: true,
|
|
56
|
+
versionNegotiation: true,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 双向版本协商。
|
|
63
|
+
* @param {object} local 本端 helloPayload
|
|
64
|
+
* @param {object} remote 对端 helloPayload
|
|
65
|
+
* @returns {{ok:boolean, compatible:boolean, fallback:'legacy'|null, reason?:string, version?:string, methods:string[], capabilities:string[], features:object}}
|
|
66
|
+
*/
|
|
67
|
+
export function negotiate(local, remote) {
|
|
68
|
+
const lv = parseVersion(local?.contractVersion)
|
|
69
|
+
const rv = parseVersion(remote?.contractVersion)
|
|
70
|
+
if (local?.protocol !== PROTOCOL || remote?.protocol !== PROTOCOL) {
|
|
71
|
+
return { ok: false, compatible: false, fallback: 'legacy', reason: 'protocol-mismatch', methods: [], capabilities: [], features: {} }
|
|
72
|
+
}
|
|
73
|
+
if (!lv || !rv) {
|
|
74
|
+
return { ok: false, compatible: false, fallback: 'legacy', reason: 'version-invalid', methods: [], capabilities: [], features: {} }
|
|
75
|
+
}
|
|
76
|
+
if (rv.major < MIN_COMPATIBLE_MAJOR) {
|
|
77
|
+
return { ok: false, compatible: false, fallback: 'legacy', reason: 'remote-too-old', methods: [], capabilities: [], features: {} }
|
|
78
|
+
}
|
|
79
|
+
if (rv.major > lv.major) {
|
|
80
|
+
return { ok: false, compatible: false, fallback: 'legacy', reason: 'remote-too-new', methods: [], capabilities: [], features: {} }
|
|
81
|
+
}
|
|
82
|
+
const localMethods = new Set(Array.isArray(local.methods) ? local.methods : [])
|
|
83
|
+
const methods = (Array.isArray(remote.methods) ? remote.methods : []).filter((method) => localMethods.has(method))
|
|
84
|
+
// 裁剪:方法所需契约 minor 不能高于双方共同支持的 minor。
|
|
85
|
+
const commonMinor = Math.min(lv.minor, rv.minor)
|
|
86
|
+
const supported = methods.filter((method) => {
|
|
87
|
+
const methodVersion = parseVersion(METHOD_VERSION[method] ?? '1.0')
|
|
88
|
+
return !methodVersion || methodVersion.minor <= commonMinor || methodVersion.major < lv.major
|
|
89
|
+
})
|
|
90
|
+
const localCaps = new Set(Array.isArray(local.capabilities) ? local.capabilities : [])
|
|
91
|
+
const capabilities = (Array.isArray(remote.capabilities) ? remote.capabilities : []).filter((cap) => localCaps.has(cap))
|
|
92
|
+
const version = `${Math.min(lv.major, rv.major)}.${commonMinor}.0`
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
compatible: true,
|
|
96
|
+
fallback: null,
|
|
97
|
+
version,
|
|
98
|
+
methods: supported,
|
|
99
|
+
capabilities,
|
|
100
|
+
features: { ...(local.features ?? {}), ...(remote.features ?? {}) },
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function unsupportedMethodError(method) {
|
|
105
|
+
return { code: ERROR_CODES.UNSUPPORTED_METHOD, message: `unsupported method: ${String(method)}`, retryable: false }
|
|
106
|
+
}
|