dsh-agy-swarm 0.1.0 → 0.1.1
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/README.md +3 -2
- package/client.js +60 -7
- package/docs/changelog.md +24 -3
- package/docs/development.md +5 -3
- package/index.js +83 -56
- package/package.json +2 -1
- package/runtime.js +106 -29
package/README.md
CHANGED
|
@@ -137,12 +137,13 @@ docs/ 配置、参考、开发与变更日志
|
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
139
|
npm run check # 全量 node --check
|
|
140
|
-
npm test # 离线主套件(含内置 ACP
|
|
140
|
+
npm test # 离线主套件(含内置 ACP 替身;Windows 沙盒用)
|
|
141
|
+
npm run test:ci # 同一套件的 CI 版,不带 --test-isolation
|
|
141
142
|
npm run test:acp-integration # 可选:真实 dsh-agy-acp 端到端
|
|
142
143
|
npm pack --dry-run --json # 打包清单预演
|
|
143
144
|
```
|
|
144
145
|
|
|
145
|
-
Windows 沙盒环境跑 `node --test` 必须带 `--test-isolation=none`(子进程命名管道在受限令牌下会 `spawn EPERM
|
|
146
|
+
Windows 沙盒环境跑 `node --test` 必须带 `--test-isolation=none`(子进程命名管道在受限令牌下会 `spawn EPERM`);Linux / macOS 反而不能带(Node 22.13 之前不认该参数,会以 `exit code 9` 失败),CI 因此统一走 `npm run test:ci`。
|
|
146
147
|
|
|
147
148
|
## 许可
|
|
148
149
|
|
package/client.js
CHANGED
|
@@ -235,6 +235,20 @@
|
|
|
235
235
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* 解包一元 Remote(Remote 直调)调用的宿主响应信封。
|
|
240
|
+
*
|
|
241
|
+
* `@deepseek-ai/dsh-api-gateway` 会把一元 Remote 的结果包成 `{ ok: true, value }`;
|
|
242
|
+
* 插件自带的 `SwarmRemoteServiceFallback`(以及离线单测里的替身)直接返回裸结果。
|
|
243
|
+
* 两种形态都必须接受,否则带信封的响应会被 `parseSwarmModelsResult` /
|
|
244
|
+
* `normalizeSwarmSnapshot` 判成非法载荷(典型症状:设置页报
|
|
245
|
+
* "models must be an array")。
|
|
246
|
+
*/
|
|
247
|
+
function unwrapRemoteResult(res) {
|
|
248
|
+
if (isPlainObject(res) && res.ok === true && 'value' in res && res.value !== undefined) return res.value;
|
|
249
|
+
return res;
|
|
250
|
+
}
|
|
251
|
+
|
|
238
252
|
function safeDecodeSegment(segment) {
|
|
239
253
|
if (typeof segment !== 'string' || segment === '') return null;
|
|
240
254
|
try {
|
|
@@ -1307,6 +1321,22 @@
|
|
|
1307
1321
|
return String(error);
|
|
1308
1322
|
}
|
|
1309
1323
|
|
|
1324
|
+
/**
|
|
1325
|
+
* 模型目录拉取失败的原因摘要:压成单行并限长,只用于设置页 status 的 tooltip,
|
|
1326
|
+
* 既方便排查(例如宿主流未挂载 / 协议校验失败),又不会把长堆栈塞进 DOM。
|
|
1327
|
+
*/
|
|
1328
|
+
function describeModelsError(error) {
|
|
1329
|
+
let text = '';
|
|
1330
|
+
try {
|
|
1331
|
+
text = formatErrorMessage(error);
|
|
1332
|
+
} catch {
|
|
1333
|
+
text = '';
|
|
1334
|
+
}
|
|
1335
|
+
text = String(text || '').replace(/\s+/g, ' ').trim();
|
|
1336
|
+
if (!text) return 'unknown error';
|
|
1337
|
+
return text.length > 240 ? text.slice(0, 237) + '...' : text;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1310
1340
|
function makeRemoteFailureFrame(code, message, details) {
|
|
1311
1341
|
const error = new Error(message);
|
|
1312
1342
|
error.name = 'RemoteError';
|
|
@@ -1469,6 +1499,8 @@
|
|
|
1469
1499
|
Array.isArray(props?.models) ? props.models.map(normalizeSwarmModelEntry) : null
|
|
1470
1500
|
);
|
|
1471
1501
|
const [modelsStatus, setModelsStatus] = useState('');
|
|
1502
|
+
/** 拉取失败时的可读原因(单行摘要,挂在 status 的 tooltip 上,不阻塞设置页)。 */
|
|
1503
|
+
const [modelsError, setModelsError] = useState('');
|
|
1472
1504
|
const loadModels = typeof props?.loadModels === 'function' ? props.loadModels : null;
|
|
1473
1505
|
useEffect(() => {
|
|
1474
1506
|
if (!loadModels) return undefined;
|
|
@@ -1479,10 +1511,12 @@
|
|
|
1479
1511
|
if (cancelled || !alive.current) return;
|
|
1480
1512
|
setDiscoveredModels(Array.isArray(list) ? list.map(normalizeSwarmModelEntry) : []);
|
|
1481
1513
|
setModelsStatus('');
|
|
1514
|
+
setModelsError('');
|
|
1482
1515
|
})
|
|
1483
|
-
.catch(() => {
|
|
1516
|
+
.catch((error) => {
|
|
1484
1517
|
if (cancelled || !alive.current) return;
|
|
1485
1518
|
setModelsStatus('modelsUnavailable');
|
|
1519
|
+
setModelsError(describeModelsError(error));
|
|
1486
1520
|
});
|
|
1487
1521
|
return () => {
|
|
1488
1522
|
cancelled = true;
|
|
@@ -1689,9 +1723,13 @@
|
|
|
1689
1723
|
if (!alive.current) return undefined;
|
|
1690
1724
|
setDiscoveredModels(Array.isArray(list) ? list.map(normalizeSwarmModelEntry) : []);
|
|
1691
1725
|
setModelsStatus('');
|
|
1726
|
+
setModelsError('');
|
|
1692
1727
|
return true;
|
|
1693
|
-
} catch {
|
|
1694
|
-
if (alive.current)
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
if (alive.current) {
|
|
1730
|
+
setModelsStatus('modelsUnavailable');
|
|
1731
|
+
setModelsError(describeModelsError(error));
|
|
1732
|
+
}
|
|
1695
1733
|
return false;
|
|
1696
1734
|
}
|
|
1697
1735
|
};
|
|
@@ -1754,6 +1792,18 @@
|
|
|
1754
1792
|
const manualDraft = Object.prototype.hasOwnProperty.call(drafts, 'manualModelRule')
|
|
1755
1793
|
? drafts.manualModelRule
|
|
1756
1794
|
: '';
|
|
1795
|
+
/**
|
|
1796
|
+
* 模型目录状态行:文字只保留本地化摘要,失败原因放进 `title`(tooltip),
|
|
1797
|
+
* 让用户能一眼看出是「宿主流未挂载」还是「协议校验失败」。
|
|
1798
|
+
*/
|
|
1799
|
+
const modelsStatusText = modelsStatus
|
|
1800
|
+
? t(modelsStatus)
|
|
1801
|
+
: (discoveredModels ? '' : t('modelsLoading'));
|
|
1802
|
+
const modelsStatusProps = {
|
|
1803
|
+
className: 'dsh-swarm-settings-status' + (modelsError ? ' is-error' : ''),
|
|
1804
|
+
'data-role': 'models-status',
|
|
1805
|
+
...(modelsError ? { title: modelsError, 'data-models-error': modelsError } : {}),
|
|
1806
|
+
};
|
|
1757
1807
|
|
|
1758
1808
|
return h('section', { className: 'dsh-swarm-settings', 'data-settings-id': SWARM_SETTINGS_ID, 'aria-label': t('title') },
|
|
1759
1809
|
h('h2', null, t('title')),
|
|
@@ -1798,8 +1848,7 @@
|
|
|
1798
1848
|
h('p', { className: 'dsh-swarm-settings-hint', 'data-role': 'whitelist-hint' },
|
|
1799
1849
|
whitelistMode ? t('whitelistHintOn') : t('whitelistHintOff')),
|
|
1800
1850
|
h('div', { className: 'dsh-swarm-settings-row' },
|
|
1801
|
-
h('p',
|
|
1802
|
-
modelsStatus ? t(modelsStatus) : (discoveredModels ? '' : t('modelsLoading'))),
|
|
1851
|
+
h('p', modelsStatusProps, modelsStatusText),
|
|
1803
1852
|
h('button', {
|
|
1804
1853
|
type: 'button',
|
|
1805
1854
|
className: 'dsh-swarm-settings-chip',
|
|
@@ -2165,7 +2214,8 @@
|
|
|
2165
2214
|
if (!api || typeof api.getSwarmSnapshot !== 'function') {
|
|
2166
2215
|
throw new Error('Host 端 dsh-swarm.getSwarmSnapshot 服务未就绪');
|
|
2167
2216
|
}
|
|
2168
|
-
|
|
2217
|
+
// 宿主 api-gateway 会包 `{ ok: true, value }` 信封,先解包再做归一化。
|
|
2218
|
+
const response = unwrapRemoteResult(await api.getSwarmSnapshot({ sessionId }));
|
|
2169
2219
|
if (isPlainObject(response) && response.ok === false) {
|
|
2170
2220
|
throw new Error(formatErrorMessage(response.error || response));
|
|
2171
2221
|
}
|
|
@@ -3407,7 +3457,8 @@
|
|
|
3407
3457
|
if (!api || typeof api.getSwarmModels !== 'function') {
|
|
3408
3458
|
throw new Error('Host 端 dsh-swarm.getSwarmModels 服务未就绪');
|
|
3409
3459
|
}
|
|
3410
|
-
|
|
3460
|
+
// 同 fetchSnapshotOnce:真实宿主的响应带 `{ ok: true, value }` 信封。
|
|
3461
|
+
const response = unwrapRemoteResult(await api.getSwarmModels({ probeAgy: false }));
|
|
3411
3462
|
if (isPlainObject(response) && response.ok === false) {
|
|
3412
3463
|
throw new Error(formatErrorMessage(response.error || response));
|
|
3413
3464
|
}
|
|
@@ -3545,6 +3596,8 @@
|
|
|
3545
3596
|
parseSwarmResourceUri: parseSwarmAddress,
|
|
3546
3597
|
parseSwarmRequest,
|
|
3547
3598
|
normalizeSwarmSnapshot,
|
|
3599
|
+
unwrapRemoteResult,
|
|
3600
|
+
describeModelsError,
|
|
3548
3601
|
tokenizeSwarmText,
|
|
3549
3602
|
isLongSwarmMessage,
|
|
3550
3603
|
getCollapsedPreviewText,
|
package/docs/changelog.md
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## [0.1.1] - 2026-11-15
|
|
4
|
+
|
|
5
|
+
### 修复 · 设置页「未能读取宿主模型列表」
|
|
6
|
+
|
|
7
|
+
- 真实宿主 `@deepseek-ai/dsh-api-gateway` 的一元 Remote 调用在**成功**时返回 `{ ok: true, value: <payload> }` 信封(失败时 `{ ok: false, error }`)。此前设置页把整封响应当载荷直接交给 `parseSwarmModelsResult`,`Array.isArray(response.models)` 恒为假,于是永远报「未能读取宿主模型列表」并且只留下一句 `models must be an array` 的内部错误。
|
|
8
|
+
- 新增 `unwrapRemoteResult(res)`:`isPlainObject(res) && res.ok === true && 'value' in res && res.value !== undefined` 时取 `res.value`,否则原样透传(插件自带的 `SwarmRemoteServiceFallback` 与离线单测替身返回裸结果,必须同样可用)。`loadSwarmModels` 与 `fetchSnapshotOnce` 统一先解包再交给校验 / 归一化函数。
|
|
9
|
+
- 设置页仍拉取失败时保留简化可读的错误摘要(`modelsError`):状态行文字维持本地化提示,真实原因(截断为单行 240 字内)挂在 `title` 与 `data-models-error` 上,便于区分「宿主流未挂载」与「协议校验失败」;拉取成功或重试成功即清空。
|
|
10
|
+
- 新增回归:注入真实 `{ ok: true, value: { models: [...] } }` 信封给 `api.getSwarmModels`,设置页 `loadModels()` 必须解包返回全量模型;同时覆盖「裸结果透传」「失败信封不被误解包」与错误 tooltip 的出现 / 清空。
|
|
11
|
+
|
|
12
|
+
### 修复 · Host 依赖加载脱节(`MODULE_NOT_FOUND` 退回 Fallback)
|
|
13
|
+
|
|
14
|
+
- 桌面 Profile 里 `require('@deepseek-ai/dsh-typert-protocol')` / `dsh-tools` / `dsh-scope` / `dsh-llm` / `dsh-session` 全部 `MODULE_NOT_FOUND`:这些包只在 `<resourcesPath>/app.asar/dsh/node_modules` 里,插件目录解析不到,于是 `index.js` 静默退回手写 Fallback(工具定义、fork 种子、中继消息、Typert 远程服务全部降级)。
|
|
15
|
+
- `runtime.js` 的宿主锚点升级:除环境变量与 `require.main` / `process.argv[1]` 外,新增 `<resourcesPath>/app.asar/{dsh,lib}` 锚点,并把入口文件所在目录的若干层祖先一并当锚点,覆盖桌面版 / CLI / 便携包布局,仍然零硬编码安装路径。
|
|
16
|
+
- 新增 `runtime.requireHostModule(specifier)`:先普通 `require`(依赖被提升到 profile 的布局),失败则 `resolveHostModule` 定位宿主 `app.asar` 入口再 `require(entry)`;`ERR_REQUIRE_ESM`、asar 读取失败等一律返回 `null`(结果含失败一并缓存),调用方继续走兜底,**绝不因为可选依赖缺失而崩溃**。`index.js` 的 `tryRequire` 改为委托它;`loadSchemastery` 复用同一条链路。
|
|
17
|
+
- `mountSwarmRemoteService` 加固:真实 `typertProto.Remote` 装饰器失败时退回手写 `remote-methods` 描述符;`new SwarmRemoteService(ctx)` 抛错(宿主 Cordis / Typert 契约漂移)时退回 `SwarmRemoteServiceFallback`;描述符定义失败也不再影响服务本身。
|
|
18
|
+
- 纯 Node 离线单测零依赖兜底 100% 保留:无 `resourcesPath`、无 `@deepseek-ai/*` 时所有解析都返回 `null`,全部用例仍跑在兜底实现上。
|
|
19
|
+
|
|
20
|
+
### 修复 · GitHub Actions 在 Node 22 报 `exit code 9`
|
|
21
|
+
|
|
22
|
+
- `npm test` 里的 `--test-isolation=none` 是为 Windows 沙盒(命名管道 `spawn EPERM`)准备的;Linux/macOS 并不需要,而 Node 22.13 之前的版本不认这个参数,会以 `exit code 9`(Invalid Argument)直接失败。
|
|
23
|
+
- `package.json` 新增 `"test:ci"`(不带 `--test-isolation`),保留 `"test"` 兼容 Windows 沙盒;`ci.yml` 与 `publish.yml` 的测试步改调 `npm run test:ci`。
|
|
24
|
+
- Matrix 的 `22` 改为 `22.19`,对齐 `engines.node` 的 `^22.19.0`,避免 setup-node 拉到 22.13 以下的小版本。
|
|
4
25
|
|
|
5
26
|
### 修复 · 严格白名单空名单的权限反转
|
|
6
27
|
|
|
7
28
|
- 新增持久化开关 `strictAllowlist`(默认 `false`):白名单激活条件改为「`strictAllowlist === true` 或 `allowedModels` 非空」。此前「严格模式下取消最后一个勾选」会写入 `allowedModels: []`,被误判成「未启用白名单 → 全部放行」,权限方向反转;现在空名单一律**全禁**,报错明确提示「已启用严格白名单模式但允许列表为空」。
|
|
8
|
-
-
|
|
9
|
-
- 新增
|
|
29
|
+
- 设置页开关与白名单写入路径均改为先持久化 `strictAllowlist` 再更新 `allowedModels`(包括 legacy 仅非空名单激活时清空最后一项,也会先钉住 `strictAllowlist: true`;关闭时先写 `strictAllowlist: false` 再清空),任一步保存失败即回滚到已持久化快照并显示保存失败,绝不停留在未落盘的开关状态。
|
|
30
|
+
- 新增 5 项设置页回归(含取消最后一个勾选、移除最后一个白名单胶囊、空目录开启严格模式、legacy 仅非空名单清空、保存失败不误显示)以及对应 Host / 协议回归。
|
|
10
31
|
|
|
11
32
|
### 新增 · 可视化模型派发权限
|
|
12
33
|
|
package/docs/development.md
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
- Node `^22.19.0 || >=24.0.0`
|
|
6
6
|
- DSH 宿主(`engines.dsh: >=0.2.2-rc.1`)
|
|
7
7
|
- Windows 沙盒下跑 `node --test` **必须**带 `--test-isolation=none`:受限令牌下子进程命名管道会 `spawn EPERM`
|
|
8
|
+
- 反之 Linux / macOS(CI)不需要该参数,且 Node 22.13 之前不认它(`exit code 9`),所以 CI 统一用 `npm run test:ci`
|
|
8
9
|
|
|
9
10
|
## 本地检查
|
|
10
11
|
|
|
11
12
|
```bash
|
|
12
13
|
npm run check # node --check 全量源码
|
|
13
|
-
npm test #
|
|
14
|
+
npm test # 离线主套件(Windows 沙盒:含 --test-isolation=none)
|
|
15
|
+
npm run test:ci # 同上但不带 --test-isolation,供 Linux / macOS CI 使用
|
|
14
16
|
npm pack --dry-run --json
|
|
15
17
|
```
|
|
16
18
|
|
|
@@ -55,8 +57,8 @@ test/ 主套件、开源友好度套件、可选集成套
|
|
|
55
57
|
|
|
56
58
|
`.github/workflows/publish.yml` 是 `workflow_dispatch` 触发,带 `dry_run` 布尔输入:
|
|
57
59
|
|
|
58
|
-
1. `actions/checkout@v4` → `actions/setup-node@v4`(Node 22
|
|
59
|
-
2. `npm run check` + `npm pack --dry-run` 作为门禁;
|
|
60
|
+
1. `actions/checkout@v4` → `actions/setup-node@v4`(Node 22.19,对齐 `engines.node`,registry `https://registry.npmjs.org`);
|
|
61
|
+
2. `npm run check` + `npm run test:ci` + `npm pack --dry-run` 作为门禁;
|
|
60
62
|
3. 正式发布走 `npm publish --provenance --access public`。
|
|
61
63
|
|
|
62
64
|
包内容由 `package.json#files` 白名单控制,`test/` 与 `.github/` 不进入 tarball。
|
package/index.js
CHANGED
|
@@ -860,12 +860,24 @@ function fenceAroundText(text) {
|
|
|
860
860
|
return `${fence}text\n${text}\n${fence}`;
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
+
/**
|
|
864
|
+
* Host dependency loader with a zero-dependency fallback.
|
|
865
|
+
*
|
|
866
|
+
* Desktop DSH bundles every `@deepseek-ai/*` runtime package inside its
|
|
867
|
+
* Electron archive, so a plain `require` from the plugin directory raises
|
|
868
|
+
* MODULE_NOT_FOUND and the plugin silently degraded to its hand-written
|
|
869
|
+
* fallbacks. `runtime.requireHostModule` first tries the plain `require`
|
|
870
|
+
* (profile-hoisted layouts), then resolves the specifier against host entry
|
|
871
|
+
* anchors (the desktop runtime's own node_modules, CLI layouts, ...) and
|
|
872
|
+
* requires that entry by path. The archive layout knowledge lives only in
|
|
873
|
+
* `runtime.js`; nothing here spawns a process or reads the archive directly.
|
|
874
|
+
*
|
|
875
|
+
* Missing / ESM-only / unreadable modules still return `null`, so every
|
|
876
|
+
* caller keeps its fallback and the pure-Node offline suite stays
|
|
877
|
+
* dependency-free — no host package is ever required for the plugin to boot.
|
|
878
|
+
*/
|
|
863
879
|
function tryRequire(specifier) {
|
|
864
|
-
|
|
865
|
-
return require(specifier);
|
|
866
|
-
} catch {
|
|
867
|
-
return null;
|
|
868
|
-
}
|
|
880
|
+
return runtime.requireHostModule(specifier);
|
|
869
881
|
}
|
|
870
882
|
|
|
871
883
|
/**
|
|
@@ -4718,58 +4730,68 @@ function mountSwarmRemoteService(ctx, controller) {
|
|
|
4718
4730
|
|
|
4719
4731
|
const attachRemotePrototypeMarkers = (Proto) => {
|
|
4720
4732
|
if (typertProto && typeof typertProto.Remote === 'function') {
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4733
|
+
// 宿主协议的装饰器会做名称 / 上下文字段校验;版本漂移时退回手写描述符,
|
|
4734
|
+
// 绝不能因为一个可选元数据步骤让整个 Swarm 服务挂不上。
|
|
4735
|
+
try {
|
|
4736
|
+
let initGet;
|
|
4737
|
+
typertProto.Remote(Proto.getSwarmSnapshot, {
|
|
4738
|
+
name: 'getSwarmSnapshot',
|
|
4739
|
+
private: false,
|
|
4740
|
+
static: false,
|
|
4741
|
+
addInitializer(fn) {
|
|
4742
|
+
initGet = fn;
|
|
4743
|
+
},
|
|
4744
|
+
});
|
|
4745
|
+
let initWatch;
|
|
4746
|
+
const streamDec = typertProto.Remote({ mode: 'stream' });
|
|
4747
|
+
streamDec(Proto.watchSwarm, {
|
|
4748
|
+
name: 'watchSwarm',
|
|
4749
|
+
private: false,
|
|
4750
|
+
static: false,
|
|
4751
|
+
addInitializer(fn) {
|
|
4752
|
+
initWatch = fn;
|
|
4753
|
+
},
|
|
4754
|
+
});
|
|
4755
|
+
let initModels;
|
|
4756
|
+
typertProto.Remote(Proto.getSwarmModels, {
|
|
4757
|
+
name: 'getSwarmModels',
|
|
4758
|
+
private: false,
|
|
4759
|
+
static: false,
|
|
4760
|
+
addInitializer(fn) {
|
|
4761
|
+
initModels = fn;
|
|
4762
|
+
},
|
|
4763
|
+
});
|
|
4764
|
+
return { initGet, initWatch, initModels };
|
|
4765
|
+
} catch {
|
|
4766
|
+
// fall through to the hand-written descriptor below
|
|
4767
|
+
}
|
|
4750
4768
|
}
|
|
4751
4769
|
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
Object.freeze(
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4770
|
+
try {
|
|
4771
|
+
Object.defineProperty(Proto, REMOTE_METHOD_DESCRIPTOR_KEY, {
|
|
4772
|
+
configurable: true,
|
|
4773
|
+
value: Object.freeze({
|
|
4774
|
+
version: 1,
|
|
4775
|
+
methods: Object.freeze([
|
|
4776
|
+
Object.freeze({
|
|
4777
|
+
method: 'getSwarmSnapshot',
|
|
4778
|
+
invocation: Object.freeze({ kind: 'direct' }),
|
|
4779
|
+
}),
|
|
4780
|
+
Object.freeze({
|
|
4781
|
+
method: 'watchSwarm',
|
|
4782
|
+
mode: 'stream',
|
|
4783
|
+
invocation: Object.freeze({ kind: 'direct' }),
|
|
4784
|
+
}),
|
|
4785
|
+
Object.freeze({
|
|
4786
|
+
method: 'getSwarmModels',
|
|
4787
|
+
invocation: Object.freeze({ kind: 'direct' }),
|
|
4788
|
+
}),
|
|
4789
|
+
]),
|
|
4790
|
+
}),
|
|
4791
|
+
});
|
|
4792
|
+
} catch {
|
|
4793
|
+
// 原型不可扩展 / 已有不可重定义描述符:服务本身依旧可用,放弃元数据声明。
|
|
4794
|
+
}
|
|
4773
4795
|
return null;
|
|
4774
4796
|
};
|
|
4775
4797
|
|
|
@@ -4805,7 +4827,12 @@ function mountSwarmRemoteService(ctx, controller) {
|
|
|
4805
4827
|
initializers.initWatch?.call(protoProxy);
|
|
4806
4828
|
initializers.initModels?.call(protoProxy);
|
|
4807
4829
|
}
|
|
4808
|
-
|
|
4830
|
+
try {
|
|
4831
|
+
return new SwarmRemoteService(ctx);
|
|
4832
|
+
} catch {
|
|
4833
|
+
// 宿主 Cordis / Typert 契约漂移(例如 TypertRemoteService 构造签名变化)时
|
|
4834
|
+
// 退回插件自带实现:degrade gracefully,绝不让 apply 直接抛错。
|
|
4835
|
+
}
|
|
4809
4836
|
}
|
|
4810
4837
|
|
|
4811
4838
|
class SwarmRemoteServiceFallback {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-swarm",
|
|
3
3
|
"displayName": "Swarm Collaboration Cluster",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"description": "Multi-agent collaboration cluster for DeepSeek Harness: shared #seq message board, doorbell-based wakeups, zero-token sleeping, hot model switching, live feed/topology panel, and an optional Antigravity ACP teammate backend.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "higekibaka",
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"icon": "./icon.svg",
|
|
119
119
|
"scripts": {
|
|
120
120
|
"test": "node --test --test-isolation=none test/swarm.test.mjs test/swarm-agy-acp.test.mjs test/swarm-open-source.test.mjs",
|
|
121
|
+
"test:ci": "node --test test/swarm.test.mjs test/swarm-agy-acp.test.mjs test/swarm-open-source.test.mjs",
|
|
121
122
|
"test:acp-integration": "node --test --test-isolation=none test/agy-acp-integration.test.mjs",
|
|
122
123
|
"check": "node --check index.js && node --check client.js && node --check runtime.js && node --check src/protocol.js && node --check src/board-store.js && node --check src/prompts.js && node --check src/redaction.js && node --check src/mcp-bridge-server.js && node --check src/mcp-stdio-entry.js"
|
|
123
124
|
}
|
package/runtime.js
CHANGED
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* 宿主依赖加载器(零依赖兜底)。
|
|
5
5
|
*
|
|
6
|
-
* DSH 桌面版把 @deepseek-ai
|
|
7
|
-
*
|
|
8
|
-
* `runtime.js`
|
|
6
|
+
* DSH 桌面版把 @deepseek-ai/* 运行时依赖打进 app.asar,插件目录之外 require
|
|
7
|
+
* 解析不到;CLI / 便携包布局又各不相同。这里按可选扩展 dsh-agy-acp 的
|
|
8
|
+
* `runtime.js` 思路,用「宿主入口」当解析锚点,而不是硬编码任何安装路径:
|
|
9
9
|
*
|
|
10
|
-
* 1) DSH_SWARM_HOST_MODULES
|
|
11
|
-
* 2) require.main.
|
|
12
|
-
* 3) process.argv[1]
|
|
13
|
-
* 4)
|
|
10
|
+
* 1) DSH_SWARM_HOST_MODULES 指向的目录(离线单测注入用);
|
|
11
|
+
* 2) require.main / process.mainModule(宿主主进程入口)及其祖先目录;
|
|
12
|
+
* 3) process.argv[1] 及其祖先目录;
|
|
13
|
+
* 4) <resourcesPath>/app.asar/{dsh,lib} —— Electron 桌面版的真实依赖树
|
|
14
|
+
* (@deepseek-ai/* 全部在 app.asar/dsh/node_modules 下);
|
|
15
|
+
* 5) 插件自身目录(宿主把依赖提升到 profile 时的兜底)。
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* 解析失败一律返回 null,由调用方切到手写兜底实现:纯 Node 离线单测因此
|
|
18
|
+
* 零依赖也能完整跑通,且这条路径绝不抛错。
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
const path = require('node:path');
|
|
@@ -21,25 +23,75 @@ const { createRequire } = require('node:module');
|
|
|
21
23
|
|
|
22
24
|
const HOST_MODULES_ENV = 'DSH_SWARM_HOST_MODULES';
|
|
23
25
|
|
|
26
|
+
/** 把某个入口文件所在目录(及其若干层祖先)加成锚点,覆盖各种宿主布局。 */
|
|
27
|
+
function ancestorAnchors(entry, depth = 6) {
|
|
28
|
+
const anchors = [];
|
|
29
|
+
if (typeof entry !== 'string' || !entry) return anchors;
|
|
30
|
+
let dir;
|
|
31
|
+
try {
|
|
32
|
+
dir = path.dirname(path.resolve(entry));
|
|
33
|
+
} catch {
|
|
34
|
+
return anchors;
|
|
35
|
+
}
|
|
36
|
+
for (let i = 0; i < depth && dir; i += 1) {
|
|
37
|
+
anchors.push(path.join(dir, 'package.json'));
|
|
38
|
+
const parent = path.dirname(dir);
|
|
39
|
+
if (parent === dir) break;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
return anchors;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Electron 桌面版:app.asar 内部的依赖树锚点(非 Electron 返回空)。 */
|
|
46
|
+
function electronAnchors() {
|
|
47
|
+
const roots = [];
|
|
48
|
+
const resources = process.resourcesPath;
|
|
49
|
+
if (typeof resources === 'string' && resources) {
|
|
50
|
+
try {
|
|
51
|
+
roots.push(path.resolve(resources));
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const anchors = [];
|
|
57
|
+
for (const root of roots) {
|
|
58
|
+
// dsh/ 是 @deepseek-ai/dsh-desktop-runtime,其 node_modules 才是完整依赖树;
|
|
59
|
+
// 根目录的 node_modules 只有桌面壳自己用到的一小部分包。
|
|
60
|
+
anchors.push(path.join(root, 'app.asar', 'dsh', 'package.json'));
|
|
61
|
+
anchors.push(path.join(root, 'app.asar', 'package.json'));
|
|
62
|
+
anchors.push(path.join(root, 'app.asar', 'lib', 'main.js'));
|
|
63
|
+
anchors.push(path.join(root, 'app', 'package.json'));
|
|
64
|
+
anchors.push(path.join(root, 'package.json'));
|
|
65
|
+
}
|
|
66
|
+
return anchors;
|
|
67
|
+
}
|
|
68
|
+
|
|
24
69
|
function hostAnchors() {
|
|
25
70
|
const anchors = [];
|
|
26
71
|
const root = process.env[HOST_MODULES_ENV];
|
|
27
72
|
if (typeof root === 'string' && root.trim()) {
|
|
28
73
|
anchors.push(path.join(root.trim(), 'anchor.cjs'));
|
|
29
74
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
75
|
+
const mainModule = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return require.main && require.main.filename;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
const main = typeof mainModule === 'string' && mainModule ? mainModule : process.mainModule && process.mainModule.filename;
|
|
83
|
+
if (typeof main === 'string' && main) anchors.push(...ancestorAnchors(main), main);
|
|
84
|
+
if (typeof process.argv[1] === 'string' && process.argv[1]) {
|
|
85
|
+
anchors.push(...ancestorAnchors(process.argv[1]), process.argv[1]);
|
|
35
86
|
}
|
|
36
|
-
|
|
87
|
+
anchors.push(...electronAnchors());
|
|
37
88
|
anchors.push(path.join(__dirname, 'index.js'));
|
|
38
89
|
return [...new Set(anchors)];
|
|
39
90
|
}
|
|
40
91
|
|
|
41
92
|
/** 从宿主锚点解析模块入口,全部失败返回 null(绝不抛错)。 */
|
|
42
93
|
function resolveHostModule(specifier) {
|
|
94
|
+
if (typeof specifier !== 'string' || !specifier) return null;
|
|
43
95
|
for (const anchor of hostAnchors()) {
|
|
44
96
|
try {
|
|
45
97
|
return createRequire(anchor).resolve(specifier);
|
|
@@ -54,6 +106,40 @@ function resolveHostModule(specifier) {
|
|
|
54
106
|
}
|
|
55
107
|
}
|
|
56
108
|
|
|
109
|
+
/** specifier -> module | null 的加载结果缓存(含失败结果,避免重复解析)。 */
|
|
110
|
+
const hostModuleCache = new Map();
|
|
111
|
+
|
|
112
|
+
function loadHostModule(specifier) {
|
|
113
|
+
// 1) 普通 require:宿主把依赖提升到 profile / 与插件同层时的最快路径。
|
|
114
|
+
try {
|
|
115
|
+
return require(specifier);
|
|
116
|
+
} catch {
|
|
117
|
+
// 落到宿主入口锚点
|
|
118
|
+
}
|
|
119
|
+
// 2) 按宿主 app.asar 入口解析,再以入口路径加载。
|
|
120
|
+
const entry = resolveHostModule(specifier);
|
|
121
|
+
if (!entry) return null;
|
|
122
|
+
try {
|
|
123
|
+
return require(entry);
|
|
124
|
+
} catch {
|
|
125
|
+
// ERR_REQUIRE_ESM(旧版 Node 的纯 ESM 宿主模块)、asar 读失败、包自检失败
|
|
126
|
+
// 等一律视为不可用:调用方会自动退回手写兜底实现,绝不因此崩溃。
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 惰性加载宿主模块:先普通 require,失败再走 app.asar 锚点。
|
|
133
|
+
* 解析或加载失败返回 null,调用方据此退回零依赖兜底。
|
|
134
|
+
*/
|
|
135
|
+
function requireHostModule(specifier) {
|
|
136
|
+
if (typeof specifier !== 'string' || !specifier) return null;
|
|
137
|
+
if (hostModuleCache.has(specifier)) return hostModuleCache.get(specifier);
|
|
138
|
+
const loaded = loadHostModule(specifier);
|
|
139
|
+
hostModuleCache.set(specifier, loaded);
|
|
140
|
+
return loaded;
|
|
141
|
+
}
|
|
142
|
+
|
|
57
143
|
let schemasteryModule;
|
|
58
144
|
let schemasteryResolved = false;
|
|
59
145
|
|
|
@@ -63,20 +149,7 @@ let schemasteryResolved = false;
|
|
|
63
149
|
*/
|
|
64
150
|
function loadSchemastery() {
|
|
65
151
|
if (schemasteryResolved) return schemasteryModule;
|
|
66
|
-
const
|
|
67
|
-
if (!entry) {
|
|
68
|
-
schemasteryResolved = true;
|
|
69
|
-
schemasteryModule = null;
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
let loaded;
|
|
73
|
-
try {
|
|
74
|
-
loaded = require(entry);
|
|
75
|
-
} catch {
|
|
76
|
-
schemasteryResolved = true;
|
|
77
|
-
schemasteryModule = null;
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
152
|
+
const loaded = requireHostModule('@deepseek-ai/schemastery');
|
|
80
153
|
const z = loaded && typeof loaded.object === 'function' ? loaded : loaded?.default;
|
|
81
154
|
schemasteryModule = z && typeof z.object === 'function' ? z : null;
|
|
82
155
|
schemasteryResolved = true;
|
|
@@ -87,12 +160,16 @@ function loadSchemastery() {
|
|
|
87
160
|
function resetHostModuleCache() {
|
|
88
161
|
schemasteryModule = undefined;
|
|
89
162
|
schemasteryResolved = false;
|
|
163
|
+
hostModuleCache.clear();
|
|
90
164
|
}
|
|
91
165
|
|
|
92
166
|
module.exports = {
|
|
93
167
|
HOST_MODULES_ENV,
|
|
168
|
+
ancestorAnchors,
|
|
169
|
+
electronAnchors,
|
|
94
170
|
hostAnchors,
|
|
95
171
|
resolveHostModule,
|
|
172
|
+
requireHostModule,
|
|
96
173
|
loadSchemastery,
|
|
97
174
|
resetHostModuleCache,
|
|
98
175
|
};
|