@twinsearth/dsh-master 0.0.0-stage → 3.2.8

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 (95) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +89 -2
  3. package/client.js +263 -0
  4. package/cordis.patch.yml +71 -0
  5. package/dist-host/contract/client.js +82 -0
  6. package/dist-host/contract/constants.js +70 -0
  7. package/dist-host/contract/envelope.js +84 -0
  8. package/dist-host/contract/index.js +17 -0
  9. package/dist-host/contract/negotiate.js +87 -0
  10. package/dist-host/contract/server.js +71 -0
  11. package/dist-host/contract/transports.js +360 -0
  12. package/dist-host/index.js +260 -0
  13. package/dist-host/kernel.js +186 -0
  14. package/dist-host/plugins/cost-optimizer.js +147 -0
  15. package/dist-host/plugins/dual-manager.js +251 -0
  16. package/dist-host/plugins/superagent-router.js +499 -0
  17. package/dist-host/plugins/superagent-worker.js +73 -0
  18. package/dist-host/superagent/bridge-tokens.js +236 -0
  19. package/dist-host/superagent/dispatch.js +198 -0
  20. package/dist-host/superagent/feishu.js +115 -0
  21. package/dist-host/superagent/github.js +119 -0
  22. package/dist-host/superagent/pr-delivery.js +351 -0
  23. package/dist-host/superagent/router.js +219 -0
  24. package/dist-host/superagent/stream-adapter.js +238 -0
  25. package/dist-host/superagent/stream-tokens.js +141 -0
  26. package/dist-host/superagent/ticket-store.js +108 -0
  27. package/dist-host/superagent/trust.js +145 -0
  28. package/dist-host/superagent/types.js +144 -0
  29. package/dist-host/superagent/worker.js +95 -0
  30. package/dist-host/superagent/writeback.js +162 -0
  31. package/dist-worker/agent-loop.js +493 -0
  32. package/dist-worker/errors.js +41 -0
  33. package/dist-worker/github-pr.js +44 -0
  34. package/dist-worker/index.js +323 -0
  35. package/dist-worker/llm-http.js +351 -0
  36. package/dist-worker/main.js +45 -0
  37. package/dist-worker/prompt.js +35 -0
  38. package/dist-worker/sink-http.js +135 -0
  39. package/dist-worker/tools.js +542 -0
  40. package/dist-worker/types.js +14 -0
  41. package/docs/ARCHITECTURE.md +114 -0
  42. package/docs/CHANGELOG.md +377 -0
  43. package/docs/INTEROP.md +60 -0
  44. package/docs/REVIEW-v3.2.7.md +121 -0
  45. package/docs/ROADMAP.md +106 -0
  46. package/docs/SECURITY.md +153 -0
  47. package/docs/SMOKE-3.2.5.md +77 -0
  48. package/docs/SMOKE-3.2.7.md +73 -0
  49. package/docs/SUPERAGENT.md +141 -0
  50. package/docs/WORKER-AGENT-LOOP.md +139 -0
  51. package/icon.svg +9 -0
  52. package/locale/en.json +6 -0
  53. package/locale/zh.json +6 -0
  54. package/package.json +79 -4
  55. package/src/contract/client.js +86 -0
  56. package/src/contract/constants.js +79 -0
  57. package/src/contract/envelope.js +83 -0
  58. package/src/contract/index.js +19 -0
  59. package/src/contract/negotiate.js +106 -0
  60. package/src/contract/server.js +77 -0
  61. package/src/contract/transports.js +319 -0
  62. package/src/index.ts +265 -0
  63. package/src/kernel.ts +190 -0
  64. package/src/plugins/cost-optimizer.ts +155 -0
  65. package/src/plugins/dual-manager.ts +252 -0
  66. package/src/plugins/superagent-router.ts +465 -0
  67. package/src/plugins/superagent-worker.ts +85 -0
  68. package/src/superagent/bridge-tokens.ts +236 -0
  69. package/src/superagent/dispatch.ts +197 -0
  70. package/src/superagent/feishu.ts +104 -0
  71. package/src/superagent/github.ts +111 -0
  72. package/src/superagent/pr-delivery.ts +308 -0
  73. package/src/superagent/router.ts +201 -0
  74. package/src/superagent/stream-adapter.ts +184 -0
  75. package/src/superagent/stream-tokens.ts +142 -0
  76. package/src/superagent/ticket-store.ts +104 -0
  77. package/src/superagent/trust.ts +139 -0
  78. package/src/superagent/types.ts +135 -0
  79. package/src/superagent/worker.ts +100 -0
  80. package/src/superagent/writeback.ts +154 -0
  81. package/tsconfig.host.build.json +12 -0
  82. package/tsconfig.host.json +21 -0
  83. package/tsconfig.worker.build.json +13 -0
  84. package/tsconfig.worker.json +17 -0
  85. package/types/superagent.d.ts +99 -0
  86. package/worker/agent-loop.ts +524 -0
  87. package/worker/errors.ts +47 -0
  88. package/worker/github-pr.ts +65 -0
  89. package/worker/index.ts +334 -0
  90. package/worker/llm-http.ts +359 -0
  91. package/worker/main.ts +45 -0
  92. package/worker/prompt.ts +39 -0
  93. package/worker/sink-http.ts +159 -0
  94. package/worker/tools.ts +531 -0
  95. package/worker/types.ts +247 -0
