@steerable/agent-shell 0.6.52 → 0.6.55
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/dist/brand.js +1 -1
- package/dist/host/pack-assembly.d.ts +6 -16
- package/dist/host/pack-assembly.js +3 -4
- package/dist/host/runtime.js +2 -0
- package/dist/i18n.d.ts +23 -0
- package/dist/i18n.js +87 -0
- package/dist/image-attachment.d.ts +31 -4
- package/dist/image-attachment.js +13 -18
- package/dist/json-store.js +3 -6
- package/dist/local-backend/portable-bundle.d.ts +16 -2
- package/dist/local-backend/portable-bundle.js +29 -16
- package/dist/local-backend/portable-service.js +25 -11
- package/dist/local-backend/router.js +56 -17
- package/dist/local-backend/skills/10-goal/SKILL.md +1 -1
- package/dist/local-backend/skills/11-loop/SKILL.md +1 -1
- package/dist/local-backend/skills/12-create-skill/SKILL.md +2 -2
- package/dist/local-executor.d.ts +22 -0
- package/dist/local-executor.js +118 -1
- package/dist/local-script-registry.js +2 -2
- package/dist/mcp-executor.d.ts +42 -10
- package/dist/mcp-executor.js +248 -41
- package/dist/mcp-server-registry.d.ts +59 -11
- package/dist/mcp-server-registry.js +193 -28
- package/dist/product-config.d.ts +24 -14
- package/dist/project-registry.d.ts +3 -3
- package/dist/project-registry.js +2 -2
- package/dist/runtime.d.ts +8 -54
- package/dist/runtime.js +15 -129
- package/dist/scenario/pack.d.ts +1 -1
- package/dist/scenario/registry.d.ts +4 -5
- package/dist/scenario/registry.js +2 -10
- package/dist/server/start.js +1 -2
- package/dist/shell-exec.d.ts +62 -0
- package/dist/shell-exec.js +147 -0
- package/dist/shell-process.d.ts +3 -0
- package/dist/shell-process.js +137 -0
- package/dist/sidecar/boot.js +4 -0
- package/dist/sidecar/reverse-spawn.js +6 -15
- package/dist/sidecar/supervisor.d.ts +0 -2
- package/dist/sidecar/supervisor.js +7 -30
- package/dist/storage/index.js +1 -1
- package/dist/storage/write-lease-error.d.ts +2 -11
- package/dist/storage/write-lease-error.js +3 -8
- package/dist/tool-router.js +21 -6
- package/package.json +7 -14
- package/dist/app-update-electron.d.ts +0 -5
- package/dist/app-update-electron.js +0 -66
- package/dist/app-update.d.ts +0 -92
- package/dist/app-update.js +0 -213
- package/dist/host/ipc.d.ts +0 -11
- package/dist/host/ipc.js +0 -20
- package/dist/main.d.ts +0 -1
- package/dist/main.js +0 -967
- package/dist/preload-default.d.ts +0 -1
- package/dist/preload-default.js +0 -9
- package/dist/preload.cjs +0 -425
- package/dist/preload.d.ts +0 -20
- package/dist/preload.js +0 -442
- package/dist/single-instance.d.ts +0 -11
- package/dist/single-instance.js +0 -21
|
@@ -42,7 +42,6 @@ export declare class SidecarSupervisor extends EventEmitter {
|
|
|
42
42
|
private healthTimer;
|
|
43
43
|
private failedPings;
|
|
44
44
|
private shuttingDown;
|
|
45
|
-
private quitListener;
|
|
46
45
|
private reverseHandlers;
|
|
47
46
|
private constructor();
|
|
48
47
|
/** Spawn the sidecar and wait for the ready marker. */
|
|
@@ -190,7 +189,6 @@ export declare class SidecarSupervisor extends EventEmitter {
|
|
|
190
189
|
private wrapLinux;
|
|
191
190
|
private wrapWindows;
|
|
192
191
|
private attachListeners;
|
|
193
|
-
private installAppQuitHook;
|
|
194
192
|
private waitForReady;
|
|
195
193
|
private handleStdoutChunk;
|
|
196
194
|
private handleStderrChunk;
|
|
@@ -16,7 +16,6 @@ import { homedir } from 'node:os';
|
|
|
16
16
|
import { dirname, join } from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { promisify } from 'node:util';
|
|
19
|
-
import { onAppWillQuit, offAppWillQuit } from '../runtime.js';
|
|
20
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
20
|
const __dirname = dirname(__filename);
|
|
22
21
|
import { resolveWinSpawnHelperPath } from './reverse-spawn.js';
|
|
@@ -68,7 +67,6 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
68
67
|
healthTimer = null;
|
|
69
68
|
failedPings = 0;
|
|
70
69
|
shuttingDown = false;
|
|
71
|
-
quitListener = null;
|
|
72
70
|
reverseHandlers = new Map();
|
|
73
71
|
constructor(options) {
|
|
74
72
|
super();
|
|
@@ -351,10 +349,6 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
351
349
|
return;
|
|
352
350
|
this.shuttingDown = true;
|
|
353
351
|
this.stopHealthTimer();
|
|
354
|
-
if (this.quitListener) {
|
|
355
|
-
offAppWillQuit(this.quitListener);
|
|
356
|
-
this.quitListener = null;
|
|
357
|
-
}
|
|
358
352
|
const child = this.child;
|
|
359
353
|
if (!child)
|
|
360
354
|
return;
|
|
@@ -409,7 +403,6 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
409
403
|
this.child = null;
|
|
410
404
|
throw err;
|
|
411
405
|
}
|
|
412
|
-
this.installAppQuitHook();
|
|
413
406
|
this.startHealthTimer();
|
|
414
407
|
this.emit('ready', this.readyHealth);
|
|
415
408
|
}
|
|
@@ -641,13 +634,6 @@ export class SidecarSupervisor extends EventEmitter {
|
|
|
641
634
|
this.emit('error', err);
|
|
642
635
|
});
|
|
643
636
|
}
|
|
644
|
-
installAppQuitHook() {
|
|
645
|
-
const hook = () => {
|
|
646
|
-
void this.shutdown();
|
|
647
|
-
};
|
|
648
|
-
this.quitListener = hook;
|
|
649
|
-
onAppWillQuit(hook);
|
|
650
|
-
}
|
|
651
637
|
async waitForReady(timeoutMs) {
|
|
652
638
|
return new Promise((resolve, reject) => {
|
|
653
639
|
const timer = setTimeout(() => {
|
|
@@ -903,22 +889,13 @@ export function resolveSidecarPython(pythonExecutable) {
|
|
|
903
889
|
// build_sidecar.py 的产物布局是 <platform>/python/<exe>(Windows:
|
|
904
890
|
// python/python.exe;POSIX:python/bin/python3,见 build_sidecar.py 的
|
|
905
891
|
// python_binary())。保留无 python/ 层的旧布局候选做向后兼容。
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
catch { /* not in Electron */ }
|
|
914
|
-
runtimeBases.push(join(__dirname, '..', '..', 'python-runtime'));
|
|
915
|
-
const candidates = [];
|
|
916
|
-
for (const base of runtimeBases) {
|
|
917
|
-
candidates.push(join(base, platformTag, 'python', binaryName));
|
|
918
|
-
candidates.push(join(base, platformTag, 'python', 'bin', binaryName));
|
|
919
|
-
candidates.push(join(base, platformTag, binaryName));
|
|
920
|
-
candidates.push(join(base, platformTag, 'bin', binaryName));
|
|
921
|
-
}
|
|
892
|
+
const runtimeBase = join(__dirname, '..', '..', 'python-runtime');
|
|
893
|
+
const candidates = [
|
|
894
|
+
join(runtimeBase, platformTag, 'python', binaryName),
|
|
895
|
+
join(runtimeBase, platformTag, 'python', 'bin', binaryName),
|
|
896
|
+
join(runtimeBase, platformTag, binaryName),
|
|
897
|
+
join(runtimeBase, platformTag, 'bin', binaryName),
|
|
898
|
+
];
|
|
922
899
|
// Dev-layout convenience: the framework repo root .venv (uv sync). The
|
|
923
900
|
// shell lives inside the framework repo (packages/agent-shell/ts), so the
|
|
924
901
|
// venv is an in-repo ancestor — no sibling-checkout probing. Without this,
|
package/dist/storage/index.js
CHANGED
|
@@ -316,7 +316,7 @@ export class SqliteScopedStore {
|
|
|
316
316
|
// context_summary / context_summary_upto / context_compaction_delegated
|
|
317
317
|
// 列成为无人读写的孤儿列(SQLite 不易 drop,保留无害)。
|
|
318
318
|
// 项目模式(增量迁移):chat 可绑定 ProjectRegistry 里的项目 id。
|
|
319
|
-
// 项目记录本身存在
|
|
319
|
+
// 项目记录本身存在 json-store(agent-projects.json),这里只存外键;
|
|
320
320
|
// 删项目时由路由层把本列置 NULL(会话降级为无项目对话)。
|
|
321
321
|
this.ensureColumn('chat_sessions', 'project_id', 'TEXT');
|
|
322
322
|
// 任务编排(增量迁移):depends_on 存 JSON string[]——本任务等待的
|
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exit(code: number): void;
|
|
4
|
-
};
|
|
5
|
-
dialog: {
|
|
6
|
-
showErrorBox(title: string, message: string): void;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
/** Maps write-lease contention to the desktop's user-facing startup failure. */
|
|
10
|
-
export declare function acquireWriteLeaseOrExit<T>(acquire: () => T, loadElectron?: () => Promise<ElectronExit>, exitProcess?: (code: number) => void): T;
|
|
11
|
-
export {};
|
|
1
|
+
/** Maps write-lease contention to a readable startup failure and process exit. */
|
|
2
|
+
export declare function acquireWriteLeaseOrExit<T>(acquire: () => T, exitProcess?: (code: number) => void): T;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StoreAlreadyOwnedError } from './write-lease.js';
|
|
2
|
-
/** Maps write-lease contention to
|
|
3
|
-
export function acquireWriteLeaseOrExit(acquire,
|
|
2
|
+
/** Maps write-lease contention to a readable startup failure and process exit. */
|
|
3
|
+
export function acquireWriteLeaseOrExit(acquire, exitProcess = (code) => process.exit(code)) {
|
|
4
4
|
try {
|
|
5
5
|
return acquire();
|
|
6
6
|
}
|
|
@@ -9,12 +9,7 @@ export function acquireWriteLeaseOrExit(acquire, loadElectron = () => import('el
|
|
|
9
9
|
console.error(`[bs] 本地数据库正在被另一个进程使用(${error.lockPath})。\n` +
|
|
10
10
|
'[bs] 再打开一次桌面应用会回到已经打开的窗口。\n' +
|
|
11
11
|
'[bs] 如果终端里的 pnpm dev:bs 占着数据库,请先在那个终端按 Ctrl+C 停掉,再重新打开。');
|
|
12
|
-
|
|
13
|
-
.then(({ app, dialog }) => {
|
|
14
|
-
dialog.showErrorBox('无法再开一个', '再打开一次应用时,会回到已经打开的窗口。\n\n这次没有回到那个窗口,是因为本地数据库正被另一个进程占用。如果终端里还在跑 pnpm dev:bs,先停掉它,再重新打开。');
|
|
15
|
-
app.exit(1);
|
|
16
|
-
})
|
|
17
|
-
.catch(() => exitProcess(1));
|
|
12
|
+
queueMicrotask(() => exitProcess(1));
|
|
18
13
|
}
|
|
19
14
|
throw error;
|
|
20
15
|
}
|
package/dist/tool-router.js
CHANGED
|
@@ -172,7 +172,8 @@ export class ToolRouter {
|
|
|
172
172
|
return [
|
|
173
173
|
{
|
|
174
174
|
name: 'local_exec_shell',
|
|
175
|
-
description: 'Execute a
|
|
175
|
+
description: 'Execute a shell command in a pseudo-terminal. Set pty to false for a plain pipe. ' +
|
|
176
|
+
'When this host is configured with a remote endpoint, the command runs on that host instead of this machine.',
|
|
176
177
|
mode: 'destructive',
|
|
177
178
|
inputSchema: {
|
|
178
179
|
type: 'object',
|
|
@@ -180,6 +181,7 @@ export class ToolRouter {
|
|
|
180
181
|
command: { type: 'string' },
|
|
181
182
|
cwd: { type: 'string' },
|
|
182
183
|
timeout: { type: 'number', description: 'Timeout in milliseconds (e.g. 30000). If a small number like 15 or 30 is provided, it is assumed to be in seconds and automatically multiplied by 1000.' },
|
|
184
|
+
pty: { type: 'boolean', description: 'Run in a pseudo-terminal. Defaults to true.' },
|
|
183
185
|
},
|
|
184
186
|
required: ['command'],
|
|
185
187
|
},
|
|
@@ -377,32 +379,35 @@ export class ToolRouter {
|
|
|
377
379
|
},
|
|
378
380
|
{
|
|
379
381
|
name: 'mcp_list_tools',
|
|
380
|
-
description: 'List tools from one MCP server',
|
|
382
|
+
description: 'List tools from one MCP server. Pass command for stdio or url for unauthenticated Streamable HTTP.',
|
|
381
383
|
mode: 'external',
|
|
382
384
|
inputSchema: {
|
|
383
385
|
type: 'object',
|
|
384
386
|
properties: {
|
|
385
387
|
command: { type: 'string' },
|
|
388
|
+
url: { type: 'string' },
|
|
386
389
|
args: { type: 'array', items: { type: 'string' } },
|
|
387
390
|
cwd: { type: 'string' },
|
|
388
391
|
},
|
|
389
|
-
|
|
392
|
+
additionalProperties: false,
|
|
390
393
|
},
|
|
391
394
|
},
|
|
392
395
|
{
|
|
393
396
|
name: 'mcp_tool_exec',
|
|
394
|
-
description: 'Execute one MCP tool',
|
|
397
|
+
description: 'Execute one MCP tool. Pass command for stdio or url for unauthenticated Streamable HTTP.',
|
|
395
398
|
mode: 'external',
|
|
396
399
|
inputSchema: {
|
|
397
400
|
type: 'object',
|
|
398
401
|
properties: {
|
|
399
402
|
command: { type: 'string' },
|
|
403
|
+
url: { type: 'string' },
|
|
400
404
|
args: { type: 'array', items: { type: 'string' } },
|
|
401
405
|
cwd: { type: 'string' },
|
|
402
406
|
toolName: { type: 'string' },
|
|
403
407
|
toolArgs: { type: 'object' },
|
|
404
408
|
},
|
|
405
|
-
required: ['
|
|
409
|
+
required: ['toolName'],
|
|
410
|
+
additionalProperties: false,
|
|
406
411
|
},
|
|
407
412
|
},
|
|
408
413
|
// deferred 层的发现缝(Wave 2 工具分层):MCP 动态工具不占每轮
|
|
@@ -890,6 +895,7 @@ export class ToolRouter {
|
|
|
890
895
|
command: String(args.command || ''),
|
|
891
896
|
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
892
897
|
timeout: typeof args.timeout === 'number' ? args.timeout : undefined,
|
|
898
|
+
pty: args.pty !== false,
|
|
893
899
|
}, projectRoot, context?.additionalWriteRoots ?? null);
|
|
894
900
|
case 'local_read_file':
|
|
895
901
|
return await this.localExecutor.readLocalFile({
|
|
@@ -1257,8 +1263,17 @@ export class ToolRouter {
|
|
|
1257
1263
|
return { path: resolved };
|
|
1258
1264
|
}
|
|
1259
1265
|
resolveMcpConfig(args) {
|
|
1266
|
+
const command = typeof args.command === 'string' ? args.command.trim() : '';
|
|
1267
|
+
const url = typeof args.url === 'string' ? args.url.trim() : '';
|
|
1268
|
+
if (command && url)
|
|
1269
|
+
throw new Error('MCP 调用只能提供 command 或 url 之一');
|
|
1270
|
+
if (url)
|
|
1271
|
+
return { transport: 'streamable-http', url };
|
|
1272
|
+
if (!command)
|
|
1273
|
+
throw new Error('MCP 调用需要 command(stdio)或 url(Streamable HTTP)');
|
|
1260
1274
|
return {
|
|
1261
|
-
|
|
1275
|
+
transport: 'stdio',
|
|
1276
|
+
command,
|
|
1262
1277
|
args: Array.isArray(args.args) ? args.args.map(item => String(item)) : undefined,
|
|
1263
1278
|
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
1264
1279
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.55",
|
|
4
4
|
"description": "Steerable framework — product-neutral Node HostRuntime and headless HTTP server shared by Tauri desktop and browser deployments; scenario packs extend it through @steerable/pack-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -45,39 +45,32 @@
|
|
|
45
45
|
"node": ">=20"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@modelcontextprotocol/sdk": "
|
|
48
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
49
49
|
"better-sqlite3": "^11.9.1",
|
|
50
50
|
"conf": "10.2.0",
|
|
51
51
|
"dotenv": "^17.4.2",
|
|
52
52
|
"electron-log": "^5.4.3",
|
|
53
|
-
"electron-store": "^8.1.0",
|
|
54
|
-
"electron-updater": "^6.8.9",
|
|
55
53
|
"he": "^1.2.0",
|
|
56
54
|
"node-pty": "^1.1.0",
|
|
57
|
-
"
|
|
58
|
-
"@steerable/
|
|
59
|
-
"@steerable/agent-protocol": "0.6.
|
|
60
|
-
"@steerable/pack-sdk": "0.6.52"
|
|
55
|
+
"@steerable/agent-harness": "0.6.55",
|
|
56
|
+
"@steerable/pack-sdk": "0.6.55",
|
|
57
|
+
"@steerable/agent-protocol": "0.6.55"
|
|
61
58
|
},
|
|
62
59
|
"devDependencies": {
|
|
63
60
|
"@types/better-sqlite3": "^7.6.13",
|
|
64
61
|
"@types/he": "^1.2.3",
|
|
65
62
|
"@types/node": "^22.10.5",
|
|
66
|
-
"@types/semver": "^7.8.0",
|
|
67
|
-
"electron": "^28.3.3",
|
|
68
|
-
"esbuild": "^0.25.0",
|
|
69
63
|
"typescript": "^5.8.3",
|
|
70
64
|
"vitest": "^3.2.4"
|
|
71
65
|
},
|
|
72
66
|
"scripts": {
|
|
73
67
|
"gen": "echo agent-shell-ts",
|
|
74
|
-
"build": "tsc -p tsconfig.json && node copy-skills.mjs
|
|
68
|
+
"build": "tsc -p tsconfig.json && node copy-skills.mjs",
|
|
75
69
|
"test": "vitest run",
|
|
76
70
|
"lint": "tsc -p tsconfig.json --noEmit && node scripts/check-shell-neutral.mjs && node scripts/check-storage-boundaries.mjs",
|
|
77
71
|
"check:drift": "echo ok",
|
|
78
72
|
"shell:neutral": "node scripts/check-shell-neutral.mjs",
|
|
79
73
|
"storage:boundaries": "node scripts/check-storage-boundaries.mjs",
|
|
80
|
-
"web": "DEEPPATH_WEB_DIST=${DEEPPATH_WEB_DIST:-../web/app/dist} node dist/server/index.js"
|
|
81
|
-
"client": "DEEPPATH_FORCE_BUNDLED_WEB=1 DEEPPATH_WEB_DIST=${DEEPPATH_WEB_DIST:-../web/app/dist} node node_modules/electron/cli.js dist/main.js"
|
|
74
|
+
"web": "DEEPPATH_WEB_DIST=${DEEPPATH_WEB_DIST:-../web/app/dist} node dist/server/index.js"
|
|
82
75
|
}
|
|
83
76
|
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* electron-updater 适配。只在桌面主进程启用更新时加载。
|
|
3
|
-
*/
|
|
4
|
-
import log from 'electron-log';
|
|
5
|
-
import electronUpdater from 'electron-updater';
|
|
6
|
-
export function createElectronAppUpdater() {
|
|
7
|
-
const { autoUpdater } = electronUpdater;
|
|
8
|
-
autoUpdater.logger = log;
|
|
9
|
-
return {
|
|
10
|
-
get autoDownload() {
|
|
11
|
-
return autoUpdater.autoDownload;
|
|
12
|
-
},
|
|
13
|
-
set autoDownload(value) {
|
|
14
|
-
autoUpdater.autoDownload = value;
|
|
15
|
-
},
|
|
16
|
-
get autoInstallOnAppQuit() {
|
|
17
|
-
return autoUpdater.autoInstallOnAppQuit;
|
|
18
|
-
},
|
|
19
|
-
set autoInstallOnAppQuit(value) {
|
|
20
|
-
autoUpdater.autoInstallOnAppQuit = value;
|
|
21
|
-
},
|
|
22
|
-
get allowDowngrade() {
|
|
23
|
-
return autoUpdater.allowDowngrade;
|
|
24
|
-
},
|
|
25
|
-
set allowDowngrade(value) {
|
|
26
|
-
autoUpdater.allowDowngrade = value;
|
|
27
|
-
},
|
|
28
|
-
setFeedURL(options) {
|
|
29
|
-
autoUpdater.setFeedURL(options);
|
|
30
|
-
},
|
|
31
|
-
async checkForUpdates() {
|
|
32
|
-
const result = await autoUpdater.checkForUpdates();
|
|
33
|
-
if (!result)
|
|
34
|
-
return null;
|
|
35
|
-
return {
|
|
36
|
-
updateInfo: { version: result.updateInfo.version },
|
|
37
|
-
isUpdateAvailable: result.isUpdateAvailable,
|
|
38
|
-
};
|
|
39
|
-
},
|
|
40
|
-
quitAndInstall(isSilent, isForceRunAfter) {
|
|
41
|
-
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
|
|
42
|
-
},
|
|
43
|
-
on(event, listener) {
|
|
44
|
-
if (event === 'download-progress') {
|
|
45
|
-
autoUpdater.on('download-progress', listener);
|
|
46
|
-
}
|
|
47
|
-
else if (event === 'update-downloaded') {
|
|
48
|
-
autoUpdater.on('update-downloaded', listener);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
autoUpdater.on('error', listener);
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
off(event, listener) {
|
|
55
|
-
if (event === 'download-progress') {
|
|
56
|
-
autoUpdater.off('download-progress', listener);
|
|
57
|
-
}
|
|
58
|
-
else if (event === 'update-downloaded') {
|
|
59
|
-
autoUpdater.off('update-downloaded', listener);
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
autoUpdater.off('error', listener);
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
package/dist/app-update.d.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 桌面自动更新。产品经 `ProductConfig.updates.feedUrl` 注入 generic 通道;
|
|
3
|
-
* 本模块只编排检查、下载和安装,具体传输交给 `AppUpdaterPort`
|
|
4
|
-
* (生产环境是 electron-updater)。
|
|
5
|
-
*/
|
|
6
|
-
export declare const APP_UPDATE_CHECK_INTERVAL_MS: number;
|
|
7
|
-
export type AppUpdatePhase = 'disabled' | 'idle' | 'checking' | 'downloading' | 'ready' | 'installing' | 'error';
|
|
8
|
-
export interface AppUpdateState {
|
|
9
|
-
phase: AppUpdatePhase;
|
|
10
|
-
version?: string;
|
|
11
|
-
percent?: number;
|
|
12
|
-
message?: string;
|
|
13
|
-
}
|
|
14
|
-
/** 渲染层看到的版本与更新状态。`version` 是当前安装版本。 */
|
|
15
|
-
export interface AppReleaseSnapshot {
|
|
16
|
-
version: string;
|
|
17
|
-
enabled: boolean;
|
|
18
|
-
phase: AppUpdatePhase;
|
|
19
|
-
availableVersion?: string;
|
|
20
|
-
percent?: number;
|
|
21
|
-
message?: string;
|
|
22
|
-
}
|
|
23
|
-
export declare function toAppReleaseSnapshot(currentVersion: string, state: AppUpdateState): AppReleaseSnapshot;
|
|
24
|
-
export interface AppUpdateCheckResult {
|
|
25
|
-
updateInfo: {
|
|
26
|
-
version: string;
|
|
27
|
-
};
|
|
28
|
-
isUpdateAvailable: boolean;
|
|
29
|
-
}
|
|
30
|
-
export interface AppUpdaterPort {
|
|
31
|
-
autoDownload: boolean;
|
|
32
|
-
autoInstallOnAppQuit: boolean;
|
|
33
|
-
allowDowngrade: boolean;
|
|
34
|
-
setFeedURL(options: {
|
|
35
|
-
provider: 'generic';
|
|
36
|
-
url: string;
|
|
37
|
-
}): void;
|
|
38
|
-
checkForUpdates(): Promise<AppUpdateCheckResult | null>;
|
|
39
|
-
quitAndInstall(isSilent?: boolean, isForceRunAfter?: boolean): void;
|
|
40
|
-
on(event: 'download-progress', listener: (progress: {
|
|
41
|
-
percent: number;
|
|
42
|
-
}) => void): void;
|
|
43
|
-
on(event: 'update-downloaded', listener: (info: {
|
|
44
|
-
version: string;
|
|
45
|
-
}) => void): void;
|
|
46
|
-
on(event: 'error', listener: (error: Error) => void): void;
|
|
47
|
-
off(event: 'download-progress', listener: (progress: {
|
|
48
|
-
percent: number;
|
|
49
|
-
}) => void): void;
|
|
50
|
-
off(event: 'update-downloaded', listener: (info: {
|
|
51
|
-
version: string;
|
|
52
|
-
}) => void): void;
|
|
53
|
-
off(event: 'error', listener: (error: Error) => void): void;
|
|
54
|
-
}
|
|
55
|
-
export interface AppUpdateOptions {
|
|
56
|
-
feedUrl?: string;
|
|
57
|
-
packaged: boolean;
|
|
58
|
-
currentVersion: string;
|
|
59
|
-
env: NodeJS.ProcessEnv;
|
|
60
|
-
updater: AppUpdaterPort;
|
|
61
|
-
onState?: (state: AppUpdateState) => void;
|
|
62
|
-
/** 测试注入。缺省写 `env.DEEPPATH_UPDATE_STATUS_FILE`。 */
|
|
63
|
-
statusFile?: string;
|
|
64
|
-
}
|
|
65
|
-
export declare function appUpdateMenuAction(state: AppUpdateState): 'none' | 'check' | 'install';
|
|
66
|
-
export declare function shouldNotifyUpdateReady(previous: AppUpdatePhase, next: AppUpdateState): boolean;
|
|
67
|
-
export declare class AppUpdateController {
|
|
68
|
-
private readonly options;
|
|
69
|
-
private current;
|
|
70
|
-
private enabled;
|
|
71
|
-
private started;
|
|
72
|
-
private disposed;
|
|
73
|
-
private bound;
|
|
74
|
-
private candidate;
|
|
75
|
-
private downloadedVersion;
|
|
76
|
-
private checkPromise;
|
|
77
|
-
private cancelTimer;
|
|
78
|
-
private readonly statusFile;
|
|
79
|
-
constructor(options: AppUpdateOptions);
|
|
80
|
-
get state(): AppUpdateState;
|
|
81
|
-
start(): Promise<AppUpdateState>;
|
|
82
|
-
checkNow(): Promise<AppUpdateState>;
|
|
83
|
-
installNow(): AppUpdateState;
|
|
84
|
-
dispose(): void;
|
|
85
|
-
private bind;
|
|
86
|
-
private readonly onProgress;
|
|
87
|
-
private readonly onDownloaded;
|
|
88
|
-
private readonly onError;
|
|
89
|
-
private doCheck;
|
|
90
|
-
private isNewer;
|
|
91
|
-
private publish;
|
|
92
|
-
}
|
package/dist/app-update.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 桌面自动更新。产品经 `ProductConfig.updates.feedUrl` 注入 generic 通道;
|
|
3
|
-
* 本模块只编排检查、下载和安装,具体传输交给 `AppUpdaterPort`
|
|
4
|
-
* (生产环境是 electron-updater)。
|
|
5
|
-
*/
|
|
6
|
-
import { appendFileSync } from 'node:fs';
|
|
7
|
-
import { gt, valid } from 'semver';
|
|
8
|
-
export const APP_UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
9
|
-
export function toAppReleaseSnapshot(currentVersion, state) {
|
|
10
|
-
const snapshot = {
|
|
11
|
-
version: currentVersion,
|
|
12
|
-
enabled: state.phase !== 'disabled',
|
|
13
|
-
phase: state.phase,
|
|
14
|
-
};
|
|
15
|
-
if (state.version)
|
|
16
|
-
snapshot.availableVersion = state.version;
|
|
17
|
-
if (state.percent !== undefined)
|
|
18
|
-
snapshot.percent = state.percent;
|
|
19
|
-
if (state.message)
|
|
20
|
-
snapshot.message = state.message;
|
|
21
|
-
return snapshot;
|
|
22
|
-
}
|
|
23
|
-
export function appUpdateMenuAction(state) {
|
|
24
|
-
if (state.phase === 'disabled')
|
|
25
|
-
return 'none';
|
|
26
|
-
if (state.phase === 'ready' || state.phase === 'installing')
|
|
27
|
-
return 'install';
|
|
28
|
-
return 'check';
|
|
29
|
-
}
|
|
30
|
-
export function shouldNotifyUpdateReady(previous, next) {
|
|
31
|
-
return previous !== 'ready' && next.phase === 'ready' && Boolean(next.version);
|
|
32
|
-
}
|
|
33
|
-
function resolveFeed(options) {
|
|
34
|
-
const override = options.env.DEEPPATH_UPDATE_FEED_URL?.trim() ?? '';
|
|
35
|
-
const feedUrl = override || options.feedUrl?.trim() || '';
|
|
36
|
-
const forceUnpackaged = options.env.DEEPPATH_APP_UPDATE === '1';
|
|
37
|
-
if (!feedUrl || (!options.packaged && !forceUnpackaged))
|
|
38
|
-
return { enabled: false };
|
|
39
|
-
return { enabled: true, feedUrl };
|
|
40
|
-
}
|
|
41
|
-
function clampPercent(percent) {
|
|
42
|
-
if (!Number.isFinite(percent))
|
|
43
|
-
return 0;
|
|
44
|
-
return Math.min(100, Math.max(0, percent));
|
|
45
|
-
}
|
|
46
|
-
function defaultSchedule(fn, ms) {
|
|
47
|
-
const timer = setInterval(fn, ms);
|
|
48
|
-
timer.unref();
|
|
49
|
-
return () => clearInterval(timer);
|
|
50
|
-
}
|
|
51
|
-
export class AppUpdateController {
|
|
52
|
-
options;
|
|
53
|
-
current = { phase: 'disabled' };
|
|
54
|
-
enabled = false;
|
|
55
|
-
started = false;
|
|
56
|
-
disposed = false;
|
|
57
|
-
bound = false;
|
|
58
|
-
candidate;
|
|
59
|
-
downloadedVersion;
|
|
60
|
-
checkPromise;
|
|
61
|
-
cancelTimer;
|
|
62
|
-
statusFile;
|
|
63
|
-
constructor(options) {
|
|
64
|
-
this.options = options;
|
|
65
|
-
this.statusFile = options.statusFile ?? options.env.DEEPPATH_UPDATE_STATUS_FILE;
|
|
66
|
-
}
|
|
67
|
-
get state() {
|
|
68
|
-
return this.current;
|
|
69
|
-
}
|
|
70
|
-
async start() {
|
|
71
|
-
if (this.started)
|
|
72
|
-
return this.checkPromise ?? this.state;
|
|
73
|
-
this.started = true;
|
|
74
|
-
const target = resolveFeed(this.options);
|
|
75
|
-
if (!target.enabled)
|
|
76
|
-
return this.publish({ phase: 'disabled' });
|
|
77
|
-
this.enabled = true;
|
|
78
|
-
this.options.updater.autoDownload = true;
|
|
79
|
-
this.options.updater.autoInstallOnAppQuit = true;
|
|
80
|
-
this.options.updater.allowDowngrade = false;
|
|
81
|
-
this.options.updater.setFeedURL({ provider: 'generic', url: target.feedUrl });
|
|
82
|
-
this.bind();
|
|
83
|
-
const first = this.checkNow();
|
|
84
|
-
this.cancelTimer = defaultSchedule(() => {
|
|
85
|
-
void this.checkNow();
|
|
86
|
-
}, APP_UPDATE_CHECK_INTERVAL_MS);
|
|
87
|
-
return first;
|
|
88
|
-
}
|
|
89
|
-
checkNow() {
|
|
90
|
-
if (this.disposed || !this.enabled)
|
|
91
|
-
return Promise.resolve(this.state);
|
|
92
|
-
if (this.state.phase === 'downloading' || this.state.phase === 'ready' || this.state.phase === 'installing') {
|
|
93
|
-
return Promise.resolve(this.state);
|
|
94
|
-
}
|
|
95
|
-
if (this.checkPromise)
|
|
96
|
-
return this.checkPromise;
|
|
97
|
-
this.checkPromise = this.doCheck().finally(() => {
|
|
98
|
-
this.checkPromise = undefined;
|
|
99
|
-
});
|
|
100
|
-
return this.checkPromise;
|
|
101
|
-
}
|
|
102
|
-
installNow() {
|
|
103
|
-
if (this.disposed || this.state.phase !== 'ready' || !this.downloadedVersion)
|
|
104
|
-
return this.state;
|
|
105
|
-
const version = this.downloadedVersion;
|
|
106
|
-
this.publish({ phase: 'installing', version });
|
|
107
|
-
try {
|
|
108
|
-
this.options.updater.quitAndInstall(false, true);
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
return this.publish({
|
|
112
|
-
phase: 'error',
|
|
113
|
-
version,
|
|
114
|
-
message: error instanceof Error ? error.message : String(error),
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return this.state;
|
|
118
|
-
}
|
|
119
|
-
dispose() {
|
|
120
|
-
this.disposed = true;
|
|
121
|
-
this.cancelTimer?.();
|
|
122
|
-
this.cancelTimer = undefined;
|
|
123
|
-
if (!this.bound)
|
|
124
|
-
return;
|
|
125
|
-
this.options.updater.off('download-progress', this.onProgress);
|
|
126
|
-
this.options.updater.off('update-downloaded', this.onDownloaded);
|
|
127
|
-
this.options.updater.off('error', this.onError);
|
|
128
|
-
this.bound = false;
|
|
129
|
-
}
|
|
130
|
-
bind() {
|
|
131
|
-
if (this.bound)
|
|
132
|
-
return;
|
|
133
|
-
this.bound = true;
|
|
134
|
-
this.options.updater.on('download-progress', this.onProgress);
|
|
135
|
-
this.options.updater.on('update-downloaded', this.onDownloaded);
|
|
136
|
-
this.options.updater.on('error', this.onError);
|
|
137
|
-
}
|
|
138
|
-
onProgress = (progress) => {
|
|
139
|
-
if (this.downloadedVersion)
|
|
140
|
-
return;
|
|
141
|
-
this.publish({
|
|
142
|
-
phase: 'downloading',
|
|
143
|
-
...(this.candidate ? { version: this.candidate } : {}),
|
|
144
|
-
percent: clampPercent(progress.percent),
|
|
145
|
-
});
|
|
146
|
-
};
|
|
147
|
-
onDownloaded = (info) => {
|
|
148
|
-
if (!this.isNewer(info.version))
|
|
149
|
-
return;
|
|
150
|
-
this.downloadedVersion = info.version;
|
|
151
|
-
this.candidate = info.version;
|
|
152
|
-
this.publish({ phase: 'ready', version: info.version });
|
|
153
|
-
};
|
|
154
|
-
onError = (error) => {
|
|
155
|
-
const version = this.candidate ?? this.state.version;
|
|
156
|
-
this.publish({
|
|
157
|
-
phase: 'error',
|
|
158
|
-
...(version ? { version } : {}),
|
|
159
|
-
message: error instanceof Error ? error.message : String(error),
|
|
160
|
-
});
|
|
161
|
-
};
|
|
162
|
-
async doCheck() {
|
|
163
|
-
this.downloadedVersion = undefined;
|
|
164
|
-
this.candidate = undefined;
|
|
165
|
-
this.publish({ phase: 'checking' });
|
|
166
|
-
try {
|
|
167
|
-
const result = await this.options.updater.checkForUpdates();
|
|
168
|
-
if (this.disposed)
|
|
169
|
-
return this.state;
|
|
170
|
-
if (this.downloadedVersion && this.isNewer(this.downloadedVersion)) {
|
|
171
|
-
if (this.state.phase === 'ready' && this.state.version === this.downloadedVersion)
|
|
172
|
-
return this.state;
|
|
173
|
-
return this.publish({ phase: 'ready', version: this.downloadedVersion });
|
|
174
|
-
}
|
|
175
|
-
if (!result)
|
|
176
|
-
return this.publish({ phase: 'error', message: 'app update: no check result' });
|
|
177
|
-
const version = result.updateInfo.version;
|
|
178
|
-
if (!result.isUpdateAvailable)
|
|
179
|
-
return this.publish({ phase: 'idle' });
|
|
180
|
-
if (valid(version) === null) {
|
|
181
|
-
return this.publish({ phase: 'error', message: 'app update: feed version is invalid' });
|
|
182
|
-
}
|
|
183
|
-
if (!this.isNewer(version))
|
|
184
|
-
return this.publish({ phase: 'idle' });
|
|
185
|
-
this.candidate = version;
|
|
186
|
-
if (this.state.phase === 'downloading') {
|
|
187
|
-
return this.publish({ phase: 'downloading', version, percent: this.state.percent ?? 0 });
|
|
188
|
-
}
|
|
189
|
-
return this.publish({ phase: 'downloading', version, percent: 0 });
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
if (this.disposed)
|
|
193
|
-
return this.state;
|
|
194
|
-
return this.publish({
|
|
195
|
-
phase: 'error',
|
|
196
|
-
...(this.candidate ? { version: this.candidate } : {}),
|
|
197
|
-
message: error instanceof Error ? error.message : String(error),
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
isNewer(version) {
|
|
202
|
-
return valid(version) !== null && valid(this.options.currentVersion) !== null && gt(version, this.options.currentVersion);
|
|
203
|
-
}
|
|
204
|
-
publish(state) {
|
|
205
|
-
if (this.disposed)
|
|
206
|
-
return this.current;
|
|
207
|
-
this.current = state;
|
|
208
|
-
if (this.statusFile)
|
|
209
|
-
appendFileSync(this.statusFile, `${JSON.stringify(state)}\n`);
|
|
210
|
-
this.options.onState?.(state);
|
|
211
|
-
return state;
|
|
212
|
-
}
|
|
213
|
-
}
|
package/dist/host/ipc.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 包 IPC 绑定器(0.3e):把包的 IpcContribution 注册到 Electron ipcMain。
|
|
3
|
-
*
|
|
4
|
-
* 命名空间规则:channel 必须以 `<packId>:` 开头——包只能占用自己的
|
|
5
|
-
* 命名空间,注册期 fail fast。handler 统一收单个序列化载荷(renderer
|
|
6
|
-
* 经 preload 透传),返回值经 IPC 结构化克隆回 renderer。
|
|
7
|
-
*/
|
|
8
|
-
import type { IpcContribution } from '../scenario/pack.js';
|
|
9
|
-
/** 命名空间校验(纯函数,单测直接覆盖)。 */
|
|
10
|
-
export declare function assertPackChannel(packId: string, channel: string): void;
|
|
11
|
-
export declare function registerPackIpc(packId: string, contributions: readonly IpcContribution[]): void;
|