@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.
- package/LICENSE +661 -0
- package/README.md +89 -2
- package/client.js +263 -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 +377 -0
- package/docs/INTEROP.md +60 -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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
|
+
/**
|
|
5
|
+
* Dsh-Master(房主)核心插件 / 主 Agent 控制面。
|
|
6
|
+
*
|
|
7
|
+
* 定位:与瓮城 dsh-Fang 并行的主插件。房主不接管瓮城的镜像/原身/分身生命周期,
|
|
8
|
+
* 只在自己的命名空间里托管三个 Agent 子插件,并通过 DSH 服务键与瓮城协作:
|
|
9
|
+
*
|
|
10
|
+
* ┌──────────────────────────── DSH Host ────────────────────────────┐
|
|
11
|
+
* │ 瓮城 bundle (dsh-Fang) 房主 bundle (dsh-Master) │
|
|
12
|
+
* │ mirror / essence / derived core ─┬─ superagent (fork) │
|
|
13
|
+
* │ fangForker / fangDecisionCenter ├─ cost (fork) │
|
|
14
|
+
* │ └─ dual (mother/essence)
|
|
15
|
+
* └──────────────────────────────────────────────────────────────────┘
|
|
16
|
+
*
|
|
17
|
+
* 协作只走服务键(见 kernel.mjs FANG_SERVICE):房主不 import 瓮城代码;瓮城服务
|
|
18
|
+
* 不可用时子插件必须 fail-closed,绝不本地降级执行。
|
|
19
|
+
*/
|
|
20
|
+
import { FANG_CONTRACT_MIN, FANG_SERVICE, MASTER_VERSION, ROLE_MATRIX, ROW, SERVICE, SUBPLUGIN_CATALOG, bearerOf, detectRole, enabledOf, jsonResponse, masterError, readJsonBody, roleAllowed, safeGet, } from "./kernel.js";
|
|
21
|
+
import { BRIDGE_PATH, filterEnv } from "./superagent/dispatch.js";
|
|
22
|
+
import { createBridgeTokenManager } from "./superagent/bridge-tokens.js";
|
|
23
|
+
export const name = 'dsh-master';
|
|
24
|
+
export const inject = [];
|
|
25
|
+
/** 状态路由(Client UI 读取;只读、非敏感、loopback-only 由宿主 webServer 保证)。 */
|
|
26
|
+
export const STATUS_ROUTE = '/dsh-master/api/status';
|
|
27
|
+
/**
|
|
28
|
+
* 房主核心 apply。
|
|
29
|
+
* @param {object} ctx Cordis context
|
|
30
|
+
* @param {object} [config] loader 行配置
|
|
31
|
+
*/
|
|
32
|
+
export function apply(ctx, config = {}) {
|
|
33
|
+
if (!enabledOf(config))
|
|
34
|
+
return undefined;
|
|
35
|
+
const role = detectRole();
|
|
36
|
+
const startedAt = Date.now();
|
|
37
|
+
// 桥 token:自动轮换 + 宽限重叠 + 摘要持久化 + 审计(v3.2.5)。
|
|
38
|
+
const bridgeTokens = createBridgeTokenManager({
|
|
39
|
+
staticToken: String(config.bridgeToken ?? '').trim(),
|
|
40
|
+
rotateMs: config.bridgeTokenRotateMs,
|
|
41
|
+
graceMs: config.bridgeTokenGraceMs,
|
|
42
|
+
dataDir: String(config.dataDir ?? '').trim(),
|
|
43
|
+
auditLimit: config.tokenAuditLimit,
|
|
44
|
+
});
|
|
45
|
+
const logInfo = (...args) => { try {
|
|
46
|
+
ctx?.logger?.info?.(...args);
|
|
47
|
+
}
|
|
48
|
+
catch { /* ignore */ } };
|
|
49
|
+
const logWarn = (...args) => { try {
|
|
50
|
+
ctx?.logger?.warn?.(...args);
|
|
51
|
+
}
|
|
52
|
+
catch { /* ignore */ } };
|
|
53
|
+
const subplugins = new Map();
|
|
54
|
+
const cooperation = () => {
|
|
55
|
+
const services = {};
|
|
56
|
+
for (const [alias, serviceName] of Object.entries(FANG_SERVICE)) {
|
|
57
|
+
const found = safeGet(ctx, serviceName);
|
|
58
|
+
services[alias] = {
|
|
59
|
+
service: serviceName,
|
|
60
|
+
available: Boolean(found),
|
|
61
|
+
spawn: typeof found?.spawn === 'function',
|
|
62
|
+
decide: typeof found?.decide === 'function' || typeof found?.create === 'function',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
contract: `dsh-fang >= ${FANG_CONTRACT_MIN}`,
|
|
67
|
+
mode: 'service-key-only',
|
|
68
|
+
services,
|
|
69
|
+
available: Object.values(services).some((item) => item.available),
|
|
70
|
+
canSpawn: services.forker.spawn === true,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const catalog = () => SUBPLUGIN_CATALOG.map((entry) => {
|
|
74
|
+
const active = subplugins.get(entry.id) ?? null;
|
|
75
|
+
const allowed = roleAllowed(entry.row, role);
|
|
76
|
+
return {
|
|
77
|
+
id: entry.id,
|
|
78
|
+
row: entry.row,
|
|
79
|
+
name: entry.name,
|
|
80
|
+
nameEn: entry.nameEn,
|
|
81
|
+
roadmap: entry.roadmap,
|
|
82
|
+
description: entry.description,
|
|
83
|
+
allowedRoles: entry.roles,
|
|
84
|
+
roleAllowed: allowed,
|
|
85
|
+
active: Boolean(active) && allowed,
|
|
86
|
+
registered: Boolean(active),
|
|
87
|
+
capabilities: active?.capabilities ?? [],
|
|
88
|
+
blockedReason: active || allowed ? null : `role-not-allowed:${role}`,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
const service = {
|
|
92
|
+
id: SERVICE.master,
|
|
93
|
+
name: 'dsh-master',
|
|
94
|
+
version: MASTER_VERSION,
|
|
95
|
+
role,
|
|
96
|
+
startedAt,
|
|
97
|
+
/** 子插件启动时注册;重复 id 直接抛(fail loud)。 */
|
|
98
|
+
registerSubplugin(descriptor) {
|
|
99
|
+
const id = String(descriptor?.id ?? '').trim();
|
|
100
|
+
if (!id)
|
|
101
|
+
throw new Error('dsh-master: subplugin id is required');
|
|
102
|
+
if (subplugins.has(id))
|
|
103
|
+
throw new Error(`dsh-master: subplugin already registered: ${id}`);
|
|
104
|
+
const record = Object.freeze({
|
|
105
|
+
id,
|
|
106
|
+
name: String(descriptor.name ?? id),
|
|
107
|
+
capabilities: Object.freeze([...(descriptor.capabilities ?? [])].map(String)),
|
|
108
|
+
description: String(descriptor.description ?? ''),
|
|
109
|
+
registeredAt: Date.now(),
|
|
110
|
+
});
|
|
111
|
+
subplugins.set(id, record);
|
|
112
|
+
logInfo(`[dsh-master] subplugin registered: ${id} (${record.capabilities.join(',')})`);
|
|
113
|
+
return () => { subplugins.delete(id); };
|
|
114
|
+
},
|
|
115
|
+
subplugins() {
|
|
116
|
+
return catalog();
|
|
117
|
+
},
|
|
118
|
+
cooperation,
|
|
119
|
+
/** 桥 token 运维:只读描述 / 审计 / 手动轮换(无明文 token)。 */
|
|
120
|
+
bridgeTokens: () => bridgeTokens.describe(),
|
|
121
|
+
bridgeTokenAudit: (limit) => bridgeTokens.audit(limit),
|
|
122
|
+
rotateBridgeToken: () => {
|
|
123
|
+
const rotated = bridgeTokens.rotate({ reason: 'manual' });
|
|
124
|
+
return { ok: true, token: rotated.token, tokenId: rotated.tokenId, graceMs: rotated.graceMs };
|
|
125
|
+
},
|
|
126
|
+
/** 统一状态:Client 状态页与工具/测试共用。 */
|
|
127
|
+
status() {
|
|
128
|
+
const dispatchCfg = config.dispatch && typeof config.dispatch === 'object' ? config.dispatch : {};
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
id: 'dsh-master',
|
|
132
|
+
version: MASTER_VERSION,
|
|
133
|
+
role,
|
|
134
|
+
startedAt,
|
|
135
|
+
uptimeMs: Date.now() - startedAt,
|
|
136
|
+
cooperation: cooperation(),
|
|
137
|
+
subplugins: catalog(),
|
|
138
|
+
roleMatrix: ROLE_MATRIX,
|
|
139
|
+
bridge: {
|
|
140
|
+
enabled: Boolean(String(config.bridgeToken ?? '').trim()) || bridgeTokens.describe().tokens.length > 0,
|
|
141
|
+
dispatchConfigured: Boolean(String(dispatchCfg.homeDir ?? '').trim() && String(dispatchCfg.cliPath ?? '').trim()),
|
|
142
|
+
tokens: bridgeTokens.describe(),
|
|
143
|
+
tokenAudit: bridgeTokens.audit(10),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
/** 房主自检:缺瓮城服务时给出可读原因,不抛。 */
|
|
148
|
+
health() {
|
|
149
|
+
const coop = cooperation();
|
|
150
|
+
const warnings = [];
|
|
151
|
+
if (!coop.available)
|
|
152
|
+
warnings.push('fang-unavailable: 瓮城 bundle 未加载或版本低于契约');
|
|
153
|
+
else if (!coop.canSpawn)
|
|
154
|
+
warnings.push('fang-forker-spawn-unavailable');
|
|
155
|
+
return {
|
|
156
|
+
ok: warnings.length === 0,
|
|
157
|
+
version: MASTER_VERSION,
|
|
158
|
+
role,
|
|
159
|
+
warnings,
|
|
160
|
+
cooperation: coop,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
ctx.effect(() => ctx.provide(SERVICE.master, service));
|
|
166
|
+
}
|
|
167
|
+
catch { /* fake ctx */ }
|
|
168
|
+
try {
|
|
169
|
+
ctx.effect(() => () => bridgeTokens.stop());
|
|
170
|
+
}
|
|
171
|
+
catch { /* fake ctx */ }
|
|
172
|
+
try {
|
|
173
|
+
if (typeof ctx.provide === 'function' && typeof ctx.effect !== 'function')
|
|
174
|
+
ctx.provide(SERVICE.master, service);
|
|
175
|
+
}
|
|
176
|
+
catch { /* ignore */ }
|
|
177
|
+
// Client 状态页数据源:只读、limit 到 non-sensitive 字段。
|
|
178
|
+
try {
|
|
179
|
+
if (typeof ctx.inject === 'function') {
|
|
180
|
+
ctx.inject(['webServer'], (child) => {
|
|
181
|
+
const webServer = child?.get?.('webServer') ?? child?.webServer;
|
|
182
|
+
if (!webServer || typeof webServer.register !== 'function')
|
|
183
|
+
return;
|
|
184
|
+
try {
|
|
185
|
+
webServer.register({
|
|
186
|
+
kind: 'exact',
|
|
187
|
+
path: STATUS_ROUTE,
|
|
188
|
+
method: 'GET',
|
|
189
|
+
handler: async (req, res) => jsonResponse(res, 200, service.status()),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
logWarn(`[dsh-master] status route registration failed: ${String(error?.message ?? error)}`);
|
|
194
|
+
}
|
|
195
|
+
// v3.2.3:原身 Router → 母体 forker 的跨进程派发桥。
|
|
196
|
+
try {
|
|
197
|
+
webServer.register({
|
|
198
|
+
kind: 'exact',
|
|
199
|
+
path: BRIDGE_PATH,
|
|
200
|
+
method: 'POST',
|
|
201
|
+
handler: async (req, res) => {
|
|
202
|
+
const bridge = bridgeTokens.describe();
|
|
203
|
+
if (!bridge.enabled)
|
|
204
|
+
return jsonResponse(res, 503, masterError('bridge-disabled', 'bridgeToken is not configured in this host'));
|
|
205
|
+
const verified = bridgeTokens.verify(bearerOf(req?.headers));
|
|
206
|
+
if (!verified.ok) {
|
|
207
|
+
return jsonResponse(res, 401, masterError('bridge-unauthorized', 'invalid bridge token'), { reason: verified.reason });
|
|
208
|
+
}
|
|
209
|
+
const parsed = await readJsonBody(req);
|
|
210
|
+
if (!parsed.ok) {
|
|
211
|
+
return jsonResponse(res, parsed.code === 'body-too-large' ? 413 : 400, masterError(parsed.code, 'invalid bridge request body'));
|
|
212
|
+
}
|
|
213
|
+
const ticket = parsed.body?.ticket;
|
|
214
|
+
if (!ticket || typeof ticket !== 'object' || typeof ticket.id !== 'string' || !ticket.id) {
|
|
215
|
+
return jsonResponse(res, 400, masterError('ticket-invalid', 'bridge request must include a ticket with id'));
|
|
216
|
+
}
|
|
217
|
+
const forker = safeGet(ctx, FANG_SERVICE.forker);
|
|
218
|
+
if (!forker || typeof forker.spawn !== 'function') {
|
|
219
|
+
return jsonResponse(res, 503, masterError('fang-forker-unavailable', 'fangForker.spawn is not available in this host'));
|
|
220
|
+
}
|
|
221
|
+
const dispatchCfg = config.dispatch && typeof config.dispatch === 'object' ? config.dispatch : {};
|
|
222
|
+
const homeDir = String(dispatchCfg.homeDir ?? '').trim();
|
|
223
|
+
const cliPath = String(dispatchCfg.cliPath ?? '').trim();
|
|
224
|
+
const profile = String(dispatchCfg.profile ?? '').trim() || 'default';
|
|
225
|
+
if (!homeDir || !cliPath) {
|
|
226
|
+
return jsonResponse(res, 503, masterError('dispatch-config-incomplete', 'bridge host requires dispatch.homeDir and dispatch.cliPath'));
|
|
227
|
+
}
|
|
228
|
+
const env = { ...filterEnv(parsed.body?.env), DSH_MASTER_TICKET: JSON.stringify(ticket) };
|
|
229
|
+
try {
|
|
230
|
+
const fork = await forker.spawn({ homeDir, cliPath, profile, taskTitle: `SuperAgent ${ticket.id}`, env });
|
|
231
|
+
if (fork && fork.ok === false) {
|
|
232
|
+
return jsonResponse(res, 502, { ...masterError('fang-spawn-failed', 'forker.spawn returned an error', fork), fork });
|
|
233
|
+
}
|
|
234
|
+
return jsonResponse(res, 200, { ok: true, fork });
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return jsonResponse(res, 500, masterError('fang-spawn-threw', String(error?.message ?? error)));
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
logWarn(`[dsh-master] bridge route registration failed: ${String(error?.message ?? error)}`);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { /* 宿主没有 webServer 时状态仍可通过服务读取 */ }
|
|
249
|
+
logInfo(`[dsh-master] core ready version=${MASTER_VERSION} role=${role}`);
|
|
250
|
+
return service;
|
|
251
|
+
}
|
|
252
|
+
/** 给子插件复用:取核心服务;不可用时返回 error(启动阶段应 fail loud)。 */
|
|
253
|
+
export function requireMaster(ctx) {
|
|
254
|
+
const master = safeGet(ctx, SERVICE.master);
|
|
255
|
+
if (!master || typeof master.registerSubplugin !== 'function') {
|
|
256
|
+
return { ok: false, error: masterError('master-core-unavailable', 'dsh-master core service is not available') };
|
|
257
|
+
}
|
|
258
|
+
return { ok: true, master };
|
|
259
|
+
}
|
|
260
|
+
export { ROLE_MATRIX, ROW, SERVICE, SUBPLUGIN_CATALOG };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
|
+
/**
|
|
5
|
+
* Dsh-Master(房主)共享内核(v3.2.3)。
|
|
6
|
+
* 只放纯函数与常量;Node 侧工具用到的 crypto 是唯一内置模块依赖。
|
|
7
|
+
* 与瓮城 dsh-Fang 的协作只走 DSH 服务键,不 import fang 代码。
|
|
8
|
+
*/
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
export const MASTER_VERSION = '3.2.8';
|
|
11
|
+
export const FANG_CONTRACT_MIN = '3.1.14';
|
|
12
|
+
export const SERVICE = Object.freeze({
|
|
13
|
+
master: 'dshMaster',
|
|
14
|
+
superagentRouter: 'dshMasterSuperagentRouter',
|
|
15
|
+
superagentWorker: 'dshMasterSuperagentWorker',
|
|
16
|
+
cost: 'dshMasterCost',
|
|
17
|
+
dual: 'dshMasterDual',
|
|
18
|
+
});
|
|
19
|
+
export const FANG_SERVICE = Object.freeze({
|
|
20
|
+
orchestrator: 'fang',
|
|
21
|
+
forker: 'fangForker',
|
|
22
|
+
decisionCenter: 'fangDecisionCenter',
|
|
23
|
+
sessionCatalog: 'fangSessionCatalog',
|
|
24
|
+
sessionBridge: 'fangSessionBridge',
|
|
25
|
+
});
|
|
26
|
+
export const ROW = Object.freeze({
|
|
27
|
+
core: 'master-core',
|
|
28
|
+
superagentRouter: 'master-superagent-router',
|
|
29
|
+
superagentWorker: 'master-superagent-worker',
|
|
30
|
+
superagentLoop: 'master-superagent-loop',
|
|
31
|
+
cost: 'master-cost-optimizer',
|
|
32
|
+
dual: 'master-dual-manager',
|
|
33
|
+
});
|
|
34
|
+
export const ROLE = Object.freeze({ mother: 'mother', essence: 'essence', fork: 'fork' });
|
|
35
|
+
/**
|
|
36
|
+
* 角色矩阵(v3.2.3):
|
|
37
|
+
* * Router 常驻原身(essence):只评估/建票/派发,不执行代码;母体不接纳。
|
|
38
|
+
* * Worker 是研究原型,只在 fork:编译后的 Worker preset 行 master-superagent-loop 也只 fork。
|
|
39
|
+
* * 成本优化 fork;双主管 mother/essence。
|
|
40
|
+
*/
|
|
41
|
+
export const ROLE_MATRIX = Object.freeze({
|
|
42
|
+
'master-core': Object.freeze(Object.values(ROLE)),
|
|
43
|
+
'master-superagent-router': Object.freeze([ROLE.essence]),
|
|
44
|
+
'master-superagent-worker': Object.freeze([ROLE.fork]),
|
|
45
|
+
'master-superagent-loop': Object.freeze([ROLE.fork]),
|
|
46
|
+
'master-cost-optimizer': Object.freeze([ROLE.fork]),
|
|
47
|
+
'master-dual-manager': Object.freeze([ROLE.mother, ROLE.essence]),
|
|
48
|
+
});
|
|
49
|
+
export const SUBPLUGIN_CATALOG = Object.freeze([
|
|
50
|
+
Object.freeze({
|
|
51
|
+
id: 'superagent-router', row: ROW.superagentRouter, name: '超级智能体 Router', nameEn: 'SuperAgent Router',
|
|
52
|
+
roles: ROLE_MATRIX[ROW.superagentRouter], roadmap: '3.2.1',
|
|
53
|
+
description: '常驻原身:Webhook → 评估 → Ticket/Mandate → 派发;不执行代码改动。',
|
|
54
|
+
}),
|
|
55
|
+
Object.freeze({
|
|
56
|
+
id: 'superagent-worker', row: ROW.superagentWorker, name: '超级智能体 Worker', nameEn: 'SuperAgent Worker',
|
|
57
|
+
roles: ROLE_MATRIX[ROW.superagentWorker], roadmap: '3.2.1',
|
|
58
|
+
description: '每个工单一个隔离分身:在 Mandate 作用域内执行;研究原型,仅 fork。',
|
|
59
|
+
}),
|
|
60
|
+
Object.freeze({
|
|
61
|
+
id: 'superagent-loop', row: ROW.superagentLoop, name: 'Worker Agent Loop', nameEn: 'SuperAgent Worker Loop',
|
|
62
|
+
roles: ROLE_MATRIX[ROW.superagentLoop], roadmap: '3.2.3',
|
|
63
|
+
description: '编译后的 Worker preset:有 Ticket 的 fork 自动跑多轮工具循环并流式回传。',
|
|
64
|
+
}),
|
|
65
|
+
Object.freeze({
|
|
66
|
+
id: 'cost-optimizer', row: ROW.cost, name: '成本优化', nameEn: 'HarnessMM',
|
|
67
|
+
roles: ROLE_MATRIX[ROW.cost], roadmap: '3.3.0',
|
|
68
|
+
description: 'HarnessMM observe 层;仅 fork,默认 observe。',
|
|
69
|
+
}),
|
|
70
|
+
Object.freeze({
|
|
71
|
+
id: 'dual-manager', row: ROW.dual, name: '双主管', nameEn: 'PM + PJM',
|
|
72
|
+
roles: ROLE_MATRIX[ROW.dual], roadmap: '3.4.0',
|
|
73
|
+
description: 'PM 产 PRD、PJM 调度分身;只允许 mother / essence。',
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
/**
|
|
77
|
+
* 从环境变量探测当前角色。
|
|
78
|
+
* 瓮城注入:母体 DSH_FANG_ROLE=mother;元宝运行态 fork+DSH_FANG_ESSENCE=1;
|
|
79
|
+
* 普通分身 DSH_FANG_ROLE=fork + DSH_FANG_TASK_ID=task_/rt-。
|
|
80
|
+
*/
|
|
81
|
+
export function detectRole(env = process.env) {
|
|
82
|
+
const raw = String(env?.DSH_FANG_ROLE ?? '').trim().toLowerCase();
|
|
83
|
+
const taskId = String(env?.DSH_FANG_TASK_ID ?? '');
|
|
84
|
+
const essence = String(env?.DSH_FANG_ESSENCE ?? '') === '1' || taskId.startsWith('rt-');
|
|
85
|
+
if (raw === 'mother')
|
|
86
|
+
return ROLE.mother;
|
|
87
|
+
if (raw === 'fork')
|
|
88
|
+
return essence ? ROLE.essence : ROLE.fork;
|
|
89
|
+
return essence ? ROLE.essence : ROLE.mother;
|
|
90
|
+
}
|
|
91
|
+
export function roleAllowed(rowId, role) {
|
|
92
|
+
const allowed = ROLE_MATRIX[rowId];
|
|
93
|
+
return Array.isArray(allowed) ? allowed.includes(role) : true;
|
|
94
|
+
}
|
|
95
|
+
export function enabledOf(config) {
|
|
96
|
+
return !(config && typeof config === 'object' && config.enabled === false);
|
|
97
|
+
}
|
|
98
|
+
export function safeGet(ctx, name) {
|
|
99
|
+
try {
|
|
100
|
+
if (!ctx || typeof ctx !== 'object')
|
|
101
|
+
return null;
|
|
102
|
+
if (typeof ctx.get === 'function')
|
|
103
|
+
return ctx.get(name) ?? null;
|
|
104
|
+
return ctx[name] ?? null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function jsonResponse(res, status, body) {
|
|
111
|
+
try {
|
|
112
|
+
if (res && typeof res.statusCode === 'number')
|
|
113
|
+
res.statusCode = status;
|
|
114
|
+
res?.setHeader?.('content-type', 'application/json; charset=utf-8');
|
|
115
|
+
res?.setHeader?.('cache-control', 'no-store');
|
|
116
|
+
res?.end?.(JSON.stringify(body));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* ignore */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** 读取请求 JSON body(大小上限;非法/超限返回 {ok:false,code})。 */
|
|
123
|
+
export async function readJsonBody(req, limit = 512 * 1024) {
|
|
124
|
+
try {
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
if (req && typeof req[Symbol.asyncIterator] === 'function') {
|
|
128
|
+
for await (const chunk of req) {
|
|
129
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
130
|
+
total += buffer.length;
|
|
131
|
+
if (total > limit)
|
|
132
|
+
return { ok: false, code: 'body-too-large' };
|
|
133
|
+
chunks.push(buffer);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (req && typeof req.on === 'function') {
|
|
137
|
+
await new Promise((resolve, reject) => {
|
|
138
|
+
req.on('data', (chunk) => {
|
|
139
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
140
|
+
total += buffer.length;
|
|
141
|
+
if (total > limit)
|
|
142
|
+
reject(new Error('body-too-large'));
|
|
143
|
+
else
|
|
144
|
+
chunks.push(buffer);
|
|
145
|
+
});
|
|
146
|
+
req.on('end', resolve);
|
|
147
|
+
req.on('error', reject);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
151
|
+
if (!raw)
|
|
152
|
+
return { ok: true, body: {} };
|
|
153
|
+
return { ok: true, body: JSON.parse(raw) };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return { ok: false, code: String(error?.message ?? error) === 'body-too-large' ? 'body-too-large' : 'invalid-json' };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** 常量时间比较两个 token(长度不同直接 false)。 */
|
|
160
|
+
export function tokenEquals(received, expected) {
|
|
161
|
+
const a = Buffer.from(String(received ?? ''), 'utf8');
|
|
162
|
+
const b = Buffer.from(String(expected ?? ''), 'utf8');
|
|
163
|
+
if (a.length === 0 || a.length !== b.length)
|
|
164
|
+
return false;
|
|
165
|
+
return crypto.timingSafeEqual(a, b);
|
|
166
|
+
}
|
|
167
|
+
export function bearerOf(headers) {
|
|
168
|
+
const raw = headers && typeof headers === 'object'
|
|
169
|
+
? (headers.authorization ?? headers.Authorization ?? '')
|
|
170
|
+
: '';
|
|
171
|
+
const match = /^Bearer\s+(\S+)$/i.exec(String(raw ?? '').trim());
|
|
172
|
+
return match ? match[1] : '';
|
|
173
|
+
}
|
|
174
|
+
export function masterError(code, message, detail) {
|
|
175
|
+
const out = { ok: false, code: String(code), error: String(message ?? code) };
|
|
176
|
+
if (detail !== undefined)
|
|
177
|
+
out.detail = detail;
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
export function createIdFactory(prefix) {
|
|
181
|
+
let seq = 0;
|
|
182
|
+
return () => {
|
|
183
|
+
seq += 1;
|
|
184
|
+
return `${prefix}_${Date.now().toString(36)}_${seq.toString(36)}`;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
|
+
/**
|
|
5
|
+
* 子插件 2:成本优化(HarnessMM 观察层)—— 为分身装“燃油表”。
|
|
6
|
+
*
|
|
7
|
+
* 3.2.0 先交付可观测/可估算的骨架,不做推理拦截:
|
|
8
|
+
* * `observe` 模式只统计/估算,不改写任何模型调用;
|
|
9
|
+
* * HarnessMM 的 EGD/GDR/CCS 真实接入在 3.3.0 里程碑(见 docs/ROADMAP.md);
|
|
10
|
+
* * 角色:只允许 fork(分身生产盘),母体/元主不加载。
|
|
11
|
+
*/
|
|
12
|
+
import { ROW, ROLE, SERVICE, detectRole, enabledOf, masterError, roleAllowed, safeGet, } from "../kernel.js";
|
|
13
|
+
export const name = 'dsh-master-cost-optimizer';
|
|
14
|
+
export const inject = ['dshMaster'];
|
|
15
|
+
export const ROW_ID = ROW.cost;
|
|
16
|
+
/** HarnessMM 官方基准(3.2.0 文档引用;3.3.0 接入后应被实测值覆盖)。 */
|
|
17
|
+
export const HARNESSMM_BENCHMARK = Object.freeze({
|
|
18
|
+
baselineTokens: 96280,
|
|
19
|
+
optimizedTokens: 11620,
|
|
20
|
+
savingsRatio: 0.8793,
|
|
21
|
+
successRate: { baseline: 0.7143, optimized: 0.8571 },
|
|
22
|
+
});
|
|
23
|
+
function finiteNonNegative(value) {
|
|
24
|
+
const n = Number(value);
|
|
25
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} ctx
|
|
29
|
+
* @param {{enabled?:boolean, mode?:string, targetSavingsRatio?:number}} [config]
|
|
30
|
+
*/
|
|
31
|
+
export function apply(ctx, config = {}) {
|
|
32
|
+
if (!enabledOf(config))
|
|
33
|
+
return { skipped: true, reason: 'disabled-by-config' };
|
|
34
|
+
const role = detectRole();
|
|
35
|
+
if (!roleAllowed(ROW_ID, role)) {
|
|
36
|
+
try {
|
|
37
|
+
ctx?.logger?.info?.(`[dsh-master] cost-optimizer skipped: role=${role} not in ${ROLE.fork}`);
|
|
38
|
+
}
|
|
39
|
+
catch { /* ignore */ }
|
|
40
|
+
return { skipped: true, reason: `role-not-allowed:${role}` };
|
|
41
|
+
}
|
|
42
|
+
const master = safeGet(ctx, SERVICE.master);
|
|
43
|
+
if (!master || typeof master.registerSubplugin !== 'function') {
|
|
44
|
+
return { skipped: true, reason: 'master-core-unavailable' };
|
|
45
|
+
}
|
|
46
|
+
const targetSavingsRatio = finiteNonNegative(config.targetSavingsRatio);
|
|
47
|
+
const policy = Object.freeze({
|
|
48
|
+
mode: 'observe', // 3.2.0 只允许 observe;replay 在 3.3.0 打开
|
|
49
|
+
requestedMode: String(config.mode ?? 'observe'),
|
|
50
|
+
targetSavingsRatio: targetSavingsRatio !== null && targetSavingsRatio <= 1 ? targetSavingsRatio : 0.8,
|
|
51
|
+
});
|
|
52
|
+
const metrics = {
|
|
53
|
+
tasks: 0,
|
|
54
|
+
baselineTokens: 0,
|
|
55
|
+
optimizedTokens: 0,
|
|
56
|
+
savedTokens: 0,
|
|
57
|
+
observedSuccess: 0,
|
|
58
|
+
observedTotal: 0,
|
|
59
|
+
};
|
|
60
|
+
const service = {
|
|
61
|
+
id: SERVICE.cost,
|
|
62
|
+
version: '3.2.5',
|
|
63
|
+
role,
|
|
64
|
+
mode: policy.mode,
|
|
65
|
+
/** 基于 HarnessMM 基准 + 目标比例做派生前预估。 */
|
|
66
|
+
estimate({ baselineTokens } = {}) {
|
|
67
|
+
const baseline = finiteNonNegative(baselineTokens);
|
|
68
|
+
if (baseline === null)
|
|
69
|
+
return masterError('baseline-tokens-invalid', 'baselineTokens must be a non-negative number');
|
|
70
|
+
const projectedOptimized = Math.round(baseline * (1 - policy.targetSavingsRatio));
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
mode: policy.mode,
|
|
74
|
+
baselineTokens: baseline,
|
|
75
|
+
targetSavingsRatio: policy.targetSavingsRatio,
|
|
76
|
+
projectedOptimizedTokens: projectedOptimized,
|
|
77
|
+
projectedSavingsTokens: baseline - projectedOptimized,
|
|
78
|
+
basis: {
|
|
79
|
+
source: 'HarnessMM benchmark',
|
|
80
|
+
baselineTokens: HARNESSMM_BENCHMARK.baselineTokens,
|
|
81
|
+
optimizedTokens: HARNESSMM_BENCHMARK.optimizedTokens,
|
|
82
|
+
savingsRatio: HARNESSMM_BENCHMARK.savingsRatio,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
/** 归档后记录实测;3.3.0 起由 HarnessMM 的 EGD/GDR/CCS 自动上报。 */
|
|
87
|
+
record({ taskId, baselineTokens, optimizedTokens, success } = {}) {
|
|
88
|
+
const baseline = finiteNonNegative(baselineTokens);
|
|
89
|
+
const optimized = finiteNonNegative(optimizedTokens);
|
|
90
|
+
if (baseline === null)
|
|
91
|
+
return masterError('baseline-tokens-invalid', 'baselineTokens must be a non-negative number');
|
|
92
|
+
if (optimized === null)
|
|
93
|
+
return masterError('optimized-tokens-invalid', 'optimizedTokens must be a non-negative number');
|
|
94
|
+
if (optimized > baseline)
|
|
95
|
+
return masterError('optimized-exceeds-baseline', 'optimizedTokens must not exceed baselineTokens');
|
|
96
|
+
metrics.tasks += 1;
|
|
97
|
+
metrics.baselineTokens += baseline;
|
|
98
|
+
metrics.optimizedTokens += optimized;
|
|
99
|
+
metrics.savedTokens += baseline - optimized;
|
|
100
|
+
if (typeof success === 'boolean') {
|
|
101
|
+
metrics.observedTotal += 1;
|
|
102
|
+
if (success)
|
|
103
|
+
metrics.observedSuccess += 1;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
taskId: taskId === undefined ? null : String(taskId),
|
|
108
|
+
savedTokens: baseline - optimized,
|
|
109
|
+
summary: service.summary(),
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
summary() {
|
|
113
|
+
const ratio = metrics.baselineTokens > 0 ? metrics.savedTokens / metrics.baselineTokens : 0;
|
|
114
|
+
const successRate = metrics.observedTotal > 0 ? metrics.observedSuccess / metrics.observedTotal : null;
|
|
115
|
+
return { ...metrics, savingsRatio: ratio, successRate };
|
|
116
|
+
},
|
|
117
|
+
reset() {
|
|
118
|
+
Object.assign(metrics, { tasks: 0, baselineTokens: 0, optimizedTokens: 0, savedTokens: 0, observedSuccess: 0, observedTotal: 0 });
|
|
119
|
+
return { ok: true };
|
|
120
|
+
},
|
|
121
|
+
status() {
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
id: 'cost-optimizer',
|
|
125
|
+
version: '3.2.5',
|
|
126
|
+
role,
|
|
127
|
+
mode: policy.mode,
|
|
128
|
+
requestedMode: policy.requestedMode,
|
|
129
|
+
targetSavingsRatio: policy.targetSavingsRatio,
|
|
130
|
+
benchmark: HARNESSMM_BENCHMARK,
|
|
131
|
+
summary: service.summary(),
|
|
132
|
+
note: '3.2.0 observe-only:不拦截/不改写模型调用;EGD/GDR/CCS 在 3.3.0 接入。',
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
ctx.effect(() => ctx.provide(SERVICE.cost, service));
|
|
138
|
+
}
|
|
139
|
+
catch { /* fake ctx */ }
|
|
140
|
+
master.registerSubplugin({
|
|
141
|
+
id: 'cost-optimizer',
|
|
142
|
+
name: 'dsh-master-cost-optimizer',
|
|
143
|
+
capabilities: ['token-estimate', 'savings-record', 'cost-status'],
|
|
144
|
+
description: 'HarnessMM observe 层(仅 fork)',
|
|
145
|
+
});
|
|
146
|
+
return service;
|
|
147
|
+
}
|