benchgo-plugin 0.0.0-stage → 0.0.2
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/DEEPSEEK-LICENSE +21 -0
- package/LICENSE +21 -0
- package/NOTICE +17 -0
- package/PASEO-LICENSE +211 -0
- package/README.md +256 -2
- package/TOOL-CONFIG.md +33 -0
- package/VERIFICATION.md +297 -0
- package/assets/fonts/README.md +13 -0
- package/assets/fonts/manifest.json +437 -0
- package/assets/fonts/web/fangyuan-latin-238bc49130b1.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u30-0fe8d99641c3.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u32-202a573131a1.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u4e-cc2f8b20be49.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u4f-9189b5f2d321.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u50-e2560e003872.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u51-a9c5e7a8c7c4.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u52-0cfd00b473c8.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u53-18122a07f60a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u54-8ca41aa9cff2.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u55-7bd55e14647e.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u56-26f093e8a810.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u57-36bde83a8378.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u58-d1df14217098.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u59-4e41ab22c51a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5a-746a367174e6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5b-3d1dd404d70c.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5c-fe0a273f69e7.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5d-af1f0a1c8d2d.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5e-598c65420e17.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5f-a92eaaa5265a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u60-48ee3d26462d.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u61-7a57f35bc077.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u62-d2d0b7b36631.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u63-cade7c6635e6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u64-ed34e40d7f65.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u65-e13e83e69c38.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u66-1dbcfa5da147.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u67-6b0a26b82da6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u68-1b8217cfceed.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u69-673e63627790.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6a-3c94008a4640.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6b-44572c410fa0.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6c-1b323b366e88.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6d-cbc376d6fe3f.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6e-7f7e211e2fee.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6f-ff280ce052fa.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u70-cb727eaf73b6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u71-0bb1747439fe.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u72-cccd39590d69.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u73-e67705133584.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u74-03a4e7ac9df5.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u75-a2d30b327e65.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u76-c08fe029c988.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u77-76f03be1f4cb.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u78-5b721deaf582.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u79-db7713c2eda4.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7a-3e40d3726a09.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7b-a8204d1c9dba.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7c-89bcdae046de.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7d-bcc45fa6351a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7e-49fab96d9269.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7f-dc110e1ecbf3.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u80-c5ce9be0335e.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u81-b026091b3779.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u82-a7c6960e875d.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u83-4b7d1c2c50d4.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u84-4119128100c8.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u85-ca43dd4c5411.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u86-54fc0e3158fb.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u87-b64f5eb55720.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u88-00feb1251f08.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u89-db419435b316.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8a-06c11d45fb87.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8b-5c2f6fda436a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8c-2b46d2d1f4cc.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8d-1a67d064000d.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8e-eb87baf0091c.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8f-96c1f0632da6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u90-86752dba8cbe.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u91-0573b8548552.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u92-f85ecf735974.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u93-e7e0451735db.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u94-f3957ff98bcd.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u95-b31129d29013.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u96-ca7f779c2c13.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u97-100a25198b2c.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u98-59e4b784a94f.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u99-d35af6e30868.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9a-1eb6f7cdfcfe.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9b-8f428d81dcbb.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9c-e10f7f8f0274.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9e-2be65003b759.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9f-02c3d5224fdb.woff2 +0 -0
- package/assets/fonts/web/fangyuan-ufb-3eaee54bc476.woff2 +0 -0
- package/assets/fonts/web/fangyuan-ufe-f64da8a3acce.woff2 +0 -0
- package/assets/fonts/web/fangyuan-uff-0a6dcfb689fd.woff2 +0 -0
- package/client/assets/iconsax/LICENSE +16 -0
- package/client/assets/iconsax/SOURCE.md +14 -0
- package/config/routergo-deepseek.example.yml +25 -0
- package/config.example.json +24 -0
- package/cordis.patch.yml +5 -0
- package/lib/LINK-THIRD-PARTY-LICENSES.txt +1197 -0
- package/lib/client.js +3577 -0
- package/lib/link-web/app.css +92 -0
- package/lib/link-web/app.js +599 -0
- package/lib/link-web/index.html +1 -0
- package/lib/link-web/workbench.js +118 -0
- package/lib/link.mjs +244 -0
- package/lib/relay/ip-certificate.mjs +559 -0
- package/lib/relay/relay.example.json +8 -0
- package/lib/relay/run-relay-server.cjs +4100 -0
- package/lib/server/bootstrap.mjs +147 -0
- package/lib/server/bootstrap.sh +39 -0
- package/lib/server/manifest.json +8 -0
- package/lib/server/server-seed.tar.gz +0 -0
- package/locale/en.json +6 -0
- package/locale/zh-CN.json +6 -0
- package/openapi.json +629 -0
- package/package.json +104 -4
- package/shared/plugin-install.mjs +198 -0
- package/shared/tool-quick-settings.mjs +70 -0
- package/shared/update-feed.mjs +191 -0
- package/shared/update-public-key.pem +3 -0
- package/src/cli.mjs +102 -0
- package/src/commands.mjs +17 -0
- package/src/delegation.mjs +47 -0
- package/src/fonts.mjs +17 -0
- package/src/harness-adapter.mjs +228 -0
- package/src/harness-commands.mjs +39 -0
- package/src/harness-selection.mjs +5 -0
- package/src/harness-usage.mjs +31 -0
- package/src/isolated-launch.mjs +22 -0
- package/src/line-process.mjs +114 -0
- package/src/link-plugin.mjs +21 -0
- package/src/mcp.mjs +21 -0
- package/src/model-providers.mjs +125 -0
- package/src/models.mjs +37 -0
- package/src/native-auth.mjs +32 -0
- package/src/picker-catalog.mjs +82 -0
- package/src/plugin.mjs +138 -0
- package/src/projects.mjs +96 -0
- package/src/provider-access.mjs +11 -0
- package/src/providers/acp.mjs +138 -0
- package/src/providers/claude.mjs +115 -0
- package/src/providers/codex.mjs +134 -0
- package/src/providers/opencode.mjs +138 -0
- package/src/providers/pi-permissions.ts +24 -0
- package/src/providers/pi.mjs +97 -0
- package/src/questions.mjs +34 -0
- package/src/recent-workspace.mjs +36 -0
- package/src/runtime.mjs +421 -0
- package/src/session-routes.mjs +89 -0
- package/src/store.mjs +98 -0
- package/src/tool-catalog.mjs +22 -0
- package/src/tool-config.mjs +94 -0
- package/src/tool-install.mjs +31 -0
- package/src/tool-profiles.mjs +114 -0
- package/src/tooling.mjs +90 -0
- package/src/updates.mjs +156 -0
- package/src/usage-import-parsers.mjs +37 -0
- package/src/usage-import.mjs +114 -0
- package/src/usage-timeline.mjs +36 -0
- package/src/usage.mjs +142 -0
- package/src/web.mjs +274 -0
- package/voice/README.md +49 -0
- package/voice/STREAMING.md +141 -0
- package/voice/VERIFICATION.md +36 -0
- package/voice/client/audio-worklet.js +35 -0
- package/voice/locale/en.json +1 -0
- package/voice/locale/zh-CN.json +1 -0
- package/voice/src/asr.mjs +44 -0
- package/voice/src/calls.mjs +37 -0
- package/voice/src/config.mjs +139 -0
- package/voice/src/media.mjs +70 -0
- package/voice/src/plugin.mjs +10 -0
- package/voice/src/realtime-protocols.mjs +30 -0
- package/voice/src/realtime-socket.mjs +86 -0
- package/voice/src/realtime.mjs +130 -0
- package/voice/src/tts.mjs +16 -0
- package/voice/src/web.mjs +171 -0
package/package.json
CHANGED
|
@@ -1,6 +1,106 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "benchgo-plugin",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"displayName": "BenchGo",
|
|
4
|
+
"version": "0.0.2",
|
|
5
|
+
"private": false,
|
|
6
|
+
"repository": { "type": "git", "url": "https://github.com/zhuogo/BenchGo.git" },
|
|
7
|
+
"publishConfig": { "access": "public" },
|
|
8
|
+
"description": "BenchGo native coding agents, shared projects, voice and enhanced Harness UI",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"main": "src/plugin.mjs",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/plugin.mjs",
|
|
13
|
+
"./link-plugin": "./src/link-plugin.mjs",
|
|
14
|
+
"./runtime": "./src/runtime.mjs",
|
|
15
|
+
"./client": "./lib/client.js",
|
|
16
|
+
"./package.json": "./package.json",
|
|
17
|
+
"./icon": "./assets/icon.svg",
|
|
18
|
+
"./locale/*.json": "./locale/*.json"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"commander": "src/cli.mjs",
|
|
22
|
+
"benchgo": "src/cli.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"shared",
|
|
27
|
+
"cordis.patch.yml",
|
|
28
|
+
"config.example.json",
|
|
29
|
+
"config/routergo-deepseek.example.yml",
|
|
30
|
+
"README.md",
|
|
31
|
+
"TOOL-CONFIG.md",
|
|
32
|
+
"openapi.json",
|
|
33
|
+
"VERIFICATION.md",
|
|
34
|
+
"lib",
|
|
35
|
+
"assets/fonts/web",
|
|
36
|
+
"assets/fonts/manifest.json",
|
|
37
|
+
"assets/fonts/README.md",
|
|
38
|
+
"NOTICE",
|
|
39
|
+
"PASEO-LICENSE",
|
|
40
|
+
"DEEPSEEK-LICENSE",
|
|
41
|
+
"client/assets/iconsax/LICENSE",
|
|
42
|
+
"client/assets/iconsax/SOURCE.md",
|
|
43
|
+
"locale",
|
|
44
|
+
"voice/src",
|
|
45
|
+
"voice/client/audio-worklet.js",
|
|
46
|
+
"voice/README.md",
|
|
47
|
+
"voice/VERIFICATION.md",
|
|
48
|
+
"voice/locale",
|
|
49
|
+
"voice/STREAMING.md"
|
|
50
|
+
],
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.19.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "node --test test/*.test.mjs",
|
|
56
|
+
"test:native": "node --test test/native.mjs",
|
|
57
|
+
"start": "node src/cli.mjs",
|
|
58
|
+
"pack:check": "npm pack --dry-run --ignore-scripts",
|
|
59
|
+
"build:client": "node scripts/build-client.mjs"
|
|
60
|
+
},
|
|
61
|
+
"optionalDependencies": {
|
|
62
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.300"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@deepseek-ai/cordis": "4.0.5-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.3",
|
|
67
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.3",
|
|
68
|
+
"@deepseek-ai/dsh-tools": "^0.2.1-alpha.3",
|
|
69
|
+
"esbuild": "^0.27.0"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.2"
|
|
73
|
+
},
|
|
74
|
+
"dsh": {
|
|
75
|
+
"bundle": {
|
|
76
|
+
"patch": "./cordis.patch.yml"
|
|
77
|
+
},
|
|
78
|
+
"client": {
|
|
79
|
+
"inject": [
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-workspace",
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
83
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
84
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
85
|
+
"@deepseek-ai/dsh-client-locale",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-theme",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-settings-models"
|
|
91
|
+
],
|
|
92
|
+
"platform": "web"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"dependencies": {
|
|
96
|
+
"@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.3",
|
|
97
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.3",
|
|
98
|
+
"json5": "2.2.3",
|
|
99
|
+
"jsonc-parser": "3.3.1",
|
|
100
|
+
"smol-toml": "1.9.0",
|
|
101
|
+
"tar": "7.5.22",
|
|
102
|
+
"ws": "8.19.0",
|
|
103
|
+
"yaml": "2.9.0",
|
|
104
|
+
"ssh2": "1.17.0"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/** Immutable profile-local plugin releases. The profile manifest is the only activation pointer. */
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { cp, lstat, mkdir, readFile, rename, rm, symlink, realpath } from 'node:fs/promises';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { compareVersions } from './update-feed.mjs';
|
|
7
|
+
|
|
8
|
+
export const PLUS_PACKAGE = 'benchgo';
|
|
9
|
+
export const LEGACY_BUNDLES = ['@dowork/benchgo-desktop', '@dowork/commander', 'benchgo'];
|
|
10
|
+
export const releaseBundleName = version => `@benchgo/desktop-${createHash('sha256').update(version).digest('hex').slice(0, 16)}`;
|
|
11
|
+
const json = async file => JSON.parse(await readFile(file, 'utf8'));
|
|
12
|
+
const present = async file => { try { await lstat(file); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; } };
|
|
13
|
+
|
|
14
|
+
export function assertReleaseVersion(version) {
|
|
15
|
+
compareVersions(version, version);
|
|
16
|
+
if (version.length > 100 || !/^[a-zA-Z0-9][a-zA-Z0-9.+-]*$/.test(version)) throw new Error('插件版本号无效');
|
|
17
|
+
return version;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertArchivePath(name) {
|
|
21
|
+
if (typeof name !== 'string' || name.length > 2048 || name.includes('\\') || isAbsolute(name)) throw new Error('更新包包含不安全路径');
|
|
22
|
+
const parts = name.replace(/\/$/, '').split('/');
|
|
23
|
+
if (parts.some(part => !part || part === '.' || part === '..' || /[<>:"|?*\x00-\x1f]/.test(part) || /[. ]$/.test(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) throw new Error('更新包包含不安全路径');
|
|
24
|
+
if (parts[0] !== 'node_modules' && name !== 'release.json') throw new Error('更新包包含非插件文件');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function describeRelease(directory, version) {
|
|
28
|
+
const metadata = await json(join(directory, 'release.json'));
|
|
29
|
+
const pluginPath = join(directory, 'node_modules', PLUS_PACKAGE);
|
|
30
|
+
const bundleName = releaseBundleName(version), bundlePath = join(directory, 'node_modules', bundleName);
|
|
31
|
+
const plugin = await json(join(pluginPath, 'package.json'));
|
|
32
|
+
const bundle = await json(join(bundlePath, 'package.json'));
|
|
33
|
+
if (metadata.schemaVersion !== 1 || metadata.version !== version || plugin.name !== PLUS_PACKAGE || plugin.version !== version || bundle.name !== bundleName || bundle.version !== version || bundle.dependencies?.[PLUS_PACKAGE] !== version)
|
|
34
|
+
throw new Error('插件版本目录信息不一致');
|
|
35
|
+
if (metadata.platform && metadata.platform !== process.platform) throw new Error(`此插件包适用于 ${metadata.platform},当前系统为 ${process.platform}`);
|
|
36
|
+
if (metadata.arch && metadata.arch !== process.arch) throw new Error(`此插件包适用于 ${metadata.arch},当前架构为 ${process.arch}`);
|
|
37
|
+
for (const file of ['src/plugin.mjs', 'lib/client.js', 'lib/link.mjs', 'lib/link-web/index.html', 'voice/src/plugin.mjs', 'cordis.patch.yml']) {
|
|
38
|
+
const stat = await lstat(join(pluginPath, file));
|
|
39
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('插件更新包缺少必要文件');
|
|
40
|
+
}
|
|
41
|
+
return { directory, version, pluginPath, bundlePath, bundleName, root: dirname(directory), shared: metadata.shared ?? [] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function attachOfficialPackages(release, runtime) {
|
|
45
|
+
if (!runtime) {
|
|
46
|
+
if (release.shared.length) throw new Error('未找到官方引擎目录,无法校验插件共享服务');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
runtime = await realpath(runtime);
|
|
50
|
+
const findPackage = async name => {
|
|
51
|
+
for (let current = runtime; ; current = dirname(current)) {
|
|
52
|
+
const candidate = join(current, 'node_modules', name);
|
|
53
|
+
if (await present(join(candidate, 'package.json'))) return candidate;
|
|
54
|
+
if (dirname(current) === current) return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
if (await findPackage(PLUS_PACKAGE)) throw new Error('此桌面版本仍内置锁定插件,请先安装支持独立插件更新的新版应用');
|
|
58
|
+
for (const item of release.shared) {
|
|
59
|
+
const name = typeof item === 'string' ? item : item.name;
|
|
60
|
+
if (typeof name !== 'string' || !/^@deepseek-ai\/[a-z0-9][a-z0-9-]*$/.test(name)) throw new Error('插件声明了不受支持的共享服务');
|
|
61
|
+
const source = await findPackage(name), target = join(release.directory, 'node_modules', name);
|
|
62
|
+
if (!source) throw new Error(`官方引擎缺少共享服务 ${name}`);
|
|
63
|
+
const manifest = await json(join(source, 'package.json'));
|
|
64
|
+
if (typeof item === 'object' && item.version !== manifest.version) throw new Error(`共享服务 ${name} 版本不兼容,请更新应用`);
|
|
65
|
+
if (await present(target)) {
|
|
66
|
+
let current;
|
|
67
|
+
try { current = await realpath(target); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
68
|
+
if (current === await realpath(source)) continue;
|
|
69
|
+
if (!(await lstat(target)).isSymbolicLink()) throw new Error('插件包不得覆盖官方共享服务');
|
|
70
|
+
await rm(target, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
await mkdir(dirname(target), { recursive: true });
|
|
73
|
+
await symlink(source, target, process.platform === 'win32' ? 'junction' : 'dir');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Prepare and validate without changing the running profile. No lifecycle scripts are executed. */
|
|
78
|
+
export async function prepareRelease({ archive, directory, root, version, runtime }) {
|
|
79
|
+
assertReleaseVersion(version);
|
|
80
|
+
root = resolve(root);
|
|
81
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
82
|
+
const destination = join(root, version);
|
|
83
|
+
if (await present(destination)) {
|
|
84
|
+
const release = await describeRelease(destination, version);
|
|
85
|
+
await attachOfficialPackages(release, runtime);
|
|
86
|
+
return release;
|
|
87
|
+
}
|
|
88
|
+
const temporary = join(root, `.prepare-${randomUUID()}`);
|
|
89
|
+
await mkdir(temporary, { mode: 0o700 });
|
|
90
|
+
try {
|
|
91
|
+
if (directory && !archive) {
|
|
92
|
+
await cp(resolve(directory), temporary, { recursive: true, dereference: false, filter: async source => {
|
|
93
|
+
if ((await lstat(source)).isSymbolicLink()) throw new Error('离线种子不得包含符号链接');
|
|
94
|
+
return true;
|
|
95
|
+
} });
|
|
96
|
+
} else {
|
|
97
|
+
const tar = await import('tar');
|
|
98
|
+
let total = 0, count = 0;
|
|
99
|
+
let validationError;
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
await tar.t({ file: archive, strict: true, onReadEntry: entry => {
|
|
102
|
+
try {
|
|
103
|
+
assertArchivePath(entry.path);
|
|
104
|
+
if (!['File', 'Directory'].includes(entry.type)) throw new Error('插件更新包不得包含链接或设备文件');
|
|
105
|
+
const normalized = entry.path.replace(/\/$/, '').toLowerCase();
|
|
106
|
+
if (seen.has(normalized)) throw new Error('插件更新包包含重复路径');
|
|
107
|
+
seen.add(normalized);
|
|
108
|
+
total += entry.size;
|
|
109
|
+
if (++count > 60_000 || total > 1024 * 1024 * 1024 || entry.size < 0) throw new Error('插件更新包解压大小超出限制');
|
|
110
|
+
} catch (error) { validationError ??= error; }
|
|
111
|
+
} });
|
|
112
|
+
if (validationError) throw validationError;
|
|
113
|
+
await tar.x({ file: archive, cwd: temporary, strict: true, preservePaths: false, noMtime: true, noChmod: true,
|
|
114
|
+
filter: (name, entry) => { assertArchivePath(name); if (!['File', 'Directory'].includes(entry.type)) throw new Error('插件更新包不得包含链接'); return true; },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const prepared = await describeRelease(temporary, version);
|
|
118
|
+
await attachOfficialPackages(prepared, runtime);
|
|
119
|
+
await rename(temporary, destination);
|
|
120
|
+
return await describeRelease(destination, version);
|
|
121
|
+
} catch (error) { await rm(temporary, { recursive: true, force: true }); throw error; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function getInstallState(profile) {
|
|
125
|
+
let manifest;
|
|
126
|
+
try { manifest = await json(join(profile, 'package.json')); }
|
|
127
|
+
catch (error) { if (error.code === 'ENOENT') return { managed: false, enabled: false }; throw error; }
|
|
128
|
+
const state = manifest.dsh?.plusInstall;
|
|
129
|
+
if (!state || state.schemaVersion !== 1) return { managed: false, enabled: (manifest.dsh?.profile?.bundles ?? []).some(name => LEGACY_BUNDLES.includes(name)) };
|
|
130
|
+
return { ...state, managed: true, enabled: (manifest.dsh?.profile?.bundles ?? []).includes(state.bundleName), previous: state.previous ?? null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Unique immutable wrapper links are created first; one manifest rename activates the whole release. */
|
|
134
|
+
export async function activateRelease(options) {
|
|
135
|
+
let atomic;
|
|
136
|
+
try { atomic = await import('@deepseek-ai/dsh-atomic-write'); }
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.code !== 'ERR_MODULE_NOT_FOUND') throw error;
|
|
139
|
+
atomic = await import(pathToFileURL(join(options.release.directory, 'node_modules/@deepseek-ai/dsh-atomic-write/lib/index.js')).href);
|
|
140
|
+
}
|
|
141
|
+
return atomic.withFileLock(join(resolve(options.profile), 'package.json'), () => activateLocked(options, atomic));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function activateLocked({ profile, release, enabled, appVersion, migrateLegacy = false }, atomic) {
|
|
145
|
+
profile = resolve(profile);
|
|
146
|
+
const checked = await describeRelease(resolve(release.directory), release.version);
|
|
147
|
+
const manifestPath = join(profile, 'package.json'), manifest = await json(manifestPath);
|
|
148
|
+
if (!Array.isArray(manifest.dsh?.profile?.bundles)) throw new Error('工作区插件配置无效,原配置已保留');
|
|
149
|
+
const before = await getInstallState(profile);
|
|
150
|
+
const target = join(profile, 'node_modules', checked.bundleName);
|
|
151
|
+
await mkdir(dirname(target), { recursive: true });
|
|
152
|
+
if (await present(target)) {
|
|
153
|
+
if (await realpath(target) !== await realpath(checked.bundlePath)) throw new Error('此插件版本的安装位置已被占用');
|
|
154
|
+
} else await symlink(checked.bundlePath, target, process.platform === 'win32' ? 'junction' : 'dir');
|
|
155
|
+
const selected = manifest.dsh.profile.bundles;
|
|
156
|
+
const wasEnabled = enabled ?? (before.managed ? before.enabled : selected.some(name => LEGACY_BUNDLES.includes(name)));
|
|
157
|
+
const isManagedName = name => LEGACY_BUNDLES.includes(name) || name.startsWith('@benchgo/desktop-');
|
|
158
|
+
manifest.dependencies ??= {};
|
|
159
|
+
for (const name of Object.keys(manifest.dependencies)) if (isManagedName(name)) delete manifest.dependencies[name];
|
|
160
|
+
manifest.dependencies[checked.bundleName] = `link:${checked.bundlePath.replaceAll('\\', '/')}`;
|
|
161
|
+
manifest.dsh.profile.bundles = selected.filter(name => !isManagedName(name));
|
|
162
|
+
if (wasEnabled) manifest.dsh.profile.bundles.push(checked.bundleName);
|
|
163
|
+
const previous = before.managed && before.version !== checked.version
|
|
164
|
+
? { version: before.version, directory: before.directory, bundleName: before.bundleName }
|
|
165
|
+
: before.previous ?? null;
|
|
166
|
+
manifest.dsh.plusInstall = { schemaVersion: 1, version: checked.version, directory: checked.directory, bundleName: checked.bundleName, root: checked.root, previous, appVersion: appVersion ?? before.appVersion ?? null };
|
|
167
|
+
// Native lookup inside the profile wins over the selected bundle's fallback.
|
|
168
|
+
// Only the offline installer may move an old package a running Host can import.
|
|
169
|
+
const shadow = join(profile, 'node_modules', PLUS_PACKAGE);
|
|
170
|
+
let backup;
|
|
171
|
+
if (await present(shadow)) {
|
|
172
|
+
const legacy = await json(join(shadow, 'package.json'));
|
|
173
|
+
if (legacy.name !== PLUS_PACKAGE) throw new Error('插件安装位置被其他内容占用,原文件已保留');
|
|
174
|
+
assertReleaseVersion(legacy.version);
|
|
175
|
+
if (migrateLegacy || await realpath(shadow) !== await realpath(checked.pluginPath)) {
|
|
176
|
+
if (!migrateLegacy) throw new Error('检测到旧版插件残留,请关闭BenchGo并重新运行新版安装程序');
|
|
177
|
+
const migrationRoot = join(profile, '.benchgo');
|
|
178
|
+
await mkdir(migrationRoot, { recursive: true, mode: 0o700 });
|
|
179
|
+
backup = join(migrationRoot, `.legacy-${randomUUID()}`);
|
|
180
|
+
await rename(shadow, backup);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await atomic.writeFileAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (backup) await rename(backup, shadow);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
if (backup) await rm(backup, { recursive: true, force: true });
|
|
190
|
+
return getInstallState(profile);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function rollbackRelease({ profile, runtime }) {
|
|
194
|
+
const state = await getInstallState(profile);
|
|
195
|
+
if (!state.previous) throw new Error('没有可回退的插件版本');
|
|
196
|
+
const release = await prepareRelease({ root: state.root, version: state.previous.version, runtime });
|
|
197
|
+
return activateRelease({ profile, release });
|
|
198
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Explicit, credential-free settings shared by the editor and launch adapters.
|
|
2
|
+
const choices = values => values.map(value => ({ value, label: value }));
|
|
3
|
+
const field = (key, label, type, path, options = {}) => ({ key, label, type, group: '快捷配置', role: 'config', path, ...options });
|
|
4
|
+
const claude = [
|
|
5
|
+
field('agentTeams', '多 Agent 协作', 'boolean', ['env', 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'], { role: 'settings', group: '通用配置', description: '开启 Claude Code 实验性 Agent Teams;由工具版本决定是否支持。' }),
|
|
6
|
+
field('agentWindow', 'Agent 窗口', 'select', ['teammateMode'], { role: 'settings', group: '通用配置', options: [{ value: 'in-process', label: '当前进程' }, { value: 'auto', label: '自动' }, { value: 'tmux', label: 'tmux 分屏' }, { value: 'iterm2', label: 'iTerm2 分屏' }], description: '分屏模式需要本机相应终端支持。' }),
|
|
7
|
+
field('thinking', '扩展思考', 'boolean', ['alwaysThinkingEnabled'], { role: 'settings' }),
|
|
8
|
+
field('effort', '推理强度', 'select', ['effortLevel'], { role: 'settings', options: choices(['low', 'medium', 'high', 'xhigh']) }),
|
|
9
|
+
field('disableHooks', '禁用 Hooks', 'boolean', ['disableAllHooks'], { role: 'settings' }),
|
|
10
|
+
];
|
|
11
|
+
const schema = {
|
|
12
|
+
claude,
|
|
13
|
+
codex: [
|
|
14
|
+
field('multiAgent', '多 Agent 协作', 'boolean', ['features', 'multi_agent'], { group: '通用配置' }),
|
|
15
|
+
field('agentThreads', '并发 Agent 数', 'number', ['agents', 'max_threads'], { group: '通用配置', min: 1, max: 32 }),
|
|
16
|
+
field('agentDepth', 'Agent 最大深度', 'number', ['agents', 'max_depth'], { group: '通用配置', min: 1, max: 8 }),
|
|
17
|
+
field('effort', '推理强度', 'select', ['model_reasoning_effort'], { options: choices(['minimal', 'low', 'medium', 'high', 'xhigh']) }),
|
|
18
|
+
field('contextWindow', '上下文窗口', 'number', ['model_context_window'], { min: 1024, max: 10000000 }),
|
|
19
|
+
field('compactLimit', '自动压缩阈值', 'number', ['model_auto_compact_token_limit'], { min: 1024, max: 10000000 }),
|
|
20
|
+
],
|
|
21
|
+
gemini: [
|
|
22
|
+
field('previewFeatures', '预览功能', 'boolean', ['general', 'previewFeatures'], { role: 'settings' }),
|
|
23
|
+
field('promptCompletion', '提示词自动补全', 'boolean', ['general', 'enablePromptCompletion'], { role: 'settings' }),
|
|
24
|
+
field('maxTurns', '会话最大轮数', 'number', ['model', 'maxSessionTurns'], { role: 'settings', min: 1, max: 10000 }),
|
|
25
|
+
],
|
|
26
|
+
opencode: [
|
|
27
|
+
field('autoUpdate', '自动更新', 'boolean', ['autoupdate']),
|
|
28
|
+
field('snapshot', '文件快照', 'boolean', ['snapshot']),
|
|
29
|
+
field('share', '会话分享', 'select', ['share'], { options: [{ value: 'disabled', label: '关闭' }, { value: 'manual', label: '手动' }, { value: 'auto', label: '自动' }] }),
|
|
30
|
+
],
|
|
31
|
+
openclaw: [
|
|
32
|
+
field('concurrency', '并发 Agent 数', 'number', ['agents', 'defaults', 'maxConcurrent'], { group: '通用配置', min: 1, max: 32 }),
|
|
33
|
+
field('contextWindow', '上下文窗口', 'number', ['agents', 'defaults', 'contextTokens'], { min: 1024, max: 10000000 }),
|
|
34
|
+
],
|
|
35
|
+
pi: [
|
|
36
|
+
field('thinking', '思考级别', 'select', ['defaultThinkingLevel'], { role: 'settings', options: choices(['off', 'minimal', 'low', 'medium', 'high', 'xhigh']) }),
|
|
37
|
+
field('compaction', '自动压缩', 'boolean', ['compaction', 'enabled'], { role: 'settings' }),
|
|
38
|
+
field('retry', '失败重试', 'boolean', ['retry', 'enabled'], { role: 'settings' }),
|
|
39
|
+
],
|
|
40
|
+
hermes: [
|
|
41
|
+
field('maxTurns', 'Agent 最大轮数', 'number', ['agent', 'max_turns'], { min: 1, max: 10000 }),
|
|
42
|
+
field('effort', '推理强度', 'select', ['agent', 'reasoning_effort'], { options: choices(['low', 'medium', 'high']) }),
|
|
43
|
+
],
|
|
44
|
+
grok: [
|
|
45
|
+
field('contextWindow', '上下文窗口', 'number', ['model', 'benchgo', 'context_window'], { min: 1024, max: 10000000 }),
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
export function quickSettingsFor(tool) { return structuredClone(schema[tool] || []); }
|
|
49
|
+
export function validateQuickSettings(tool, input = {}) {
|
|
50
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('快捷配置必须为对象');
|
|
51
|
+
const fields = schema[tool] || [], result = {};
|
|
52
|
+
for (const [key, value] of Object.entries(input)) {
|
|
53
|
+
const spec = fields.find(item => item.key === key);
|
|
54
|
+
if (!spec) throw new Error('此工具不支持快捷配置:' + key);
|
|
55
|
+
if (spec.type === 'boolean' && typeof value !== 'boolean' || spec.type === 'select' && !spec.options.some(option => option.value === value) || spec.type === 'number' && (!Number.isSafeInteger(value) || value < spec.min || value > spec.max)) throw new Error('无效的快捷配置:' + spec.label);
|
|
56
|
+
result[key] = value;
|
|
57
|
+
}
|
|
58
|
+
if (result.compactLimit && result.contextWindow && result.compactLimit > result.contextWindow) throw new Error('自动压缩阈值不能超过上下文窗口');
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
export function quickSettingsPatches(tool, input = {}) {
|
|
62
|
+
const values = validateQuickSettings(tool, input), result = {};
|
|
63
|
+
for (const spec of schema[tool] || []) {
|
|
64
|
+
if (!Object.hasOwn(values, spec.key)) continue;
|
|
65
|
+
let target = result[spec.role] ??= {};
|
|
66
|
+
for (const key of spec.path.slice(0, -1)) target = target[key] ??= {};
|
|
67
|
+
target[spec.path.at(-1)] = spec.key === 'agentTeams' ? (values[spec.key] ? '1' : '0') : values[spec.key];
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
export const UPDATE_REPOSITORY = 'zhuogo/BenchGo';
|
|
5
|
+
export const DEFAULT_UPDATE_CONFIG = Object.freeze({ source: 'mirror', mirrorUrl: 'https://ghfast.top/' });
|
|
6
|
+
export const MAX_PLUGIN_BYTES = 256 * 1024 * 1024;
|
|
7
|
+
const releaseBase = `https://github.com/${UPDATE_REPOSITORY}/releases/`;
|
|
8
|
+
const metadataUrl = `${releaseBase}download/plugin-updates/benchgo-update.json`;
|
|
9
|
+
|
|
10
|
+
export class UpdateFeedError extends Error {
|
|
11
|
+
constructor(code, message, options) { super(message, options); this.name = 'UpdateFeedError'; this.code = code; }
|
|
12
|
+
}
|
|
13
|
+
const fail = (code, message) => { throw new UpdateFeedError(code, message); };
|
|
14
|
+
|
|
15
|
+
function mirrorPrefix(value) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try { parsed = new URL(value); } catch { fail('UPDATE_SOURCE', '更新镜像地址无效'); }
|
|
18
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
19
|
+
fail('UPDATE_SOURCE', '更新镜像须使用不含凭据、查询参数和片段的 HTTPS 地址');
|
|
20
|
+
return `${parsed.href.replace(/\/+$/, '')}/`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeUpdateConfig(config = {}) {
|
|
24
|
+
const source = config.source ?? DEFAULT_UPDATE_CONFIG.source;
|
|
25
|
+
if (!['mirror', 'github', 'custom'].includes(source)) fail('UPDATE_SOURCE', '未知更新源');
|
|
26
|
+
if (source === 'custom' && !config.mirrorUrl) fail('UPDATE_SOURCE', '请填写自定义 HTTPS 镜像地址');
|
|
27
|
+
const mirrorUrl = source === 'custom' ? mirrorPrefix(config.mirrorUrl) : DEFAULT_UPDATE_CONFIG.mirrorUrl;
|
|
28
|
+
return { source, mirrorUrl };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function releaseDownloadUrl(filename, tag = 'latest') {
|
|
32
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(filename) || !/^[a-zA-Z0-9][a-zA-Z0-9._+-]*$/.test(tag))
|
|
33
|
+
fail('UPDATE_URL', '更新文件名或版本标签无效');
|
|
34
|
+
return tag === 'latest' ? `${releaseBase}latest/download/${filename}` : `${releaseBase}download/${tag}/${filename}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function filenameFromReleaseUrl(value, { metadata = false } = {}) {
|
|
38
|
+
if (typeof value !== 'string' || !value.startsWith(releaseBase)) fail('UPDATE_URL', '仅支持本仓库的 GitHub Release 下载地址');
|
|
39
|
+
const suffix = value.slice(releaseBase.length);
|
|
40
|
+
const match = suffix.match(/^download\/([a-zA-Z0-9][a-zA-Z0-9._+-]*)\/([a-zA-Z0-9][a-zA-Z0-9._-]*)$/)
|
|
41
|
+
|| (metadata && suffix.match(/^(latest)\/download\/(benchgo-update\.json(?:\.sig)?)$/));
|
|
42
|
+
if (!match) fail('UPDATE_URL', '更新下载地址无效');
|
|
43
|
+
return match[2];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveUpdateUrl(url, config = DEFAULT_UPDATE_CONFIG) {
|
|
47
|
+
filenameFromReleaseUrl(url, { metadata: true });
|
|
48
|
+
const normalized = normalizeUpdateConfig(config);
|
|
49
|
+
return normalized.source === 'github' ? url : `${normalized.mirrorUrl}${url}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseVersion(value) {
|
|
53
|
+
if (typeof value !== 'string' || value.length > 128) fail('UPDATE_VERSION', '版本号无效');
|
|
54
|
+
const match = value.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
|
55
|
+
if (!match) fail('UPDATE_VERSION', `版本号无效:${value}`);
|
|
56
|
+
const prerelease = match[4]?.split('.') ?? [];
|
|
57
|
+
if (prerelease.some(part => /^\d+$/.test(part) && part.length > 1 && part.startsWith('0'))) fail('UPDATE_VERSION', '预发布版本号无效');
|
|
58
|
+
return { parts: match.slice(1, 4).map(BigInt), prerelease };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function compareVersions(left, right) {
|
|
62
|
+
const a = parseVersion(left), b = parseVersion(right);
|
|
63
|
+
for (let index = 0; index < 3; index++) if (a.parts[index] !== b.parts[index]) return a.parts[index] > b.parts[index] ? 1 : -1;
|
|
64
|
+
if (!a.prerelease.length || !b.prerelease.length) return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length ? -1 : 1;
|
|
65
|
+
for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index++) {
|
|
66
|
+
const x = a.prerelease[index], y = b.prerelease[index];
|
|
67
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
|
|
68
|
+
if (x === y) continue;
|
|
69
|
+
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
|
|
70
|
+
if (nx && ny) return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
71
|
+
if (nx !== ny) return nx ? -1 : 1;
|
|
72
|
+
return x > y ? 1 : -1;
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
export const isNewerVersion = (candidate, current) => compareVersions(candidate, current) > 0;
|
|
77
|
+
export function isEngineCompatible(version, range) {
|
|
78
|
+
if (!range || typeof range !== 'object' || !range.min) return false;
|
|
79
|
+
return compareVersions(version, range.min) >= 0 && (!range.max || compareVersions(version, range.max) <= 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateArtifact(artifact, { plugin = false } = {}) {
|
|
83
|
+
if (!artifact || typeof artifact !== 'object') fail('UPDATE_MANIFEST', '更新包信息缺失');
|
|
84
|
+
parseVersion(artifact.version);
|
|
85
|
+
const filename = filenameFromReleaseUrl(artifact.url);
|
|
86
|
+
if (plugin && !filename.endsWith('.tar.gz')) fail('UPDATE_MANIFEST', '插件更新包格式须为 tar.gz');
|
|
87
|
+
if (plugin || artifact.sha256 !== undefined) {
|
|
88
|
+
if (!/^[a-f0-9]{64}$/.test(artifact.sha256)) fail('UPDATE_MANIFEST', '更新包校验值无效');
|
|
89
|
+
}
|
|
90
|
+
if (plugin || artifact.size !== undefined) {
|
|
91
|
+
if (!Number.isSafeInteger(artifact.size) || artifact.size <= 0 || (plugin && artifact.size > MAX_PLUGIN_BYTES)) fail('UPDATE_MANIFEST', '更新包大小无效');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateReleaseManifest(manifest) {
|
|
96
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest) || manifest.schemaVersion !== 1)
|
|
97
|
+
fail('UPDATE_MANIFEST', '不支持的更新清单格式');
|
|
98
|
+
parseVersion(manifest.version);
|
|
99
|
+
if (!manifest.engine || typeof manifest.engine !== 'object') fail('UPDATE_MANIFEST', '缺少引擎兼容信息');
|
|
100
|
+
parseVersion(manifest.engine.min);
|
|
101
|
+
if (manifest.engine.max !== undefined && compareVersions(manifest.engine.max, manifest.engine.min) < 0) fail('UPDATE_MANIFEST', '引擎兼容范围无效');
|
|
102
|
+
validateArtifact(manifest.plugin, { plugin: true });
|
|
103
|
+
if (manifest.version !== manifest.plugin.version) fail('UPDATE_MANIFEST', '插件版本与清单版本不一致');
|
|
104
|
+
if (manifest.app !== undefined) validateArtifact(manifest.app);
|
|
105
|
+
if (manifest.notes !== undefined && (typeof manifest.notes !== 'string' || manifest.notes.length > 16_384)) fail('UPDATE_MANIFEST', '更新说明过长或格式无效');
|
|
106
|
+
return manifest;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function allowedRedirect(url, initial) {
|
|
110
|
+
const parsed = new URL(url);
|
|
111
|
+
const allowedHosts = new Set([new URL(initial).hostname, 'github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com']);
|
|
112
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.hash || !allowedHosts.has(parsed.hostname) || (parsed.port && parsed.port !== '443'))
|
|
113
|
+
fail('UPDATE_REDIRECT', '更新源返回了不受信任的跳转地址');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function fetchUpdateBytes(url, { config = DEFAULT_UPDATE_CONFIG, fetcher = globalThis.fetch, maxBytes = 256 * 1024, timeoutMs = 20_000 } = {}) {
|
|
117
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || !Number.isFinite(timeoutMs) || timeoutMs < 1) fail('UPDATE_REQUEST', '更新请求限制无效');
|
|
118
|
+
const initial = resolveUpdateUrl(url, config), controller = new AbortController();
|
|
119
|
+
let timer;
|
|
120
|
+
try {
|
|
121
|
+
return await Promise.race([
|
|
122
|
+
(async () => {
|
|
123
|
+
let target = initial;
|
|
124
|
+
for (let redirects = 0; redirects <= 4; redirects++) {
|
|
125
|
+
const response = await fetcher(target, { signal: controller.signal, redirect: 'manual', credentials: 'omit', headers: { accept: 'application/octet-stream' } });
|
|
126
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
127
|
+
const location = response.headers.get('location');
|
|
128
|
+
await response.body?.cancel();
|
|
129
|
+
if (!location || redirects === 4) fail('UPDATE_REDIRECT', '更新源跳转次数过多或地址缺失');
|
|
130
|
+
target = new URL(location, target).href;
|
|
131
|
+
allowedRedirect(target, initial);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
await response.body?.cancel();
|
|
136
|
+
throw Object.assign(new UpdateFeedError('UPDATE_HTTP', `更新源请求失败(HTTP ${response.status})`), { httpStatus: response.status });
|
|
137
|
+
}
|
|
138
|
+
const length = response.headers.get('content-length');
|
|
139
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > maxBytes)) { await response.body?.cancel(); fail('UPDATE_SIZE', '更新文件超过允许大小'); }
|
|
140
|
+
if (!response.body) fail('UPDATE_BODY', '更新源返回空响应');
|
|
141
|
+
const reader = response.body.getReader(), chunks = [];
|
|
142
|
+
let size = 0;
|
|
143
|
+
try {
|
|
144
|
+
while (true) {
|
|
145
|
+
const { done, value } = await reader.read();
|
|
146
|
+
if (done) break;
|
|
147
|
+
size += value.byteLength;
|
|
148
|
+
if (size > maxBytes) { await reader.cancel(); fail('UPDATE_SIZE', '更新文件超过允许大小'); }
|
|
149
|
+
chunks.push(Buffer.from(value));
|
|
150
|
+
}
|
|
151
|
+
} finally { reader.releaseLock(); }
|
|
152
|
+
return Buffer.concat(chunks, size);
|
|
153
|
+
}
|
|
154
|
+
})(),
|
|
155
|
+
new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new UpdateFeedError('UPDATE_TIMEOUT', '更新源响应超时,请重试或切换更新源')); }, timeoutMs); }),
|
|
156
|
+
]);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof UpdateFeedError) throw error;
|
|
159
|
+
throw new UpdateFeedError('UPDATE_NETWORK', '无法连接更新源,请检查网络或切换更新源', { cause: error });
|
|
160
|
+
} finally { clearTimeout(timer); }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function fetchVerifiedRelease({ config = DEFAULT_UPDATE_CONFIG, publicKey, fetcher = globalThis.fetch, timeoutMs = 20_000 } = {}) {
|
|
164
|
+
const downloaded = await Promise.allSettled([
|
|
165
|
+
fetchUpdateBytes(metadataUrl, { config, fetcher, timeoutMs }).catch(error => {
|
|
166
|
+
if (error.httpStatus === 404) throw new UpdateFeedError('UPDATE_UNPUBLISHED', '更新通道尚未就绪');
|
|
167
|
+
throw error;
|
|
168
|
+
}),
|
|
169
|
+
fetchUpdateBytes(`${metadataUrl}.sig`, { config, fetcher, timeoutMs, maxBytes: 4096 }),
|
|
170
|
+
publicKey || readFile(new URL('./update-public-key.pem', import.meta.url)),
|
|
171
|
+
]);
|
|
172
|
+
for (const result of downloaded) if (result.status === 'rejected') throw result.reason;
|
|
173
|
+
const [bytes, signatureBytes, trustedKey] = downloaded.map(result => result.value);
|
|
174
|
+
const encoded = signatureBytes.toString('utf8').trim();
|
|
175
|
+
if (!/^[A-Za-z0-9+/]{86}==$/.test(encoded)) fail('UPDATE_SIGNATURE', '更新清单签名格式无效');
|
|
176
|
+
let valid = false;
|
|
177
|
+
try {
|
|
178
|
+
const key = createPublicKey(trustedKey);
|
|
179
|
+
valid = key.asymmetricKeyType === 'ed25519' && verify(null, bytes, key, Buffer.from(encoded, 'base64'));
|
|
180
|
+
} catch { fail('UPDATE_SIGNATURE', '更新公钥或签名无效'); }
|
|
181
|
+
if (!valid) fail('UPDATE_SIGNATURE', '更新清单签名校验失败');
|
|
182
|
+
let manifest;
|
|
183
|
+
try { manifest = JSON.parse(bytes.toString('utf8')); } catch { fail('UPDATE_MANIFEST', '更新清单不是有效 JSON'); }
|
|
184
|
+
return validateReleaseManifest(manifest);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function verifyArtifact(bytes, artifact) {
|
|
188
|
+
if (bytes.byteLength !== artifact.size) fail('UPDATE_SIZE', '更新包大小与清单不一致');
|
|
189
|
+
if (createHash('sha256').update(bytes).digest('hex') !== artifact.sha256) fail('UPDATE_HASH', '更新包完整性校验失败');
|
|
190
|
+
return bytes;
|
|
191
|
+
}
|