@twinsearth/dsh-master 0.0.0-stage → 3.2.9
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/LICENSE +661 -0
- package/README.md +93 -2
- package/client.js +334 -0
- package/cordis.patch.yml +71 -0
- package/dist-host/contract/client.js +82 -0
- package/dist-host/contract/constants.js +70 -0
- package/dist-host/contract/envelope.js +84 -0
- package/dist-host/contract/index.js +17 -0
- package/dist-host/contract/negotiate.js +87 -0
- package/dist-host/contract/server.js +71 -0
- package/dist-host/contract/transports.js +360 -0
- package/dist-host/index.js +260 -0
- package/dist-host/kernel.js +186 -0
- package/dist-host/plugins/cost-optimizer.js +147 -0
- package/dist-host/plugins/dual-manager.js +251 -0
- package/dist-host/plugins/superagent-router.js +499 -0
- package/dist-host/plugins/superagent-worker.js +73 -0
- package/dist-host/superagent/bridge-tokens.js +236 -0
- package/dist-host/superagent/dispatch.js +198 -0
- package/dist-host/superagent/feishu.js +115 -0
- package/dist-host/superagent/github.js +119 -0
- package/dist-host/superagent/pr-delivery.js +351 -0
- package/dist-host/superagent/router.js +219 -0
- package/dist-host/superagent/stream-adapter.js +238 -0
- package/dist-host/superagent/stream-tokens.js +141 -0
- package/dist-host/superagent/ticket-store.js +108 -0
- package/dist-host/superagent/trust.js +145 -0
- package/dist-host/superagent/types.js +144 -0
- package/dist-host/superagent/worker.js +95 -0
- package/dist-host/superagent/writeback.js +162 -0
- package/dist-worker/agent-loop.js +493 -0
- package/dist-worker/errors.js +41 -0
- package/dist-worker/github-pr.js +44 -0
- package/dist-worker/index.js +323 -0
- package/dist-worker/llm-http.js +351 -0
- package/dist-worker/main.js +45 -0
- package/dist-worker/prompt.js +35 -0
- package/dist-worker/sink-http.js +135 -0
- package/dist-worker/tools.js +542 -0
- package/dist-worker/types.js +14 -0
- package/docs/ARCHITECTURE.md +114 -0
- package/docs/CHANGELOG.md +389 -0
- package/docs/INTEROP.md +60 -0
- package/docs/RELEASE-v3.2.8.md +27 -0
- package/docs/RELEASE-v3.2.9.md +29 -0
- package/docs/REVIEW-v3.2.7.md +121 -0
- package/docs/ROADMAP.md +106 -0
- package/docs/SECURITY.md +153 -0
- package/docs/SMOKE-3.2.5.md +77 -0
- package/docs/SMOKE-3.2.7.md +73 -0
- package/docs/SUPERAGENT.md +141 -0
- package/docs/WORKER-AGENT-LOOP.md +139 -0
- package/icon.svg +9 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +79 -4
- package/src/contract/client.js +86 -0
- package/src/contract/constants.js +79 -0
- package/src/contract/envelope.js +83 -0
- package/src/contract/index.js +19 -0
- package/src/contract/negotiate.js +106 -0
- package/src/contract/server.js +77 -0
- package/src/contract/transports.js +319 -0
- package/src/index.ts +265 -0
- package/src/kernel.ts +190 -0
- package/src/plugins/cost-optimizer.ts +155 -0
- package/src/plugins/dual-manager.ts +252 -0
- package/src/plugins/superagent-router.ts +465 -0
- package/src/plugins/superagent-worker.ts +85 -0
- package/src/superagent/bridge-tokens.ts +236 -0
- package/src/superagent/dispatch.ts +197 -0
- package/src/superagent/feishu.ts +104 -0
- package/src/superagent/github.ts +111 -0
- package/src/superagent/pr-delivery.ts +308 -0
- package/src/superagent/router.ts +201 -0
- package/src/superagent/stream-adapter.ts +184 -0
- package/src/superagent/stream-tokens.ts +142 -0
- package/src/superagent/ticket-store.ts +104 -0
- package/src/superagent/trust.ts +139 -0
- package/src/superagent/types.ts +135 -0
- package/src/superagent/worker.ts +100 -0
- package/src/superagent/writeback.ts +154 -0
- package/tsconfig.host.build.json +12 -0
- package/tsconfig.host.json +21 -0
- package/tsconfig.worker.build.json +13 -0
- package/tsconfig.worker.json +17 -0
- package/types/superagent.d.ts +99 -0
- package/worker/agent-loop.ts +524 -0
- package/worker/errors.ts +47 -0
- package/worker/github-pr.ts +65 -0
- package/worker/index.ts +334 -0
- package/worker/llm-http.ts +359 -0
- package/worker/main.ts +45 -0
- package/worker/prompt.ts +39 -0
- package/worker/sink-http.ts +159 -0
- package/worker/tools.ts +531 -0
- package/worker/types.ts +247 -0
package/README.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-Master(房主)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 与瓮城 **dsh-Fang** 并行的主 Agent 插件。
|
|
4
|
+
> 当前版本:`3.2.9`(面板交互修复线;tag / GitHub Release / NPM public)。
|
|
5
|
+
> 仓库:https://github.com/TwinsEarth/dsh-Master
|
|
6
|
+
|
|
7
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
8
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
9
|
+
|
|
10
|
+
## 这是什么
|
|
11
|
+
|
|
12
|
+
dsh-Master 不接管瓮城的「母体 / 原身 / 分身」生命周期;它是一块**并列的主插件**,
|
|
13
|
+
在自己的名字空间里托管 Agent 子插件,并通过 DSH 服务键与瓮城协作:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
DSH Host
|
|
17
|
+
├── 瓮城 bundle (dsh-Fang) 房主 bundle (dsh-Master)
|
|
18
|
+
│ mirror / essence / derived core(主 Agent 控制面)
|
|
19
|
+
│ fangForker / fangDecisionCenter ◄────── ├── superagent-router (essence)
|
|
20
|
+
│ fangSessionCatalog ├── superagent-worker (fork)
|
|
21
|
+
│ ├── cost-optimizer (fork)
|
|
22
|
+
│ └── dual-manager (mother/essence)
|
|
23
|
+
└── 协作只走服务键;任一方向失败一律 fail-closed,不本地降级执行
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 子插件
|
|
27
|
+
|
|
28
|
+
| 子插件 | 行 id | 允许角色 | 路线图 | 已交付 |
|
|
29
|
+
|---|---|---|---|---|
|
|
30
|
+
| SuperAgent Router | `master-superagent-router` | **essence only** | 3.2.1/3.2.3 | Webhook/Ticket/Mandate、渐进信任、本地或母体桥派发、Worker 实时事件、回写 |
|
|
31
|
+
| SuperAgent Worker | `master-superagent-worker` | **fork only** | 3.2.1 | Ticket 上下文、Mandate 守卫 |
|
|
32
|
+
| Worker Agent Loop | `master-superagent-loop` | **fork only** | 3.2.3 | tsc 编译 preset;有 Ticket 自动跑多轮工具循环并流式回传 |
|
|
33
|
+
| 成本优化 HarnessMM | `master-cost-optimizer` | **fork only** | 3.3.0 | observe 层:token 估算、实测记录 |
|
|
34
|
+
| 双主管 PM + PJM | `master-dual-manager` | **mother / essence** | 3.4.0 | PRD/计划/调度/决策中心澄清 |
|
|
35
|
+
|
|
36
|
+
## 本地开发
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install # devDeps: typescript + @types/node(仅类型检查/构建用)
|
|
40
|
+
npm test # node:test 全量(含 worker/*.ts 测试)
|
|
41
|
+
npm run typecheck:worker # tsc strict 类型检查
|
|
42
|
+
npm run build:worker # tsc 构建 dist-worker/(DSH 实际加载的 preset)
|
|
43
|
+
node dist-worker/main.js # CLI(需 DSH_MASTER_TICKET + LLM/流式配置)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
仓库运行态零依赖。运行时全部加载**编译后的 JS**:
|
|
47
|
+
|
|
48
|
+
- 房主 Host/插件:`src/**/*.ts` → `npm run build:host` → `dist-host/`;
|
|
49
|
+
- Worker preset:`worker/**/*.ts` → `npm run build:worker` → `dist-worker/`;
|
|
50
|
+
- 浏览器侧 `client.js` 保持 JS(DSH client module 运行时加载);
|
|
51
|
+
- 对外类型:`types/superagent.d.ts`;
|
|
52
|
+
- `npm test` 直接由 Node 26 类型擦除执行 TS 源码/测试;production 用 dist JS。
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm run typecheck:host && npm run build:host
|
|
56
|
+
npm run typecheck:worker && npm run build:worker
|
|
57
|
+
npm run build # 两者一起
|
|
58
|
+
dsh plugin add <package> # 加载 dist-host/dist-worker
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 安装(审核通过后)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 目前请勿安装/发布;审核通过后:
|
|
65
|
+
# dsh plugin add @twinsearth/dsh-master (发布后)
|
|
66
|
+
# 或以本地 bundle 目录安装
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
安装后:
|
|
70
|
+
|
|
71
|
+
- 插件目录多出 **房主(dsh-Master)** + 五个子插件行(Router / Worker / Loop / HarnessMM / PM+PJM);
|
|
72
|
+
- 侧栏底部出现「◈ 房主」入口,打开只读状态抽屉(角色/子插件矩阵/瓮城协作状态);
|
|
73
|
+
- 角色边界在代码里强制:Router 只在 essence、Worker/成本只 fork、双主管只 mother/essence;
|
|
74
|
+
- SuperAgent 默认 **observe**,不会自动接单。
|
|
75
|
+
|
|
76
|
+
## 文档
|
|
77
|
+
|
|
78
|
+
- [房主 ↔ 瓮城 互联契约](docs/INTEROP.md)
|
|
79
|
+
- [真实 Host 集成 smoke(3.2.7)](docs/SMOKE-3.2.7.md)
|
|
80
|
+
- [v3.2.7 全面审计与实机复盘](docs/REVIEW-v3.2.7.md)
|
|
81
|
+
- [Worker Agent Loop](docs/WORKER-AGENT-LOOP.md)
|
|
82
|
+
- [SuperAgent v3.2.1 设计与契约](docs/SUPERAGENT.md)
|
|
83
|
+
- [架构设计](docs/ARCHITECTURE.md)
|
|
84
|
+
- [路线图(3.2.0–3.4.0)](docs/ROADMAP.md)
|
|
85
|
+
- [安全边界](docs/SECURITY.md)
|
|
86
|
+
- [变更记录](docs/CHANGELOG.md)
|
|
87
|
+
|
|
88
|
+
## 状态
|
|
89
|
+
|
|
90
|
+
- 3.2.9 发布线:房主面板改注册到 `shell.overlay` 常驻浮层——点击面板之外不收起、侧栏「房主」按钮二次点击收起、正文独立滚动;新增无浏览器 hook 运行时行为测试(开关、无 outside-click 监听、滚动容器、SSE 失败不卸载面板)。
|
|
91
|
+
- 3.2.8 发布线:与瓮城通过 fang-master contract 1.0 互联(local/http/file/legacy 四级保底);GitHub Release + NPM public。
|
|
92
|
+
- 当前工作树完成新一轮审计修复:工具名/死循环阈值、Router 鉴权与生命周期、真实路由分流、契约幂等与超时、PR 护栏与交付、飞书加密、长 Shell 后台作业、成本实测接线、PJM 派生契约。
|
|
93
|
+
- 验收:`npm run check` / `npm run build` / `npm test` 全绿(188/188);真实双 Host/fork 落库、工具写读、SSE、回写、配额与桥清理断言通过。详见复盘文档。
|
|
94
|
+
- 发布不等于安装:本流程不修改任何用户 profile(当前 desktop profile 仍为 3.2.8,升级需另行授权)。
|
package/client.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dsh-Master(房主)Web Client:侧栏「房主」入口 + 只读状态面板。
|
|
6
|
+
*
|
|
7
|
+
* 只做观察面板(3.2.0 架构骨架):展示角色、三个子插件的激活状态、与瓮城的
|
|
8
|
+
* 服务协作状态。数据来自 core 的只读路由 `/dsh-master/api/status`。
|
|
9
|
+
* 不写任何业务状态,不接管宿主根节点。
|
|
10
|
+
*
|
|
11
|
+
* 面板与按钮分离(3.2.9):按钮注册在 `sidebar.footer.action`,面板注册在
|
|
12
|
+
* `shell.overlay`(DSH 的整框浮层,位于所有列与滚动容器之外,且本身
|
|
13
|
+
* click-through)。两者通过模块级 store 共享「是否展开」:
|
|
14
|
+
* - 点击页面其他位置不会关闭面板(浮层没有 outside-click 收回逻辑);
|
|
15
|
+
* - 再次点击「房主」按钮由 `togglePanel()` 收起;
|
|
16
|
+
* - 面板固定 viewport 高度,正文 `overflow-y: auto`,长内容可滚动查看。
|
|
17
|
+
*/
|
|
18
|
+
window.__ModuleLoader__.load({
|
|
19
|
+
id: '@twinsearth/dsh-master',
|
|
20
|
+
factory(require) {
|
|
21
|
+
const React = require('react')
|
|
22
|
+
const h = React.createElement
|
|
23
|
+
|
|
24
|
+
const S = {
|
|
25
|
+
panel: {
|
|
26
|
+
position: 'fixed', top: 0, right: 0, bottom: 0, width: 'min(380px, 92vw)',
|
|
27
|
+
zIndex: 2147482800, background: '#fff', borderLeft: '1px solid rgba(16,17,20,.12)',
|
|
28
|
+
boxShadow: '-16px 0 40px rgba(16,17,20,.16)', display: 'flex', flexDirection: 'column',
|
|
29
|
+
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif',
|
|
30
|
+
fontSize: 13, color: '#1D1D1F',
|
|
31
|
+
// shell.overlay 整层是 click-through:面板自己opt-in接收指针事件,
|
|
32
|
+
// 面板之外的点击穿透到下面的应用,不会触发任何收回。
|
|
33
|
+
pointerEvents: 'auto',
|
|
34
|
+
},
|
|
35
|
+
head: {
|
|
36
|
+
flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 8, padding: '14px 16px',
|
|
37
|
+
borderBottom: '1px solid rgba(16,17,20,.08)',
|
|
38
|
+
},
|
|
39
|
+
title: { fontSize: 14, fontWeight: 700 },
|
|
40
|
+
sub: { fontSize: 11.5, color: '#86868B' },
|
|
41
|
+
close: { marginLeft: 'auto', width: 28, height: 28, borderRadius: 8, border: '1px solid rgba(16,17,20,.12)', background: '#fff', color: '#5F6368', cursor: 'pointer' },
|
|
42
|
+
body: {
|
|
43
|
+
flex: '1 1 auto', minHeight: 0, overflowY: 'auto', overscrollBehavior: 'contain',
|
|
44
|
+
scrollbarGutter: 'stable', scrollbarWidth: 'thin',
|
|
45
|
+
padding: '12px 14px 18px', display: 'flex', flexDirection: 'column', gap: 10,
|
|
46
|
+
},
|
|
47
|
+
card: { border: '1px solid rgba(16,17,20,.1)', borderRadius: 12, padding: '10px 11px', background: '#FAFAFC', display: 'flex', flexDirection: 'column', gap: 4 },
|
|
48
|
+
row: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
|
|
49
|
+
name: { fontSize: 13, fontWeight: 600 },
|
|
50
|
+
tag: { display: 'inline-flex', alignItems: 'center', height: 18, padding: '0 6px', borderRadius: 6, fontSize: 10.5, fontWeight: 700, background: '#F2F2F5', color: '#5F6368' },
|
|
51
|
+
desc: { fontSize: 12, color: '#5F6368' },
|
|
52
|
+
ok: { color: '#17803D', fontWeight: 600 },
|
|
53
|
+
off: { color: '#B45309', fontWeight: 600 },
|
|
54
|
+
error: { fontSize: 12, color: '#C8102E' },
|
|
55
|
+
fab: {
|
|
56
|
+
display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, padding: '0 9px',
|
|
57
|
+
borderRadius: 8, border: '1px solid rgba(16,17,20,.12)', background: '#fff', color: '#1D1D1F',
|
|
58
|
+
cursor: 'pointer', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit',
|
|
59
|
+
},
|
|
60
|
+
fabOpen: {
|
|
61
|
+
display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, padding: '0 9px',
|
|
62
|
+
borderRadius: 8, border: '1px solid rgba(16,17,20,.24)', background: '#F2F2F5', color: '#1D1D1F',
|
|
63
|
+
cursor: 'pointer', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit',
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 面板展开状态:按钮(sidebar.footer.action)与面板(shell.overlay)是两个
|
|
69
|
+
* 独立注册的组件,状态必须放在模块级 store 里,不能放在任一组件内部。
|
|
70
|
+
*/
|
|
71
|
+
const panelState = { open: false }
|
|
72
|
+
const panelListeners = new Set()
|
|
73
|
+
const getPanelOpen = () => panelState.open
|
|
74
|
+
const setPanelOpen = (next) => {
|
|
75
|
+
const value = Boolean(next)
|
|
76
|
+
if (panelState.open === value) return
|
|
77
|
+
panelState.open = value
|
|
78
|
+
for (const listener of panelListeners) {
|
|
79
|
+
try { listener() } catch { /* 订阅者异常不影响其他订阅者 */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const togglePanel = () => setPanelOpen(!panelState.open)
|
|
83
|
+
const subscribePanel = (listener) => {
|
|
84
|
+
panelListeners.add(listener)
|
|
85
|
+
return () => { panelListeners.delete(listener) }
|
|
86
|
+
}
|
|
87
|
+
function usePanelOpen() {
|
|
88
|
+
const [open, setOpen] = React.useState(getPanelOpen)
|
|
89
|
+
React.useEffect(() => subscribePanel(() => setOpen(getPanelOpen())), [])
|
|
90
|
+
return open
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const tag = (text, color) => h('span', { style: { ...S.tag, ...(color ? { color, background: 'rgba(16,17,20,.05)' } : {}) } }, text)
|
|
94
|
+
|
|
95
|
+
function SubpluginCard({ item }) {
|
|
96
|
+
const color = item.active ? S.ok : S.off
|
|
97
|
+
return h('div', { style: S.card },
|
|
98
|
+
h('div', { style: S.row },
|
|
99
|
+
h('span', { style: S.name }, item.name),
|
|
100
|
+
tag(item.nameEn),
|
|
101
|
+
tag('roadmap ' + item.roadmap),
|
|
102
|
+
h('span', { style: { marginLeft: 'auto', fontSize: 11.5, ...color } }, item.active ? '已激活' : '未激活')),
|
|
103
|
+
h('div', { style: S.desc }, item.description),
|
|
104
|
+
h('div', { style: S.desc },
|
|
105
|
+
'允许角色:' + (item.allowedRoles || []).join(' / '),
|
|
106
|
+
item.blockedReason ? ' · ' + item.blockedReason : ''),
|
|
107
|
+
item.capabilities && item.capabilities.length > 0
|
|
108
|
+
? h('div', { style: S.desc }, '能力:' + item.capabilities.join(' · '))
|
|
109
|
+
: null,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function FangCard({ cooperation }) {
|
|
114
|
+
if (!cooperation) return null
|
|
115
|
+
const entries = Object.entries(cooperation.services || {})
|
|
116
|
+
return h('div', { style: S.card },
|
|
117
|
+
h('div', { style: S.row },
|
|
118
|
+
h('span', { style: S.name }, '与瓮城协作'),
|
|
119
|
+
tag(cooperation.contract || 'dsh-fang'),
|
|
120
|
+
h('span', { style: { marginLeft: 'auto', fontSize: 11.5, ...(cooperation.available ? S.ok : S.off) } },
|
|
121
|
+
cooperation.available ? '在线' : '未发现')),
|
|
122
|
+
entries.length === 0
|
|
123
|
+
? h('div', { style: S.desc }, '未发现瓮城服务;房主子插件会 fail-closed。')
|
|
124
|
+
: entries.map(([alias, info]) => h('div', { key: alias, style: S.desc },
|
|
125
|
+
alias + ' → ' + info.service + ' ' + (info.available ? '✓' : '×'))),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** v3.2.7:实时事件流(SSE)+ token 轮换;只读,不写宿主状态。 */
|
|
130
|
+
function LiveFeed() {
|
|
131
|
+
const [events, setEvents] = React.useState([])
|
|
132
|
+
const [routerStatus, setRouterStatus] = React.useState(null)
|
|
133
|
+
const [error, setError] = React.useState('')
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
let alive = true
|
|
136
|
+
let source = null
|
|
137
|
+
let rotateTimer = null
|
|
138
|
+
let currentToken = ''
|
|
139
|
+
const append = (event) => {
|
|
140
|
+
setEvents((prev) => prev.concat([event]).slice(-20))
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* v3.2.7:上游可能把多个 delta 合并成一批(fork 启动期阻塞会把 SSE 报文在
|
|
144
|
+
* socket 里积压后一次排空)。直接 append 会让卡片"跳变"一次;
|
|
145
|
+
* 这里把 stream 增量排进节拍队列,按 ~70ms 逐个上屏,保证视觉上逐字流动。
|
|
146
|
+
*/
|
|
147
|
+
const deltaQueue = []
|
|
148
|
+
let pacing = null
|
|
149
|
+
const paceStreamDeltas = () => {
|
|
150
|
+
if (pacing || deltaQueue.length === 0) return
|
|
151
|
+
pacing = setTimeout(() => {
|
|
152
|
+
pacing = null
|
|
153
|
+
const next = deltaQueue.shift()
|
|
154
|
+
if (next) append(next)
|
|
155
|
+
paceStreamDeltas()
|
|
156
|
+
}, 70)
|
|
157
|
+
}
|
|
158
|
+
const appendPaced = (event) => {
|
|
159
|
+
const isStreamDelta = event && event.type === 'stream' && event.payload && event.payload.content
|
|
160
|
+
if (!isStreamDelta) { append(event); return }
|
|
161
|
+
deltaQueue.push(event)
|
|
162
|
+
if (deltaQueue.length > 40) deltaQueue.splice(0, deltaQueue.length - 40)
|
|
163
|
+
paceStreamDeltas()
|
|
164
|
+
}
|
|
165
|
+
// 供 ssr/jsdom 测试断言使用(不影响正常渲染)
|
|
166
|
+
if (typeof window !== 'undefined') window.__fangLiveFeedQueue = deltaQueue
|
|
167
|
+
const loadStatus = async () => {
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch('/dsh-master/api/superagent/status', { headers: { accept: 'application/json' } })
|
|
170
|
+
const body = await response.json().catch(() => null)
|
|
171
|
+
if (alive && body) setRouterStatus(body)
|
|
172
|
+
} catch { /* 状态可选 */ }
|
|
173
|
+
}
|
|
174
|
+
const connect = async (oldToken = currentToken) => {
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch('/dsh-master/api/superagent/stream-token', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'content-type': 'application/json' },
|
|
179
|
+
body: JSON.stringify(oldToken ? { token: oldToken } : {}),
|
|
180
|
+
})
|
|
181
|
+
const body = await response.json().catch(() => null)
|
|
182
|
+
if (!alive) return
|
|
183
|
+
if (!response.ok || !body?.token) { setError(`实时流 token 获取失败(HTTP ${response.status})`); return }
|
|
184
|
+
source?.close()
|
|
185
|
+
currentToken = String(body.token)
|
|
186
|
+
source = new EventSource(`/dsh-master/api/superagent/stream?token=${encodeURIComponent(body.token)}`)
|
|
187
|
+
source.addEventListener('worker-event', (event) => {
|
|
188
|
+
try { appendPaced(JSON.parse(event.data)) } catch { /* ignore */ }
|
|
189
|
+
})
|
|
190
|
+
source.onerror = () => { if (alive) setError('实时流连接中断(将自动重连)') }
|
|
191
|
+
setError('')
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (alive) setError('实时流连接失败:' + String(err && err.message ? err.message : err))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
void connect()
|
|
197
|
+
const statusTimer = setInterval(loadStatus, 5000)
|
|
198
|
+
loadStatus()
|
|
199
|
+
rotateTimer = setInterval(() => { void connect(currentToken) }, 4 * 60 * 1000)
|
|
200
|
+
return () => {
|
|
201
|
+
alive = false
|
|
202
|
+
clearInterval(statusTimer)
|
|
203
|
+
clearInterval(rotateTimer)
|
|
204
|
+
try { source?.close() } catch { /* ignore */ }
|
|
205
|
+
if (pacing) { clearTimeout(pacing); pacing = null }
|
|
206
|
+
deltaQueue.length = 0
|
|
207
|
+
currentToken = ''
|
|
208
|
+
if (typeof window !== 'undefined' && window.__fangLiveFeedQueue === deltaQueue) delete window.__fangLiveFeedQueue
|
|
209
|
+
}
|
|
210
|
+
}, [])
|
|
211
|
+
const quota = routerStatus && routerStatus.quota
|
|
212
|
+
// v3.2.7:真实流式文本(provider 的 token delta 经 SSE 到达),只保留尾部。
|
|
213
|
+
const streamedText = events
|
|
214
|
+
.filter((event) => event && event.type === 'stream' && event.payload && event.payload.content)
|
|
215
|
+
.map((event) => String(event.payload.content))
|
|
216
|
+
.join('')
|
|
217
|
+
.slice(-400)
|
|
218
|
+
const lastToolEvent = events
|
|
219
|
+
.filter((event) => event && event.type === 'stream' && event.payload && event.payload.toolName)
|
|
220
|
+
.slice(-1)[0]
|
|
221
|
+
const toolFinished = lastToolEvent && events.some((event) => event && event.type === 'done'
|
|
222
|
+
&& String(event.ticketId ?? '') === String(lastToolEvent.ticketId ?? '')
|
|
223
|
+
&& Number(event.at ?? 0) >= Number(lastToolEvent.at ?? 0))
|
|
224
|
+
const streamedTool = toolFinished ? '' : (lastToolEvent ? String(lastToolEvent.payload.toolName) : '')
|
|
225
|
+
return h('div', { style: S.card },
|
|
226
|
+
h('div', { style: S.row },
|
|
227
|
+
h('span', { style: S.name }, '实时事件流'),
|
|
228
|
+
tag(routerStatus ? `SSE ${routerStatus.streaming?.clients ?? 0}` : 'SSE'),
|
|
229
|
+
routerStatus && routerStatus.streaming?.hostAdapter ? tag('宿主 adapter') : null,
|
|
230
|
+
quota ? tag(`并发 ${quota.active ?? 0}/${quota.maxConcurrent ?? '?'}`) : null,
|
|
231
|
+
h('span', { style: { marginLeft: 'auto', fontSize: 11, ...(error ? S.error : S.desc) } }, error || 'live')),
|
|
232
|
+
streamedText
|
|
233
|
+
? h('div', { style: { ...S.desc, whiteSpace: 'pre-wrap', maxHeight: 120, overflow: 'auto', fontFamily: 'monospace' } }, streamedText)
|
|
234
|
+
: null,
|
|
235
|
+
streamedTool ? h('div', { style: S.desc }, `工具中:${streamedTool}`) : null,
|
|
236
|
+
events.length === 0
|
|
237
|
+
? h('div', { style: S.desc }, '暂无 Worker 事件;派发 Worker 后会实时出现。')
|
|
238
|
+
: events.slice(-8).reverse().map((event, index) => h('div', { key: index, style: S.desc },
|
|
239
|
+
`${String(event.type ?? 'event')} · ${String(event.ticketId ?? '')} · ${event.step?.type ?? event.payload?.phase ?? ''}`)),
|
|
240
|
+
h('div', { style: S.desc }, 'SSE token 每 4 分钟轮换;token 审计见状态接口。'),
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function MasterPanel() {
|
|
245
|
+
const [status, setStatus] = React.useState(null)
|
|
246
|
+
const [error, setError] = React.useState('')
|
|
247
|
+
|
|
248
|
+
React.useEffect(() => {
|
|
249
|
+
let alive = true
|
|
250
|
+
const load = async () => {
|
|
251
|
+
try {
|
|
252
|
+
const response = await fetch('/dsh-master/api/status', { headers: { accept: 'application/json' } })
|
|
253
|
+
const body = await response.json().catch(() => null)
|
|
254
|
+
if (!alive) return
|
|
255
|
+
if (!response.ok || !body) setError(`状态读取失败(HTTP ${response.status})`)
|
|
256
|
+
else { setStatus(body); setError('') }
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (alive) setError('状态读取失败:' + String(err && err.message ? err.message : err))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
void load()
|
|
262
|
+
const timer = setInterval(load, 5000)
|
|
263
|
+
return () => { alive = false; clearInterval(timer) }
|
|
264
|
+
}, [])
|
|
265
|
+
|
|
266
|
+
const items = status && Array.isArray(status.subplugins) ? status.subplugins : []
|
|
267
|
+
return h('aside', { style: S.panel, id: 'dsh-master-panel', 'aria-label': '房主状态' },
|
|
268
|
+
h('div', { style: S.head },
|
|
269
|
+
h('span', { style: S.title }, '房主 · dsh-Master'),
|
|
270
|
+
h('span', { style: S.sub }, status ? `${status.version} · ${status.role}` : '读取中'),
|
|
271
|
+
h('button', {
|
|
272
|
+
type: 'button',
|
|
273
|
+
style: S.close,
|
|
274
|
+
title: '收起面板',
|
|
275
|
+
'aria-label': '收起房主面板',
|
|
276
|
+
onClick: () => setPanelOpen(false),
|
|
277
|
+
}, '×')),
|
|
278
|
+
h('div', { style: S.body },
|
|
279
|
+
error ? h('div', { style: S.error }, error) : null,
|
|
280
|
+
status ? h('div', { style: S.card },
|
|
281
|
+
h('div', { style: S.row }, h('span', { style: S.name }, '主 Agent 控制面'), tag('与瓮城并行')),
|
|
282
|
+
h('div', { style: S.desc }, '角色:' + status.role + ' · 版本:' + status.version),
|
|
283
|
+
h('div', { style: S.desc }, '运行:' + Math.max(1, Math.round((status.uptimeMs || 0) / 60000)) + ' 分钟'),
|
|
284
|
+
) : null,
|
|
285
|
+
...items.map((item) => h(SubpluginCard, { key: item.id, item })),
|
|
286
|
+
h(FangCard, { cooperation: status && status.cooperation }),
|
|
287
|
+
h(LiveFeed),
|
|
288
|
+
h('div', { style: S.desc }, '3.2.9:面板常驻(点面板外不收起、按钮二次点击收起、长内容可滚动);实时流(SSE)+ token 轮换/审计 + Worker 并发配额。'),
|
|
289
|
+
))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** shell.overlay:整框浮层里的面板本体;关闭时不渲染任何节点。 */
|
|
293
|
+
function MasterOverlay() {
|
|
294
|
+
const open = usePanelOpen()
|
|
295
|
+
return open ? h(MasterPanel) : null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* sidebar.footer.action 槽:一个「房主」按钮。
|
|
300
|
+
* 再点一次收起面板(之前只 setOpen(true),面板打开后按钮无法关闭)。
|
|
301
|
+
*/
|
|
302
|
+
function MasterSeat() {
|
|
303
|
+
const open = usePanelOpen()
|
|
304
|
+
return h('button', {
|
|
305
|
+
type: 'button',
|
|
306
|
+
style: open ? S.fabOpen : S.fab,
|
|
307
|
+
title: open ? '房主 · 点击收起控制面' : '房主 · 主 Agent 控制面',
|
|
308
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
309
|
+
'aria-controls': 'dsh-master-panel',
|
|
310
|
+
onClick: () => togglePanel(),
|
|
311
|
+
}, '◈ 房主')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
inject: ['slots'],
|
|
316
|
+
apply(ctx) {
|
|
317
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
318
|
+
name: 'sidebar.footer.action',
|
|
319
|
+
id: 'dsh-master-home',
|
|
320
|
+
order: 79,
|
|
321
|
+
label: '房主',
|
|
322
|
+
}, MasterSeat))
|
|
323
|
+
// 面板放在 shell.overlay:位于所有列与滚动容器之外,且整层 click-through,
|
|
324
|
+
// 因此不会随侧栏(或宿主弹层)卸载,点击面板外也不会收回。
|
|
325
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
326
|
+
name: 'shell.overlay',
|
|
327
|
+
id: 'dsh-master-panel',
|
|
328
|
+
order: 78,
|
|
329
|
+
label: '房主面板',
|
|
330
|
+
}, MasterOverlay))
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Dsh-Master(房主)bundle
|
|
2
|
+
#
|
|
3
|
+
# 与瓮城 dsh-Fang 并行:本 bundle 只插入房主自己的行,不覆盖 fang 的任何行。
|
|
4
|
+
# v3.2.3:Worker Agent Loop 编译产物(dist-worker)作为独立 preset 行接入;
|
|
5
|
+
# Router 增加母体派发桥、Worker 实时事件与结果回写。
|
|
6
|
+
# 角色边界在插件代码里强制(见 src/kernel.mjs ROLE_MATRIX 与 docs/SECURITY.md)。
|
|
7
|
+
- insert:
|
|
8
|
+
- id: master-core
|
|
9
|
+
name: '@twinsearth/dsh-master'
|
|
10
|
+
config:
|
|
11
|
+
enabled: true
|
|
12
|
+
bridgeToken: ''
|
|
13
|
+
bridgeTokenRotateMs: 1800000
|
|
14
|
+
bridgeTokenGraceMs: 300000
|
|
15
|
+
tokenAuditLimit: 500
|
|
16
|
+
dispatch:
|
|
17
|
+
homeDir: ''
|
|
18
|
+
cliPath: ''
|
|
19
|
+
profile: ''
|
|
20
|
+
- id: master-superagent-router
|
|
21
|
+
name: '@twinsearth/dsh-master/superagent-router'
|
|
22
|
+
config:
|
|
23
|
+
enabled: true
|
|
24
|
+
trustMode: observe
|
|
25
|
+
allowedActors: []
|
|
26
|
+
requireMention: true
|
|
27
|
+
allowedSources: [github, feishu]
|
|
28
|
+
repoAllowlist: []
|
|
29
|
+
streamTokenTtlMs: 21600000
|
|
30
|
+
uiTokenTtlMs: 600000
|
|
31
|
+
streamTokenRequired: true
|
|
32
|
+
workerQuota:
|
|
33
|
+
enabled: true
|
|
34
|
+
maxConcurrent: 2
|
|
35
|
+
maxPerSource: 0
|
|
36
|
+
dispatch:
|
|
37
|
+
homeDir: ''
|
|
38
|
+
cliPath: ''
|
|
39
|
+
profile: ''
|
|
40
|
+
motherUrl: ''
|
|
41
|
+
motherToken: ''
|
|
42
|
+
motherAuthToken: ''
|
|
43
|
+
contract: true
|
|
44
|
+
contractFileDir: ''
|
|
45
|
+
writeback:
|
|
46
|
+
url: ''
|
|
47
|
+
token: ''
|
|
48
|
+
githubToken: ''
|
|
49
|
+
feishuWebhookUrl: ''
|
|
50
|
+
feishuSignSecret: ''
|
|
51
|
+
- id: master-superagent-worker
|
|
52
|
+
name: '@twinsearth/dsh-master/superagent-worker'
|
|
53
|
+
config:
|
|
54
|
+
enabled: true
|
|
55
|
+
- id: master-superagent-loop
|
|
56
|
+
name: '@twinsearth/dsh-master/superagent-loop'
|
|
57
|
+
config:
|
|
58
|
+
enabled: true
|
|
59
|
+
autorun: true
|
|
60
|
+
model: ''
|
|
61
|
+
workspace: ''
|
|
62
|
+
- id: master-cost-optimizer
|
|
63
|
+
name: '@twinsearth/dsh-master/cost-optimizer'
|
|
64
|
+
config:
|
|
65
|
+
enabled: true
|
|
66
|
+
mode: observe
|
|
67
|
+
targetSavingsRatio: 0.8
|
|
68
|
+
- id: master-dual-manager
|
|
69
|
+
name: '@twinsearth/dsh-master/dual-manager'
|
|
70
|
+
config:
|
|
71
|
+
enabled: true
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
|
|
3
|
+
import { DEFAULT_TRANSPORT_ORDER, METHODS } from './constants.js';
|
|
4
|
+
import { createFileTransport, createHttpTransport, createLegacyTransport, createLocalTransport, createTransportRouter, } from './transports.js';
|
|
5
|
+
/**
|
|
6
|
+
* 客户端门面:一次配置,按 local → http → file → legacy 自动降级。
|
|
7
|
+
*
|
|
8
|
+
* @param {{
|
|
9
|
+
* ctx?: object,
|
|
10
|
+
* config?: {httpBaseUrl?:string, httpToken?:string, fileDir?:string, order?:string[], timeoutMs?:number},
|
|
11
|
+
* fetchImpl?: typeof fetch,
|
|
12
|
+
* logger?: object,
|
|
13
|
+
* instanceId?: string,
|
|
14
|
+
* }} options
|
|
15
|
+
*/
|
|
16
|
+
export function createContractClient(options = {}) {
|
|
17
|
+
const { ctx = null, config = {}, fetchImpl = fetch, logger = null, instanceId = 'contract-client' } = options;
|
|
18
|
+
const transports = [];
|
|
19
|
+
if (ctx)
|
|
20
|
+
transports.push(createLocalTransport({ ctx }));
|
|
21
|
+
if (config.httpBaseUrl)
|
|
22
|
+
transports.push(createHttpTransport({
|
|
23
|
+
baseUrl: config.httpBaseUrl,
|
|
24
|
+
token: config.httpToken,
|
|
25
|
+
headers: config.httpHeaders,
|
|
26
|
+
fetchImpl,
|
|
27
|
+
timeoutMs: config.timeoutMs,
|
|
28
|
+
pathPrefix: config.pathPrefix,
|
|
29
|
+
}));
|
|
30
|
+
if (config.fileDir)
|
|
31
|
+
transports.push(createFileTransport({
|
|
32
|
+
dir: config.fileDir,
|
|
33
|
+
pollMs: config.filePollMs,
|
|
34
|
+
timeoutMs: config.fileTimeoutMs,
|
|
35
|
+
}));
|
|
36
|
+
if (ctx)
|
|
37
|
+
transports.push(createLegacyTransport({ ctx }));
|
|
38
|
+
const order = Array.isArray(config.order) && config.order.length > 0 ? config.order : DEFAULT_TRANSPORT_ORDER;
|
|
39
|
+
const router = createTransportRouter({
|
|
40
|
+
transports,
|
|
41
|
+
order,
|
|
42
|
+
localHello: null,
|
|
43
|
+
logger,
|
|
44
|
+
});
|
|
45
|
+
let lastHello = null;
|
|
46
|
+
let helloPromise = null;
|
|
47
|
+
const client = {
|
|
48
|
+
instanceId,
|
|
49
|
+
transports: () => router.transports(),
|
|
50
|
+
describe: () => ({
|
|
51
|
+
instanceId,
|
|
52
|
+
routes: router.describe(),
|
|
53
|
+
hello: lastHello,
|
|
54
|
+
}),
|
|
55
|
+
attempts: () => router.attempts(),
|
|
56
|
+
async hello(force = false) {
|
|
57
|
+
if (!force && lastHello)
|
|
58
|
+
return lastHello;
|
|
59
|
+
if (!force && helloPromise)
|
|
60
|
+
return helloPromise;
|
|
61
|
+
helloPromise = (async () => {
|
|
62
|
+
const result = await router.hello();
|
|
63
|
+
lastHello = result;
|
|
64
|
+
return result;
|
|
65
|
+
})().finally(() => { helloPromise = null; });
|
|
66
|
+
return helloPromise;
|
|
67
|
+
},
|
|
68
|
+
async invoke(method, params, invokeOptions = {}) {
|
|
69
|
+
const result = await router.invoke(method, params, { timeoutMs: config.timeoutMs, ...invokeOptions });
|
|
70
|
+
return result;
|
|
71
|
+
},
|
|
72
|
+
async health() { return await client.invoke(METHODS.HEALTH, null); },
|
|
73
|
+
async spawnFork(params, invokeOptions = {}) { return await client.invoke(METHODS.FORK_SPAWN, params, invokeOptions); },
|
|
74
|
+
async stopFork(params, invokeOptions = {}) { return await client.invoke(METHODS.FORK_STOP, params, invokeOptions); },
|
|
75
|
+
async listForks(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.FORK_LIST, params, invokeOptions); },
|
|
76
|
+
async createDecision(params, invokeOptions = {}) { return await client.invoke(METHODS.DECISION_CREATE, params, invokeOptions); },
|
|
77
|
+
async resolveDecision(params, invokeOptions = {}) { return await client.invoke(METHODS.DECISION_RESOLVE, params, invokeOptions); },
|
|
78
|
+
async listSessions(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.SESSION_LIST, params, invokeOptions); },
|
|
79
|
+
async orchestratorStatus(params = {}, invokeOptions = {}) { return await client.invoke(METHODS.ORCHESTRATOR_STATUS, params, invokeOptions); },
|
|
80
|
+
};
|
|
81
|
+
return client;
|
|
82
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
|
|
3
|
+
/**
|
|
4
|
+
* 契约常量与兼容规则(contract SDK 1.x)。
|
|
5
|
+
*
|
|
6
|
+
* 稳定承诺:
|
|
7
|
+
* * MAJOR 相同的版本保证“旧字段可读、旧方法可调、未知字段忽略”;
|
|
8
|
+
* * 新增能力只通过 METHODS/CAPABILITIES 扩展,不改变既有方法语义;
|
|
9
|
+
* * 删除/改义必须升 MAJOR,并保留至少一个 MINOR 周期的双写(method@1 与 method@2)。
|
|
10
|
+
*/
|
|
11
|
+
export const CONTRACT_PACKAGE = '@twinsearth/dsh-fang-contract';
|
|
12
|
+
export const CONTRACT_VERSION = '1.0.0';
|
|
13
|
+
export const PROTOCOL = 'fang-master';
|
|
14
|
+
export const PROTOCOL_VERSION = 1;
|
|
15
|
+
export const MIN_COMPATIBLE_MAJOR = 1;
|
|
16
|
+
/** 稳定方法名(新方法只能追加)。 */
|
|
17
|
+
export const METHODS = Object.freeze({
|
|
18
|
+
HELLO: 'contract.hello',
|
|
19
|
+
HEALTH: 'contract.health',
|
|
20
|
+
FORK_SPAWN: 'fang.fork.spawn',
|
|
21
|
+
FORK_STOP: 'fang.fork.stop',
|
|
22
|
+
FORK_LIST: 'fang.fork.list',
|
|
23
|
+
DECISION_CREATE: 'fang.decision.create',
|
|
24
|
+
DECISION_RESOLVE: 'fang.decision.resolve',
|
|
25
|
+
SESSION_LIST: 'fang.session.list',
|
|
26
|
+
ORCHESTRATOR_STATUS: 'fang.orchestrator.status',
|
|
27
|
+
});
|
|
28
|
+
/** 方法 → 契约版本(用于协商后的方法裁剪)。 */
|
|
29
|
+
export const METHOD_VERSION = Object.freeze({
|
|
30
|
+
[METHODS.HELLO]: '1.0',
|
|
31
|
+
[METHODS.HEALTH]: '1.0',
|
|
32
|
+
[METHODS.FORK_SPAWN]: '1.0',
|
|
33
|
+
[METHODS.FORK_STOP]: '1.0',
|
|
34
|
+
[METHODS.FORK_LIST]: '1.0',
|
|
35
|
+
[METHODS.DECISION_CREATE]: '1.0',
|
|
36
|
+
[METHODS.DECISION_RESOLVE]: '1.0',
|
|
37
|
+
[METHODS.SESSION_LIST]: '1.0',
|
|
38
|
+
[METHODS.ORCHESTRATOR_STATUS]: '1.0',
|
|
39
|
+
});
|
|
40
|
+
export const CAPABILITIES = Object.freeze([
|
|
41
|
+
'sync', 'async', 'idempotent', 'stream-events', 'file-outbox',
|
|
42
|
+
]);
|
|
43
|
+
export const ROLES = Object.freeze(['mother', 'essence', 'fork']);
|
|
44
|
+
export const ERROR_CODES = Object.freeze({
|
|
45
|
+
INVALID_ENVELOPE: 'invalid-envelope',
|
|
46
|
+
UNSUPPORTED_METHOD: 'unsupported-method',
|
|
47
|
+
UNSUPPORTED_VERSION: 'unsupported-version',
|
|
48
|
+
INCOMPATIBLE: 'contract-incompatible',
|
|
49
|
+
TRANSPORT_UNAVAILABLE: 'transport-unavailable',
|
|
50
|
+
TRANSPORT_TIMEOUT: 'transport-timeout',
|
|
51
|
+
TRANSPORT_HTTP_ERROR: 'transport-http-error',
|
|
52
|
+
REMOTE_ERROR: 'remote-error',
|
|
53
|
+
ALL_TRANSPORTS_FAILED: 'all-transports-failed',
|
|
54
|
+
HANDLER_THREW: 'handler-threw',
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* 兜底顺序(从快到慢):
|
|
58
|
+
* local —— 同一 Host 的 DSH 服务(零网络、首选)
|
|
59
|
+
* http —— 母体/原身 HTTP 契约端点
|
|
60
|
+
* file —— 同一文件系统的 outbox/inbox(HTTP 不可用/被防火墙拦)
|
|
61
|
+
* legacy —— 直接调用 3.1.x 的旧 DSH 服务键(接口包被替换/新端点不存在时)
|
|
62
|
+
*/
|
|
63
|
+
export const DEFAULT_TRANSPORT_ORDER = Object.freeze(['local', 'http', 'file', 'legacy']);
|
|
64
|
+
/** 3.1.x 及更早的旧服务键(只读兼容;不新增语义)。 */
|
|
65
|
+
export const LEGACY_SERVICE_KEYS = Object.freeze({
|
|
66
|
+
forker: 'fangForker',
|
|
67
|
+
decisionCenter: 'fangDecisionCenter',
|
|
68
|
+
sessionCatalog: 'fangSessionCatalog',
|
|
69
|
+
orchestrator: 'fang',
|
|
70
|
+
});
|