package/README.md CHANGED
@@ -1,3 +1,90 @@
1
- # Temporary Holding Version
1
+ # dsh-Master(房主)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > 与瓮城 **dsh-Fang** 并行的主 Agent 插件。
4
+ > 当前版本:`3.2.8`(审计修复发布线;tag / GitHub Release / NPM public)。
5
+ > 仓库:https://github.com/TwinsEarth/dsh-Master
6
+
7
+ ## 这是什么
8
+
9
+ dsh-Master 不接管瓮城的「母体 / 原身 / 分身」生命周期;它是一块**并列的主插件**,
10
+ 在自己的名字空间里托管 Agent 子插件,并通过 DSH 服务键与瓮城协作:
11
+
12
+ ```text
13
+ DSH Host
14
+ ├── 瓮城 bundle (dsh-Fang) 房主 bundle (dsh-Master)
15
+ │ mirror / essence / derived core(主 Agent 控制面)
16
+ │ fangForker / fangDecisionCenter ◄────── ├── superagent-router (essence)
17
+ │ fangSessionCatalog ├── superagent-worker (fork)
18
+ │ ├── cost-optimizer (fork)
19
+ │ └── dual-manager (mother/essence)
20
+ └── 协作只走服务键;任一方向失败一律 fail-closed,不本地降级执行
21
+ ```
22
+
23
+ ## 子插件
24
+
25
+ | 子插件 | 行 id | 允许角色 | 路线图 | 已交付 |
26
+ |---|---|---|---|---|
27
+ | SuperAgent Router | `master-superagent-router` | **essence only** | 3.2.1/3.2.3 | Webhook/Ticket/Mandate、渐进信任、本地或母体桥派发、Worker 实时事件、回写 |
28
+ | SuperAgent Worker | `master-superagent-worker` | **fork only** | 3.2.1 | Ticket 上下文、Mandate 守卫 |
29
+ | Worker Agent Loop | `master-superagent-loop` | **fork only** | 3.2.3 | tsc 编译 preset;有 Ticket 自动跑多轮工具循环并流式回传 |
30
+ | 成本优化 HarnessMM | `master-cost-optimizer` | **fork only** | 3.3.0 | observe 层:token 估算、实测记录 |
31
+ | 双主管 PM + PJM | `master-dual-manager` | **mother / essence** | 3.4.0 | PRD/计划/调度/决策中心澄清 |
32
+
33
+ ## 本地开发
34
+
35
+ ```bash
36
+ npm install # devDeps: typescript + @types/node(仅类型检查/构建用)
37
+ npm test # node:test 全量(含 worker/*.ts 测试)
38
+ npm run typecheck:worker # tsc strict 类型检查
39
+ npm run build:worker # tsc 构建 dist-worker/(DSH 实际加载的 preset)
40
+ node dist-worker/main.js # CLI(需 DSH_MASTER_TICKET + LLM/流式配置)
41
+ ```
42
+
43
+ 仓库运行态零依赖。运行时全部加载**编译后的 JS**:
44
+
45
+ - 房主 Host/插件:`src/**/*.ts` → `npm run build:host` → `dist-host/`;
46
+ - Worker preset:`worker/**/*.ts` → `npm run build:worker` → `dist-worker/`;
47
+ - 浏览器侧 `client.js` 保持 JS(DSH client module 运行时加载);
48
+ - 对外类型:`types/superagent.d.ts`;
49
+ - `npm test` 直接由 Node 26 类型擦除执行 TS 源码/测试;production 用 dist JS。
50
+
51
+ ```bash
52
+ npm run typecheck:host && npm run build:host
53
+ npm run typecheck:worker && npm run build:worker
54
+ npm run build # 两者一起
55
+ dsh plugin add <package> # 加载 dist-host/dist-worker
56
+ ```
57
+
58
+ ## 安装(审核通过后)
59
+
60
+ ```bash
61
+ # 目前请勿安装/发布;审核通过后:
62
+ # dsh plugin add @twinsearth/dsh-master (发布后)
63
+ # 或以本地 bundle 目录安装
64
+ ```
65
+
66
+ 安装后:
67
+
68
+ - 插件目录多出 **房主(dsh-Master)** + 五个子插件行(Router / Worker / Loop / HarnessMM / PM+PJM);
69
+ - 侧栏底部出现「◈ 房主」入口,打开只读状态抽屉(角色/子插件矩阵/瓮城协作状态);
70
+ - 角色边界在代码里强制:Router 只在 essence、Worker/成本只 fork、双主管只 mother/essence;
71
+ - SuperAgent 默认 **observe**,不会自动接单。
72
+
73
+ ## 文档
74
+
75
+ - [房主 ↔ 瓮城 互联契约](docs/INTEROP.md)
76
+ - [真实 Host 集成 smoke(3.2.7)](docs/SMOKE-3.2.7.md)
77
+ - [v3.2.7 全面审计与实机复盘](docs/REVIEW-v3.2.7.md)
78
+ - [Worker Agent Loop](docs/WORKER-AGENT-LOOP.md)
79
+ - [SuperAgent v3.2.1 设计与契约](docs/SUPERAGENT.md)
80
+ - [架构设计](docs/ARCHITECTURE.md)
81
+ - [路线图(3.2.0–3.4.0)](docs/ROADMAP.md)
82
+ - [安全边界](docs/SECURITY.md)
83
+ - [变更记录](docs/CHANGELOG.md)
84
+
85
+ ## 状态
86
+
87
+ - 3.2.8 发布线:与瓮城通过 fang-master contract 1.0 互联(local/http/file/legacy 四级保底);GitHub Release + NPM public。
88
+ - 当前工作树完成新一轮审计修复:工具名/死循环阈值、Router 鉴权与生命周期、真实路由分流、契约幂等与超时、PR 护栏与交付、飞书加密、长 Shell 后台作业、成本实测接线、PJM 派生契约。
89
+ - 验收:`npm run check` / `npm run build` / `npm test` 全绿;真实双 Host/fork 落库、工具写读、SSE、回写、配额与桥清理断言通过。详见复盘文档。
90
+ - 后续修复尚未发布,也未安装到用户主 profile;发布仍需明确指令。
package/client.js ADDED
@@ -0,0 +1,263 @@
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
+ window.__ModuleLoader__.load({
12
+ id: '@twinsearth/dsh-master',
13
+ factory(require) {
14
+ const React = require('react')
15
+ const h = React.createElement
16
+
17
+ const S = {
18
+ panel: {
19
+ position: 'fixed', top: 0, right: 0, bottom: 0, width: 'min(380px, 92vw)',
20
+ zIndex: 2147482800, background: '#fff', borderLeft: '1px solid rgba(16,17,20,.12)',
21
+ boxShadow: '-16px 0 40px rgba(16,17,20,.16)', display: 'flex', flexDirection: 'column',
22
+ fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif',
23
+ fontSize: 13, color: '#1D1D1F',
24
+ },
25
+ head: { display: 'flex', alignItems: 'center', gap: 8, padding: '14px 16px', borderBottom: '1px solid rgba(16,17,20,.08)' },
26
+ title: { fontSize: 14, fontWeight: 700 },
27
+ sub: { fontSize: 11.5, color: '#86868B' },
28
+ close: { marginLeft: 'auto', width: 28, height: 28, borderRadius: 8, border: '1px solid rgba(16,17,20,.12)', background: '#fff', color: '#5F6368', cursor: 'pointer' },
29
+ body: { flex: 1, overflowY: 'auto', padding: '12px 14px 18px', display: 'flex', flexDirection: 'column', gap: 10 },
30
+ card: { border: '1px solid rgba(16,17,20,.1)', borderRadius: 12, padding: '10px 11px', background: '#FAFAFC', display: 'flex', flexDirection: 'column', gap: 4 },
31
+ row: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
32
+ name: { fontSize: 13, fontWeight: 600 },
33
+ tag: { display: 'inline-flex', alignItems: 'center', height: 18, padding: '0 6px', borderRadius: 6, fontSize: 10.5, fontWeight: 700, background: '#F2F2F5', color: '#5F6368' },
34
+ desc: { fontSize: 12, color: '#5F6368' },
35
+ ok: { color: '#17803D', fontWeight: 600 },
36
+ off: { color: '#B45309', fontWeight: 600 },
37
+ error: { fontSize: 12, color: '#C8102E' },
38
+ fab: {
39
+ display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, padding: '0 9px',
40
+ borderRadius: 8, border: '1px solid rgba(16,17,20,.12)', background: '#fff', color: '#1D1D1F',
41
+ cursor: 'pointer', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit',
42
+ },
43
+ banner: { position: 'fixed', top: 0, right: 0, bottom: 0, left: 0, zIndex: 2147482799, background: 'rgba(18,19,22,.18)' },
44
+ }
45
+
46
+ const tag = (text, color) => h('span', { style: { ...S.tag, ...(color ? { color, background: 'rgba(16,17,20,.05)' } : {}) } }, text)
47
+
48
+ function SubpluginCard({ item }) {
49
+ const color = item.active ? S.ok : S.off
50
+ return h('div', { style: S.card },
51
+ h('div', { style: S.row },
52
+ h('span', { style: S.name }, item.name),
53
+ tag(item.nameEn),
54
+ tag('roadmap ' + item.roadmap),
55
+ h('span', { style: { marginLeft: 'auto', fontSize: 11.5, ...color } }, item.active ? '已激活' : '未激活')),
56
+ h('div', { style: S.desc }, item.description),
57
+ h('div', { style: S.desc },
58
+ '允许角色:' + (item.allowedRoles || []).join(' / '),
59
+ item.blockedReason ? ' · ' + item.blockedReason : ''),
60
+ item.capabilities && item.capabilities.length > 0
61
+ ? h('div', { style: S.desc }, '能力:' + item.capabilities.join(' · '))
62
+ : null,
63
+ )
64
+ }
65
+
66
+ function FangCard({ cooperation }) {
67
+ if (!cooperation) return null
68
+ const entries = Object.entries(cooperation.services || {})
69
+ return h('div', { style: S.card },
70
+ h('div', { style: S.row },
71
+ h('span', { style: S.name }, '与瓮城协作'),
72
+ tag(cooperation.contract || 'dsh-fang'),
73
+ h('span', { style: { marginLeft: 'auto', fontSize: 11.5, ...(cooperation.available ? S.ok : S.off) } },
74
+ cooperation.available ? '在线' : '未发现')),
75
+ entries.length === 0
76
+ ? h('div', { style: S.desc }, '未发现瓮城服务;房主子插件会 fail-closed。')
77
+ : entries.map(([alias, info]) => h('div', { key: alias, style: S.desc },
78
+ alias + ' → ' + info.service + ' ' + (info.available ? '✓' : '×'))),
79
+ )
80
+ }
81
+
82
+ /** v3.2.7:实时事件流(SSE)+ token 轮换;只读,不写宿主状态。 */
83
+ function LiveFeed() {
84
+ const [events, setEvents] = React.useState([])
85
+ const [routerStatus, setRouterStatus] = React.useState(null)
86
+ const [error, setError] = React.useState('')
87
+ React.useEffect(() => {
88
+ let alive = true
89
+ let source = null
90
+ let rotateTimer = null
91
+ let currentToken = ''
92
+ const append = (event) => {
93
+ setEvents((prev) => prev.concat([event]).slice(-20))
94
+ }
95
+ /**
96
+ * v3.2.7:上游可能把多个 delta 合并成一批(fork 启动期阻塞会把 SSE 报文在
97
+ * socket 里积压后一次排空)。直接 append 会让卡片"跳变"一次;
98
+ * 这里把 stream 增量排进节拍队列,按 ~70ms 逐个上屏,保证视觉上逐字流动。
99
+ */
100
+ const deltaQueue = []
101
+ let pacing = null
102
+ const paceStreamDeltas = () => {
103
+ if (pacing || deltaQueue.length === 0) return
104
+ pacing = setTimeout(() => {
105
+ pacing = null
106
+ const next = deltaQueue.shift()
107
+ if (next) append(next)
108
+ paceStreamDeltas()
109
+ }, 70)
110
+ }
111
+ const appendPaced = (event) => {
112
+ const isStreamDelta = event && event.type === 'stream' && event.payload && event.payload.content
113
+ if (!isStreamDelta) { append(event); return }
114
+ deltaQueue.push(event)
115
+ if (deltaQueue.length > 40) deltaQueue.splice(0, deltaQueue.length - 40)
116
+ paceStreamDeltas()
117
+ }
118
+ // 供 ssr/jsdom 测试断言使用(不影响正常渲染)
119
+ if (typeof window !== 'undefined') window.__fangLiveFeedQueue = deltaQueue
120
+ const loadStatus = async () => {
121
+ try {
122
+ const response = await fetch('/dsh-master/api/superagent/status', { headers: { accept: 'application/json' } })
123
+ const body = await response.json().catch(() => null)
124
+ if (alive && body) setRouterStatus(body)
125
+ } catch { /* 状态可选 */ }
126
+ }
127
+ const connect = async (oldToken = currentToken) => {
128
+ try {
129
+ const response = await fetch('/dsh-master/api/superagent/stream-token', {
130
+ method: 'POST',
131
+ headers: { 'content-type': 'application/json' },
132
+ body: JSON.stringify(oldToken ? { token: oldToken } : {}),
133
+ })
134
+ const body = await response.json().catch(() => null)
135
+ if (!alive) return
136
+ if (!response.ok || !body?.token) { setError(`实时流 token 获取失败(HTTP ${response.status})`); return }
137
+ source?.close()
138
+ currentToken = String(body.token)
139
+ source = new EventSource(`/dsh-master/api/superagent/stream?token=${encodeURIComponent(body.token)}`)
140
+ source.addEventListener('worker-event', (event) => {
141
+ try { appendPaced(JSON.parse(event.data)) } catch { /* ignore */ }
142
+ })
143
+ source.onerror = () => { if (alive) setError('实时流连接中断(将自动重连)') }
144
+ setError('')
145
+ } catch (err) {
146
+ if (alive) setError('实时流连接失败:' + String(err && err.message ? err.message : err))
147
+ }
148
+ }
149
+ void connect()
150
+ const statusTimer = setInterval(loadStatus, 5000)
151
+ loadStatus()
152
+ rotateTimer = setInterval(() => { void connect(currentToken) }, 4 * 60 * 1000)
153
+ return () => {
154
+ alive = false
155
+ clearInterval(statusTimer)
156
+ clearInterval(rotateTimer)
157
+ try { source?.close() } catch { /* ignore */ }
158
+ if (pacing) { clearTimeout(pacing); pacing = null }
159
+ deltaQueue.length = 0
160
+ currentToken = ''
161
+ if (typeof window !== 'undefined' && window.__fangLiveFeedQueue === deltaQueue) delete window.__fangLiveFeedQueue
162
+ }
163
+ }, [])
164
+ const quota = routerStatus && routerStatus.quota
165
+ // v3.2.7:真实流式文本(provider 的 token delta 经 SSE 到达),只保留尾部。
166
+ const streamedText = events
167
+ .filter((event) => event && event.type === 'stream' && event.payload && event.payload.content)
168
+ .map((event) => String(event.payload.content))
169
+ .join('')
170
+ .slice(-400)
171
+ const lastToolEvent = events
172
+ .filter((event) => event && event.type === 'stream' && event.payload && event.payload.toolName)
173
+ .slice(-1)[0]
174
+ const toolFinished = lastToolEvent && events.some((event) => event && event.type === 'done'
175
+ && String(event.ticketId ?? '') === String(lastToolEvent.ticketId ?? '')
176
+ && Number(event.at ?? 0) >= Number(lastToolEvent.at ?? 0))
177
+ const streamedTool = toolFinished ? '' : (lastToolEvent ? String(lastToolEvent.payload.toolName) : '')
178
+ return h('div', { style: S.card },
179
+ h('div', { style: S.row },
180
+ h('span', { style: S.name }, '实时事件流'),
181
+ tag(routerStatus ? `SSE ${routerStatus.streaming?.clients ?? 0}` : 'SSE'),
182
+ routerStatus && routerStatus.streaming?.hostAdapter ? tag('宿主 adapter') : null,
183
+ quota ? tag(`并发 ${quota.active ?? 0}/${quota.maxConcurrent ?? '?'}`) : null,
184
+ h('span', { style: { marginLeft: 'auto', fontSize: 11, ...(error ? S.error : S.desc) } }, error || 'live')),
185
+ streamedText
186
+ ? h('div', { style: { ...S.desc, whiteSpace: 'pre-wrap', maxHeight: 120, overflow: 'auto', fontFamily: 'monospace' } }, streamedText)
187
+ : null,
188
+ streamedTool ? h('div', { style: S.desc }, `工具中:${streamedTool}`) : null,
189
+ events.length === 0
190
+ ? h('div', { style: S.desc }, '暂无 Worker 事件;派发 Worker 后会实时出现。')
191
+ : events.slice(-8).reverse().map((event, index) => h('div', { key: index, style: S.desc },
192
+ `${String(event.type ?? 'event')} · ${String(event.ticketId ?? '')} · ${event.step?.type ?? event.payload?.phase ?? ''}`)),
193
+ h('div', { style: S.desc }, 'SSE token 每 4 分钟轮换;token 审计见状态接口。'),
194
+ )
195
+ }
196
+
197
+ function MasterPanel({ onClose }) {
198
+ const [status, setStatus] = React.useState(null)
199
+ const [error, setError] = React.useState('')
200
+
201
+ React.useEffect(() => {
202
+ let alive = true
203
+ const load = async () => {
204
+ try {
205
+ const response = await fetch('/dsh-master/api/status', { headers: { accept: 'application/json' } })
206
+ const body = await response.json().catch(() => null)
207
+ if (!alive) return
208
+ if (!response.ok || !body) setError(`状态读取失败(HTTP ${response.status})`)
209
+ else { setStatus(body); setError('') }
210
+ } catch (err) {
211
+ if (alive) setError('状态读取失败:' + String(err && err.message ? err.message : err))
212
+ }
213
+ }
214
+ void load()
215
+ const timer = setInterval(load, 5000)
216
+ return () => { alive = false; clearInterval(timer) }
217
+ }, [])
218
+
219
+ const items = status && Array.isArray(status.subplugins) ? status.subplugins : []
220
+ return h('div', null,
221
+ h('div', { style: S.fab, role: 'presentation' }),
222
+ h('aside', { style: S.panel, 'aria-label': '房主状态' },
223
+ h('div', { style: S.head },
224
+ h('span', { style: S.title }, '房主 · dsh-Master'),
225
+ h('span', { style: S.sub }, status ? `${status.version} · ${status.role}` : '读取中'),
226
+ h('button', { type: 'button', style: S.close, title: '关闭', onClick: onClose }, '×')),
227
+ h('div', { style: S.body },
228
+ error ? h('div', { style: S.error }, error) : null,
229
+ status ? h('div', { style: S.card },
230
+ h('div', { style: S.row }, h('span', { style: S.name }, '主 Agent 控制面'), tag('与瓮城并行')),
231
+ h('div', { style: S.desc }, '角色:' + status.role + ' · 版本:' + status.version),
232
+ h('div', { style: S.desc }, '运行:' + Math.max(1, Math.round((status.uptimeMs || 0) / 60000)) + ' 分钟'),
233
+ ) : null,
234
+ ...items.map((item) => h(SubpluginCard, { key: item.id, item })),
235
+ h(FangCard, { cooperation: status && status.cooperation }),
236
+ h(LiveFeed),
237
+ h('div', { style: S.desc }, '3.2.7:真实流式(SSE)+ token 轮换/审计 + Worker 并发配额 + 宿主 adapter;未发布。'),
238
+ )),
239
+ )
240
+ }
241
+
242
+ /** sidebar.footer.action 槽:一个「房主」按钮,点击打开状态抽屉。 */
243
+ function MasterSeat() {
244
+ const [open, setOpen] = React.useState(false)
245
+ return h('div', null,
246
+ h('button', { type: 'button', style: S.fab, title: '房主 · 主 Agent 控制面', onClick: () => setOpen(true) }, '◈ 房主'),
247
+ open ? h(MasterPanel, { onClose: () => setOpen(false) }) : null,
248
+ )
249
+ }
250
+
251
+ return {
252
+ inject: ['slots'],
253
+ apply(ctx) {
254
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
255
+ name: 'sidebar.footer.action',
256
+ id: 'dsh-master-home',
257
+ order: 79,
258
+ label: '房主',
259
+ }, MasterSeat))
260
+ },
261
+ }
262
+ },
263
+ })
@@ -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
+ });
@@ -0,0 +1,84 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Fang/dsh-Master contributors — see LICENSE.
3
+ import crypto from 'node:crypto';
4
+ import { ERROR_CODES, PROTOCOL, PROTOCOL_VERSION } from './constants.js';
5
+ export function newId(prefix = 'msg') {
6
+ return `${prefix}_${crypto.randomUUID()}`;
7
+ }
8
+ /** 创建请求信封;未知字段由接收方忽略(兼容规则)。 */
9
+ export function createEnvelope({ method, params = null, id, source = '', instanceId = '', timeoutMs = 30_000, meta = {} } = {}) {
10
+ if (!method || typeof method !== 'string')
11
+ throw new Error('method is required');
12
+ return {
13
+ protocol: PROTOCOL,
14
+ protocolVersion: PROTOCOL_VERSION,
15
+ id: String(id ?? newId('req')),
16
+ method: String(method),
17
+ params: params ?? null,
18
+ meta: {
19
+ at: Date.now(),
20
+ deadline: Date.now() + Math.max(1, Number(timeoutMs) || 30_000),
21
+ source: String(source ?? ''),
22
+ instanceId: String(instanceId ?? ''),
23
+ ...(meta && typeof meta === 'object' ? meta : {}),
24
+ },
25
+ };
26
+ }
27
+ /** 校验请求/响应信封;返回 {ok, errors, envelope}。 */
28
+ export function validateEnvelope(value, { response = false } = {}) {
29
+ const errors = [];
30
+ const raw = value && typeof value === 'object' ? value : {};
31
+ if (raw.protocol !== PROTOCOL)
32
+ errors.push(`protocol-invalid:${String(raw.protocol)}`);
33
+ if (raw.protocolVersion !== PROTOCOL_VERSION)
34
+ errors.push(`protocol-version-invalid:${String(raw.protocolVersion)}`);
35
+ if (!raw.id)
36
+ errors.push('id-required');
37
+ if (response) {
38
+ if (typeof raw.ok !== 'boolean')
39
+ errors.push('ok-required');
40
+ if (raw.ok === false && !raw.error?.code)
41
+ errors.push('error-code-required');
42
+ }
43
+ else if (!raw.method) {
44
+ errors.push('method-required');
45
+ }
46
+ return { ok: errors.length === 0, errors, envelope: raw };
47
+ }
48
+ export function createResponse({ id, result, error, meta = {} } = {}) {
49
+ const base = {
50
+ protocol: PROTOCOL,
51
+ protocolVersion: PROTOCOL_VERSION,
52
+ id: String(id ?? newId('res')),
53
+ ok: !error,
54
+ meta: { at: Date.now(), ...(meta && typeof meta === 'object' ? meta : {}) },
55
+ };
56
+ if (error)
57
+ return { ...base, error: normalizeError(error) };
58
+ return { ...base, result: result ?? null };
59
+ }
60
+ export function normalizeError(error, fallbackCode = ERROR_CODES.REMOTE_ERROR) {
61
+ if (!error)
62
+ return { code: fallbackCode, message: String(fallbackCode), retryable: false };
63
+ if (typeof error === 'string')
64
+ return { code: fallbackCode, message: error, retryable: false };
65
+ return {
66
+ code: String(error.code ?? fallbackCode),
67
+ message: String(error.message ?? error.error ?? fallbackCode),
68
+ retryable: error.retryable === true,
69
+ ...(error.detail === undefined ? {} : { detail: error.detail }),
70
+ };
71
+ }
72
+ export class ContractTransportError extends Error {
73
+ constructor(code, message, { transport = '', detail = null, retryable = false } = {}) {
74
+ super(String(message ?? code));
75
+ this.name = 'ContractTransportError';
76
+ this.code = String(code);
77
+ this.transport = String(transport);
78
+ this.detail = detail;
79
+ this.retryable = retryable === true;
80
+ }
81
+ }
82
+ export function isCompatibleResponse(value) {
83
+ return validateEnvelope(value, { response: true }).ok;
84
+ }