@steerable/agent-shell 0.6.53 → 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/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.js +8 -7
- package/dist/local-backend/router.js +2 -2
- 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-script-registry.js +2 -2
- package/dist/mcp-server-registry.d.ts +3 -3
- package/dist/mcp-server-registry.js +2 -2
- package/dist/product-config.d.ts +10 -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/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/package.json +6 -13
- 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
package/dist/main.js
DELETED
|
@@ -1,967 +0,0 @@
|
|
|
1
|
-
// 产品组装根(products/<id>/main.ts 或 devtools/dev-main.ts)必须先于本
|
|
2
|
-
// 模块完成 import:包迁移注册要先于 storage 单例构造(0.3b / 2.3)。
|
|
3
|
-
import { setSecondInstanceHandler } from './single-instance.js';
|
|
4
|
-
import { app, BrowserWindow, Menu, screen, shell, nativeTheme, ipcMain, clipboard, dialog, net as electronNet, Notification as ElectronNotification, session, } from 'electron';
|
|
5
|
-
import { randomUUID } from 'crypto';
|
|
6
|
-
import { existsSync, promises as fsPromises } from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
|
-
import dotenv from 'dotenv';
|
|
10
|
-
import log from 'electron-log';
|
|
11
|
-
import { getBrand } from './brand.js';
|
|
12
|
-
import { getPreloadPath } from './runtime.js';
|
|
13
|
-
import { getProductConfig } from './product-config.js';
|
|
14
|
-
import { AppUpdateController, appUpdateMenuAction, shouldNotifyUpdateReady, toAppReleaseSnapshot, } from './app-update.js';
|
|
15
|
-
import { createElectronAppUpdater } from './app-update-electron.js';
|
|
16
|
-
import { assertHostIpcAllowed, getResolvedHostTools } from './host-tools-runtime.js';
|
|
17
|
-
import { createJsonStore } from './json-store.js';
|
|
18
|
-
import { getUserDataDir } from './runtime.js';
|
|
19
|
-
import { saveAttachmentFiles } from './attachments.js';
|
|
20
|
-
// Re-derive __filename / __dirname for ESM modules. Used below for resolving
|
|
21
|
-
// paths relative to the compiled main.js (preload, dotenv files, page HTML).
|
|
22
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
-
const __dirname = path.dirname(__filename);
|
|
24
|
-
// Load env files BEFORE anything else reads process.env so the preload
|
|
25
|
-
// (which inherits process.env from the main process) can see them. Loading
|
|
26
|
-
// .env.local takes precedence over .env(与上游 Python 服务同约定)。
|
|
27
|
-
// Existing process.env values always win (override: false).
|
|
28
|
-
(function loadDotenvFiles() {
|
|
29
|
-
// dist/src/main.js → 仓库根是 ../..(rootDir=仓库根,dist 镜像源码树)。
|
|
30
|
-
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
31
|
-
for (const file of ['.env.local', '.env']) {
|
|
32
|
-
const fullPath = path.join(repoRoot, file);
|
|
33
|
-
if (existsSync(fullPath)) {
|
|
34
|
-
dotenv.config({ path: fullPath, override: false });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
})();
|
|
38
|
-
// 让 Electron 自身的 userData(electron-log 文件日志、Chromium 缓存等)
|
|
39
|
-
// 跟随 DEEPPATH_USER_DATA_DIR——否则只读 HOME 上 `pnpm dev` 的日志/缓存
|
|
40
|
-
// 仍会写 ~/.config/<appName> 并 EROFS。未设置时是 no-op(返回值就是
|
|
41
|
-
// 默认 userData)。
|
|
42
|
-
app.setPath('userData', getUserDataDir());
|
|
43
|
-
import { getActiveCoreLoopStreamId } from './local-backend/coreloop-stream.js';
|
|
44
|
-
import { getSidecarSupervisor } from './llm/index.js';
|
|
45
|
-
import { createHostRuntime } from './host/runtime.js';
|
|
46
|
-
import { registerPackIpc } from './host/ipc.js';
|
|
47
|
-
const store = createJsonStore({ name: 'config' });
|
|
48
|
-
const FORCE_BUNDLED_WEB = process.env.DEEPPATH_FORCE_BUNDLED_WEB === '1';
|
|
49
|
-
const USE_BUNDLED_WEB = app.isPackaged || FORCE_BUNDLED_WEB;
|
|
50
|
-
const IS_DEV = !USE_BUNDLED_WEB;
|
|
51
|
-
const WEB_DEV_URL = process.env.DEEPPATH_WEB_DEV_URL || 'http://127.0.0.1:5173';
|
|
52
|
-
const START_PATH = process.env.DEEPPATH_START_PATH || '/agent';
|
|
53
|
-
// 主图标路径——dev 时是仓库 assets/,打包后是 app.asar/assets/。
|
|
54
|
-
// Linux 必须显式给 BrowserWindow 一个 icon(不会自动从 .deb/.AppImage 继承);
|
|
55
|
-
// Windows 一般用 .exe 自带的 ICO,但 dev 模式下传 PNG 能让任务栏立刻看到 logo;
|
|
56
|
-
// macOS 用 .icns(package.json 里 electron-builder 配置已指向 icon-mac.png)。
|
|
57
|
-
function resolveAppIconPath() {
|
|
58
|
-
const assetsDir = path.join(app.getAppPath(), 'assets');
|
|
59
|
-
const flavorIcon = path.join(assetsDir, `icon-${getBrand().flavor}.png`);
|
|
60
|
-
if (existsSync(flavorIcon))
|
|
61
|
-
return flavorIcon;
|
|
62
|
-
return path.join(assetsDir, 'icon.png');
|
|
63
|
-
}
|
|
64
|
-
const APP_ICON_PATH = resolveAppIconPath();
|
|
65
|
-
let mainWindow = null;
|
|
66
|
-
let isQuitting = false;
|
|
67
|
-
let retryTimer = null;
|
|
68
|
-
let retryCount = 0;
|
|
69
|
-
const MAX_RETRY_COUNT = 10;
|
|
70
|
-
const RETRY_DELAYS = [2000, 5000, 10000, 30000];
|
|
71
|
-
// 全部宿主服务由共享 HostRuntime 装配(与 BS server 同一份,见
|
|
72
|
-
// src/host/runtime.ts 模块头——历史上两处逐行重复,BS 曾漏接 TaskService
|
|
73
|
-
// 导致模型退化成"假后台")。CS 的差异只剩广播(IPC)与生命周期。
|
|
74
|
-
let runtime;
|
|
75
|
-
let hostStore;
|
|
76
|
-
let localExecutor;
|
|
77
|
-
let localScriptRegistry;
|
|
78
|
-
let terminalManager;
|
|
79
|
-
let packHandles;
|
|
80
|
-
let localBackendRouter;
|
|
81
|
-
let approvalBridge;
|
|
82
|
-
let askUserBridge;
|
|
83
|
-
let maybeExecInTerminal;
|
|
84
|
-
let routeWindow = null;
|
|
85
|
-
// 进行中的 agent 流(streamId → AbortController)。cancelStream IPC、
|
|
86
|
-
// 窗口销毁、应用退出时统一 abort,保证不会有"关了还在跑"的幽灵循环。
|
|
87
|
-
const activeStreamControllers = new Map();
|
|
88
|
-
// 终端已内嵌进主窗口(/terminal 路由),不再有独立终端窗。PTY 输出广播到
|
|
89
|
-
// 所有窗口——没挂载 TerminalView 的窗口没有 listener,天然忽略;主窗切走
|
|
90
|
-
// 路由后 xterm 卸载,回来时靠 `terminal:ensure` 的 replay buffer 补输出,
|
|
91
|
-
// 所以这里不需要任何"窗口就绪"排队机制。
|
|
92
|
-
function broadcastTerminalEvent(channel, payload) {
|
|
93
|
-
for (const win of BrowserWindow.getAllWindows()) {
|
|
94
|
-
if (!win.isDestroyed())
|
|
95
|
-
win.webContents.send(channel, payload);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
function getStartPath() {
|
|
99
|
-
if (!START_PATH)
|
|
100
|
-
return '/agent';
|
|
101
|
-
return START_PATH.startsWith('/') ? START_PATH : `/${START_PATH}`;
|
|
102
|
-
}
|
|
103
|
-
function getBundledWebIndexPath() {
|
|
104
|
-
// 2.3 起 web 产物按产品构建:dev/未打包运行时由产品入口注入
|
|
105
|
-
// DEEPPATH_WEB_DIST(products/<id>/web/dist);打包后 electron-builder
|
|
106
|
-
// 把产品 web dist 映射为包内 web-dist/(全产品统一约定)。
|
|
107
|
-
const injected = process.env.DEEPPATH_WEB_DIST;
|
|
108
|
-
if (injected)
|
|
109
|
-
return path.join(injected, 'index.html');
|
|
110
|
-
return path.join(app.getAppPath(), 'web-dist', 'index.html');
|
|
111
|
-
}
|
|
112
|
-
async function loadWebRoute(win, routePath) {
|
|
113
|
-
const route = routePath.startsWith('/') ? routePath : `/${routePath}`;
|
|
114
|
-
if (IS_DEV) {
|
|
115
|
-
const url = new URL(WEB_DEV_URL);
|
|
116
|
-
url.hash = route;
|
|
117
|
-
await win.loadURL(url.toString());
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
await win.loadFile(getBundledWebIndexPath(), { hash: route });
|
|
121
|
-
}
|
|
122
|
-
async function checkDevServerConnection() {
|
|
123
|
-
return new Promise(resolve => {
|
|
124
|
-
const request = electronNet.request({ method: 'HEAD', url: WEB_DEV_URL });
|
|
125
|
-
const timeoutId = setTimeout(() => {
|
|
126
|
-
request.abort();
|
|
127
|
-
resolve(false);
|
|
128
|
-
}, 3000);
|
|
129
|
-
request.on('response', response => {
|
|
130
|
-
clearTimeout(timeoutId);
|
|
131
|
-
resolve((response.statusCode || 0) > 0);
|
|
132
|
-
});
|
|
133
|
-
request.on('error', () => {
|
|
134
|
-
clearTimeout(timeoutId);
|
|
135
|
-
resolve(false);
|
|
136
|
-
});
|
|
137
|
-
request.on('abort', () => {
|
|
138
|
-
clearTimeout(timeoutId);
|
|
139
|
-
resolve(false);
|
|
140
|
-
});
|
|
141
|
-
request.end();
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
async function loadOfflinePage() {
|
|
145
|
-
if (!mainWindow || mainWindow.isDestroyed())
|
|
146
|
-
return;
|
|
147
|
-
const offlinePath = path.join(__dirname, '../pages/offline.html');
|
|
148
|
-
await mainWindow.loadFile(offlinePath, { query: { brand: getBrand().displayName } });
|
|
149
|
-
mainWindow.webContents.send('network-status-changed', { online: false });
|
|
150
|
-
scheduleRetry();
|
|
151
|
-
}
|
|
152
|
-
function scheduleRetry() {
|
|
153
|
-
if (retryTimer)
|
|
154
|
-
clearTimeout(retryTimer);
|
|
155
|
-
if (retryCount >= MAX_RETRY_COUNT)
|
|
156
|
-
return;
|
|
157
|
-
const delayMs = RETRY_DELAYS[Math.min(retryCount, RETRY_DELAYS.length - 1)];
|
|
158
|
-
retryTimer = setTimeout(async () => {
|
|
159
|
-
retryCount++;
|
|
160
|
-
const isUp = await checkDevServerConnection();
|
|
161
|
-
if (isUp) {
|
|
162
|
-
await loadApp();
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
scheduleRetry();
|
|
166
|
-
}
|
|
167
|
-
}, delayMs);
|
|
168
|
-
}
|
|
169
|
-
async function loadApp() {
|
|
170
|
-
if (!mainWindow || mainWindow.isDestroyed())
|
|
171
|
-
return;
|
|
172
|
-
if (IS_DEV) {
|
|
173
|
-
try {
|
|
174
|
-
await loadWebRoute(mainWindow, getStartPath());
|
|
175
|
-
retryCount = 0;
|
|
176
|
-
if (retryTimer) {
|
|
177
|
-
clearTimeout(retryTimer);
|
|
178
|
-
retryTimer = null;
|
|
179
|
-
}
|
|
180
|
-
mainWindow.webContents.send('network-status-changed', { online: true });
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
await loadOfflinePage();
|
|
184
|
-
}
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
await loadWebRoute(mainWindow, getStartPath());
|
|
189
|
-
mainWindow.webContents.send('network-status-changed', { online: true });
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
dialog.showErrorBox('应用启动失败', '内置前端资源可能缺失或损坏,请重新安装。');
|
|
193
|
-
app.quit();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
function applyStrictCsp() {
|
|
197
|
-
// Dev 模式下不强加 CSP:Next.js dev server 的静态资源 MIME 经常是 text/plain,
|
|
198
|
-
// strict CSP + nosniff 会把所有 CSS/JS 全部拒掉,页面卡在 SSR fallback。
|
|
199
|
-
// Bundled production 模式才应用 CSP。现在生产模式走 file:// + Vite 打包产物,
|
|
200
|
-
// 需要允许 file: 源;connect 只放行本机 Ollama(用户可在设置里改 URL)。
|
|
201
|
-
if (IS_DEV) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
205
|
-
const csp = [
|
|
206
|
-
"default-src 'self' file:",
|
|
207
|
-
"connect-src 'self' file: http://127.0.0.1:11434",
|
|
208
|
-
"img-src 'self' data: blob:",
|
|
209
|
-
"style-src 'self' 'unsafe-inline'",
|
|
210
|
-
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
|
|
211
|
-
"font-src 'self' data: blob:",
|
|
212
|
-
].join('; ');
|
|
213
|
-
callback({
|
|
214
|
-
responseHeaders: {
|
|
215
|
-
...details.responseHeaders,
|
|
216
|
-
'Content-Security-Policy': [csp],
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
function createWindow() {
|
|
222
|
-
// 出厂默认窗口按主屏工作区缩放( capped ):480×720 的手机尺寸在桌面端
|
|
223
|
-
// 太窄,侧栏 + 对话区展不开。用户 resize/move 后持久化,之后的启动走
|
|
224
|
-
// 已存状态,不再回这里。
|
|
225
|
-
const workArea = screen.getPrimaryDisplay().workAreaSize;
|
|
226
|
-
const defaultState = {
|
|
227
|
-
width: Math.min(1280, Math.max(720, Math.round(workArea.width * 0.7))),
|
|
228
|
-
height: Math.min(860, Math.max(600, Math.round(workArea.height * 0.85))),
|
|
229
|
-
isMaximized: false,
|
|
230
|
-
};
|
|
231
|
-
const savedState = store.get('windowState', defaultState);
|
|
232
|
-
// 老版本出厂默认是 480×720,且 close 时无条件持久化——没调整过窗口的
|
|
233
|
-
// 老安装也存着这个值。把它视为"从未自定义",升级后跟随新默认。
|
|
234
|
-
const isLegacyDefault = savedState.width === 480 && savedState.height === 720 && !savedState.isMaximized;
|
|
235
|
-
const windowState = isLegacyDefault ? defaultState : savedState;
|
|
236
|
-
mainWindow = new BrowserWindow({
|
|
237
|
-
width: windowState.width,
|
|
238
|
-
height: windowState.height,
|
|
239
|
-
x: windowState.x,
|
|
240
|
-
y: windowState.y,
|
|
241
|
-
minWidth: 420,
|
|
242
|
-
minHeight: 600,
|
|
243
|
-
title: getBrand().displayName,
|
|
244
|
-
icon: APP_ICON_PATH,
|
|
245
|
-
webPreferences: {
|
|
246
|
-
preload: getPreloadPath(__dirname),
|
|
247
|
-
contextIsolation: true,
|
|
248
|
-
nodeIntegration: false,
|
|
249
|
-
webSecurity: true,
|
|
250
|
-
allowRunningInsecureContent: false,
|
|
251
|
-
},
|
|
252
|
-
show: false,
|
|
253
|
-
backgroundColor: '#ffffff',
|
|
254
|
-
});
|
|
255
|
-
if (windowState.isMaximized)
|
|
256
|
-
mainWindow.maximize();
|
|
257
|
-
const loadingPath = path.join(__dirname, '../pages/loading.html');
|
|
258
|
-
void mainWindow.loadFile(loadingPath, { query: { brand: getBrand().displayName } });
|
|
259
|
-
mainWindow.once('ready-to-show', () => {
|
|
260
|
-
mainWindow?.show();
|
|
261
|
-
setTimeout(() => void loadApp(), 300);
|
|
262
|
-
});
|
|
263
|
-
const saveWindowState = () => {
|
|
264
|
-
if (!mainWindow)
|
|
265
|
-
return;
|
|
266
|
-
const bounds = mainWindow.getBounds();
|
|
267
|
-
store.set('windowState', {
|
|
268
|
-
width: bounds.width,
|
|
269
|
-
height: bounds.height,
|
|
270
|
-
x: bounds.x,
|
|
271
|
-
y: bounds.y,
|
|
272
|
-
isMaximized: mainWindow.isMaximized(),
|
|
273
|
-
});
|
|
274
|
-
};
|
|
275
|
-
mainWindow.on('resize', saveWindowState);
|
|
276
|
-
mainWindow.on('move', saveWindowState);
|
|
277
|
-
mainWindow.on('close', event => {
|
|
278
|
-
saveWindowState();
|
|
279
|
-
// 只有 macOS 保留"关窗口 = 隐藏到 Dock"的惯例。Windows / Linux 上没有
|
|
280
|
-
// 托盘图标,隐藏窗口等于留下一个用户看不见、只能靠任务管理器杀掉的
|
|
281
|
-
// 后台进程——这正是"关闭软件后有后台残留"的根因。非 macOS 直接退出。
|
|
282
|
-
if (!isQuitting && process.platform === 'darwin') {
|
|
283
|
-
event.preventDefault();
|
|
284
|
-
mainWindow?.hide();
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
mainWindow.on('closed', () => {
|
|
288
|
-
mainWindow = null;
|
|
289
|
-
if (process.platform !== 'darwin') {
|
|
290
|
-
app.quit();
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
294
|
-
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
295
|
-
void shell.openExternal(url);
|
|
296
|
-
}
|
|
297
|
-
return { action: 'deny' };
|
|
298
|
-
});
|
|
299
|
-
// Lock the window title. Without this, Electron auto-syncs from the page's
|
|
300
|
-
// <title> (产品页可能带营销长标题). Calling preventDefault() keeps whatever
|
|
301
|
-
// title we set on the BrowserWindow.
|
|
302
|
-
mainWindow.on('page-title-updated', event => {
|
|
303
|
-
event.preventDefault();
|
|
304
|
-
});
|
|
305
|
-
// Auto-open DevTools in dev mode so renderer issues are visible immediately.
|
|
306
|
-
// Set DEEPPATH_DEVTOOLS=0 to suppress, or =1 in packaged builds to enable.
|
|
307
|
-
const devtoolsFlag = process.env.DEEPPATH_DEVTOOLS;
|
|
308
|
-
const shouldOpenDevtools = devtoolsFlag === '1' || (IS_DEV && devtoolsFlag !== '0');
|
|
309
|
-
if (shouldOpenDevtools) {
|
|
310
|
-
mainWindow.webContents.once('did-finish-load', () => {
|
|
311
|
-
try {
|
|
312
|
-
mainWindow?.webContents.openDevTools({ mode: 'detach' });
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
// ignore — devtools are optional
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
createMenu();
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* 宿主窗口能力(2.3):为包打开一个加载应用内路由的独立窗口(如包
|
|
323
|
-
* 自带的调试/日志窗)。同一时刻只保留一个独立路由窗口,重复打开聚焦
|
|
324
|
-
* 既有窗口。复用 preload.js 暴露的 window.electron API。
|
|
325
|
-
*/
|
|
326
|
-
async function openRouteWindow(route, options) {
|
|
327
|
-
if (routeWindow && !routeWindow.isDestroyed()) {
|
|
328
|
-
routeWindow.show();
|
|
329
|
-
routeWindow.focus();
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
routeWindow = new BrowserWindow({
|
|
333
|
-
width: 920,
|
|
334
|
-
height: 620,
|
|
335
|
-
minWidth: 640,
|
|
336
|
-
minHeight: 360,
|
|
337
|
-
title: `${getBrand().displayName} · ${options.title}`,
|
|
338
|
-
icon: APP_ICON_PATH,
|
|
339
|
-
backgroundColor: '#0b0b0c',
|
|
340
|
-
webPreferences: {
|
|
341
|
-
preload: getPreloadPath(__dirname),
|
|
342
|
-
contextIsolation: true,
|
|
343
|
-
nodeIntegration: false,
|
|
344
|
-
webSecurity: true,
|
|
345
|
-
allowRunningInsecureContent: false,
|
|
346
|
-
},
|
|
347
|
-
show: false,
|
|
348
|
-
});
|
|
349
|
-
routeWindow.on('page-title-updated', (event) => {
|
|
350
|
-
event.preventDefault();
|
|
351
|
-
});
|
|
352
|
-
routeWindow.on('closed', () => {
|
|
353
|
-
routeWindow = null;
|
|
354
|
-
});
|
|
355
|
-
routeWindow.show();
|
|
356
|
-
await loadWebRoute(routeWindow, route);
|
|
357
|
-
if (process.env.DEEPPATH_DEVTOOLS === '1' || (IS_DEV && process.env.DEEPPATH_DEVTOOLS !== '0')) {
|
|
358
|
-
try {
|
|
359
|
-
routeWindow.webContents.openDevTools({ mode: 'detach' });
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
/* ignore */
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function createMenu() {
|
|
367
|
-
const isMac = process.platform === 'darwin';
|
|
368
|
-
const appName = app.getName();
|
|
369
|
-
const template = [
|
|
370
|
-
// macOS 必须有 app 菜单(首位),否则 Cmd+H/Cmd+Q 等系统快捷键也会失效
|
|
371
|
-
...(isMac
|
|
372
|
-
? [
|
|
373
|
-
{
|
|
374
|
-
label: appName,
|
|
375
|
-
submenu: [
|
|
376
|
-
{ role: 'about', label: `关于 ${appName}` },
|
|
377
|
-
{ type: 'separator' },
|
|
378
|
-
{ role: 'services', label: '服务' },
|
|
379
|
-
{ type: 'separator' },
|
|
380
|
-
{ role: 'hide', label: `隐藏 ${appName}` },
|
|
381
|
-
{ role: 'hideOthers', label: '隐藏其他' },
|
|
382
|
-
{ role: 'unhide', label: '全部显示' },
|
|
383
|
-
{ type: 'separator' },
|
|
384
|
-
{ role: 'quit', label: `退出 ${appName}` },
|
|
385
|
-
],
|
|
386
|
-
},
|
|
387
|
-
]
|
|
388
|
-
: []),
|
|
389
|
-
{
|
|
390
|
-
label: '文件',
|
|
391
|
-
submenu: [
|
|
392
|
-
{
|
|
393
|
-
label: '新建对话',
|
|
394
|
-
accelerator: 'CmdOrCtrl+N',
|
|
395
|
-
click: () => mainWindow?.webContents.send('menu:new-chat'),
|
|
396
|
-
},
|
|
397
|
-
{ type: 'separator' },
|
|
398
|
-
isMac ? { role: 'close', label: '关闭窗口' } : { role: 'quit', label: '退出' },
|
|
399
|
-
],
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
label: '编辑',
|
|
403
|
-
submenu: [
|
|
404
|
-
{ role: 'undo', label: '撤销' },
|
|
405
|
-
{ role: 'redo', label: '重做' },
|
|
406
|
-
{ type: 'separator' },
|
|
407
|
-
{ role: 'cut', label: '剪切' },
|
|
408
|
-
{ role: 'copy', label: '复制' },
|
|
409
|
-
{ role: 'paste', label: '粘贴' },
|
|
410
|
-
...(isMac
|
|
411
|
-
? [
|
|
412
|
-
{ role: 'pasteAndMatchStyle', label: '粘贴并匹配样式' },
|
|
413
|
-
{ role: 'delete', label: '删除' },
|
|
414
|
-
{ role: 'selectAll', label: '全选' },
|
|
415
|
-
{ type: 'separator' },
|
|
416
|
-
{
|
|
417
|
-
label: '语音',
|
|
418
|
-
submenu: [
|
|
419
|
-
{ role: 'startSpeaking', label: '开始朗读' },
|
|
420
|
-
{ role: 'stopSpeaking', label: '停止朗读' },
|
|
421
|
-
],
|
|
422
|
-
},
|
|
423
|
-
]
|
|
424
|
-
: [
|
|
425
|
-
{ role: 'delete', label: '删除' },
|
|
426
|
-
{ type: 'separator' },
|
|
427
|
-
{ role: 'selectAll', label: '全选' },
|
|
428
|
-
]),
|
|
429
|
-
],
|
|
430
|
-
},
|
|
431
|
-
{
|
|
432
|
-
label: '视图',
|
|
433
|
-
submenu: [
|
|
434
|
-
...(getResolvedHostTools().terminal.chrome
|
|
435
|
-
? [
|
|
436
|
-
{
|
|
437
|
-
label: '打开终端',
|
|
438
|
-
accelerator: 'CmdOrCtrl+T',
|
|
439
|
-
click: () => {
|
|
440
|
-
// 终端内嵌在主窗口的 /terminal 路由——通知 renderer 导航过去。
|
|
441
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
442
|
-
mainWindow.show();
|
|
443
|
-
mainWindow.focus();
|
|
444
|
-
mainWindow.webContents.send('menu:open-terminal');
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
},
|
|
448
|
-
{ type: 'separator' },
|
|
449
|
-
]
|
|
450
|
-
: []),
|
|
451
|
-
{ role: 'reload', label: '重新加载' },
|
|
452
|
-
{ role: 'forceReload', label: '强制重新加载' },
|
|
453
|
-
{ type: 'separator' },
|
|
454
|
-
{ role: 'toggleDevTools', label: '切换开发者工具' },
|
|
455
|
-
{ type: 'separator' },
|
|
456
|
-
{ role: 'resetZoom', label: '实际大小' },
|
|
457
|
-
{ role: 'zoomIn', label: '放大' },
|
|
458
|
-
{ role: 'zoomOut', label: '缩小' },
|
|
459
|
-
{ type: 'separator' },
|
|
460
|
-
{ role: 'togglefullscreen', label: '切换全屏' },
|
|
461
|
-
],
|
|
462
|
-
},
|
|
463
|
-
...(isMac
|
|
464
|
-
? [
|
|
465
|
-
{
|
|
466
|
-
label: '窗口',
|
|
467
|
-
submenu: [
|
|
468
|
-
{ role: 'minimize', label: '最小化' },
|
|
469
|
-
{ role: 'zoom', label: '缩放' },
|
|
470
|
-
{ type: 'separator' },
|
|
471
|
-
{ role: 'front', label: '全部置于顶层' },
|
|
472
|
-
],
|
|
473
|
-
},
|
|
474
|
-
]
|
|
475
|
-
: []),
|
|
476
|
-
// 帮助菜单:更新动作来自自动更新状态;链接是产品注入配置
|
|
477
|
-
// (3.1,product.json links)。都没有则不渲染「帮助」。
|
|
478
|
-
...(() => {
|
|
479
|
-
const links = getProductConfig().links ?? {};
|
|
480
|
-
const submenu = [];
|
|
481
|
-
const updateAction = appUpdates ? appUpdateMenuAction(appUpdates.state) : 'none';
|
|
482
|
-
if (updateAction !== 'none') {
|
|
483
|
-
submenu.push({
|
|
484
|
-
label: updateAction === 'install' ? '重启并安装更新' : '检查更新',
|
|
485
|
-
click: () => {
|
|
486
|
-
if (!appUpdates)
|
|
487
|
-
return;
|
|
488
|
-
if (appUpdateMenuAction(appUpdates.state) === 'install')
|
|
489
|
-
appUpdates.installNow();
|
|
490
|
-
else
|
|
491
|
-
void appUpdates.checkNow();
|
|
492
|
-
},
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
if (links.releasePage) {
|
|
496
|
-
submenu.push({
|
|
497
|
-
label: '打开发布页',
|
|
498
|
-
click: async () => {
|
|
499
|
-
await shell.openExternal(links.releasePage);
|
|
500
|
-
},
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
if (links.website) {
|
|
504
|
-
submenu.push({
|
|
505
|
-
label: '访问官网',
|
|
506
|
-
click: async () => {
|
|
507
|
-
await shell.openExternal(links.website);
|
|
508
|
-
},
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
return submenu.length > 0 ? [{ label: '帮助', submenu }] : [];
|
|
512
|
-
})(),
|
|
513
|
-
];
|
|
514
|
-
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
515
|
-
}
|
|
516
|
-
function currentReleaseSnapshot(state) {
|
|
517
|
-
return toAppReleaseSnapshot(app.getVersion(), state ?? appUpdates?.state ?? { phase: 'disabled' });
|
|
518
|
-
}
|
|
519
|
-
function broadcastRelease(state) {
|
|
520
|
-
if (!mainWindow || mainWindow.isDestroyed())
|
|
521
|
-
return;
|
|
522
|
-
mainWindow.webContents.send('app-update-state', currentReleaseSnapshot(state));
|
|
523
|
-
}
|
|
524
|
-
function setupIpcHandlers() {
|
|
525
|
-
ipcMain.handle('app:release-snapshot', () => currentReleaseSnapshot());
|
|
526
|
-
ipcMain.handle('app:release-check', async () => {
|
|
527
|
-
if (!appUpdates)
|
|
528
|
-
return currentReleaseSnapshot({ phase: 'disabled' });
|
|
529
|
-
return currentReleaseSnapshot(await appUpdates.checkNow());
|
|
530
|
-
});
|
|
531
|
-
ipcMain.handle('app:release-install', () => {
|
|
532
|
-
if (!appUpdates)
|
|
533
|
-
return currentReleaseSnapshot({ phase: 'disabled' });
|
|
534
|
-
return currentReleaseSnapshot(appUpdates.installNow());
|
|
535
|
-
});
|
|
536
|
-
ipcMain.on('retry-connection', async () => {
|
|
537
|
-
retryCount = 0;
|
|
538
|
-
if (retryTimer) {
|
|
539
|
-
clearTimeout(retryTimer);
|
|
540
|
-
retryTimer = null;
|
|
541
|
-
}
|
|
542
|
-
if (IS_DEV) {
|
|
543
|
-
const isUp = await checkDevServerConnection();
|
|
544
|
-
if (isUp)
|
|
545
|
-
await loadApp();
|
|
546
|
-
else
|
|
547
|
-
scheduleRetry();
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
await loadApp();
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
ipcMain.handle('check-network-status', async () => {
|
|
554
|
-
if (IS_DEV)
|
|
555
|
-
return { online: await checkDevServerConnection() };
|
|
556
|
-
return { online: true };
|
|
557
|
-
});
|
|
558
|
-
ipcMain.on('show-notification', (_event, payload) => {
|
|
559
|
-
const notification = new ElectronNotification({
|
|
560
|
-
title: payload.title || getBrand().displayName,
|
|
561
|
-
body: payload.body || '',
|
|
562
|
-
silent: false,
|
|
563
|
-
});
|
|
564
|
-
notification.on('click', () => {
|
|
565
|
-
if (!mainWindow)
|
|
566
|
-
return;
|
|
567
|
-
if (mainWindow.isMinimized())
|
|
568
|
-
mainWindow.restore();
|
|
569
|
-
mainWindow.focus();
|
|
570
|
-
if (payload.link) {
|
|
571
|
-
mainWindow.webContents.send('notification-clicked', { link: payload.link });
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
notification.show();
|
|
575
|
-
});
|
|
576
|
-
// W4-1: the renderer's approval modal answers here. The decision is
|
|
577
|
-
// validated and routed to the pending sidecar `approval.request` call;
|
|
578
|
-
// unknown requestIds (late/stale answers) are dropped.
|
|
579
|
-
ipcMain.handle('approval:decide', (_event, payload) => approvalBridge.decide(payload));
|
|
580
|
-
ipcMain.handle('approval:pending', () => approvalBridge.pending());
|
|
581
|
-
// W8: the renderer's ask-user card answers here; routed to the pending
|
|
582
|
-
// sidecar `ask_user.request` call (fail-open empty answers on mismatch).
|
|
583
|
-
ipcMain.handle('ask-user:answer', (_event, payload) => askUserBridge.answer(payload));
|
|
584
|
-
ipcMain.handle('ask-user:pending', () => askUserBridge.pending());
|
|
585
|
-
ipcMain.handle('local:update-safety-config', async (_event, config) => {
|
|
586
|
-
localExecutor.updateSafetyConfig(config);
|
|
587
|
-
return { success: true };
|
|
588
|
-
});
|
|
589
|
-
ipcMain.handle('local:select-directory', async (_event, args) => {
|
|
590
|
-
if (!mainWindow)
|
|
591
|
-
return { canceled: true, filePaths: [] };
|
|
592
|
-
const options = args ?? {};
|
|
593
|
-
const result = await dialog.showOpenDialog(mainWindow, {
|
|
594
|
-
title: options.title || '选择文件夹',
|
|
595
|
-
properties: ['openDirectory'],
|
|
596
|
-
});
|
|
597
|
-
return result;
|
|
598
|
-
});
|
|
599
|
-
// 会话附件:把渲染进程给出的源路径(Electron)或 base64 字节(BS)落盘。
|
|
600
|
-
ipcMain.handle('attachments:save', async (_event, input) => {
|
|
601
|
-
assertHostIpcAllowed('attachments:save');
|
|
602
|
-
const chatId = typeof input?.chatId === 'string' ? input.chatId : '';
|
|
603
|
-
const files = Array.isArray(input?.files)
|
|
604
|
-
? input.files.filter((f) => f && (typeof f.path === 'string' || typeof f.data === 'string'))
|
|
605
|
-
: [];
|
|
606
|
-
return saveAttachmentFiles(chatId, files);
|
|
607
|
-
});
|
|
608
|
-
ipcMain.handle('local:save-text-file', async (_event, args) => {
|
|
609
|
-
if (!mainWindow)
|
|
610
|
-
return { canceled: true };
|
|
611
|
-
const options = args ?? {};
|
|
612
|
-
const save = await dialog.showSaveDialog(mainWindow, {
|
|
613
|
-
title: options.title || '导出文件',
|
|
614
|
-
defaultPath: options.defaultPath || 'insights-export.json',
|
|
615
|
-
filters: [{ name: 'JSON', extensions: ['json'] }],
|
|
616
|
-
});
|
|
617
|
-
if (save.canceled || !save.filePath)
|
|
618
|
-
return { canceled: true };
|
|
619
|
-
await fsPromises.writeFile(save.filePath, options.content ?? '', 'utf8');
|
|
620
|
-
return { canceled: false, filePath: save.filePath };
|
|
621
|
-
});
|
|
622
|
-
// 分享对话截图:渲染层把 .chat-panel-container 的 getBoundingClientRect()
|
|
623
|
-
// (CSS px = DIP)传上来,capturePage 按该区域截图并写入系统剪贴板。
|
|
624
|
-
ipcMain.handle('local:capture-screenshot', async (event, args) => {
|
|
625
|
-
try {
|
|
626
|
-
const win = BrowserWindow.fromWebContents(event.sender);
|
|
627
|
-
if (!win)
|
|
628
|
-
return { success: false, error: 'window not found' };
|
|
629
|
-
const rect = args?.rect;
|
|
630
|
-
const image = rect
|
|
631
|
-
? await win.webContents.capturePage({
|
|
632
|
-
x: Math.max(0, Math.round(rect.x)),
|
|
633
|
-
y: Math.max(0, Math.round(rect.y)),
|
|
634
|
-
width: Math.max(1, Math.round(rect.width)),
|
|
635
|
-
height: Math.max(1, Math.round(rect.height)),
|
|
636
|
-
})
|
|
637
|
-
: await win.webContents.capturePage();
|
|
638
|
-
if (image.isEmpty())
|
|
639
|
-
return { success: false, error: 'capturePage 返回空图像' };
|
|
640
|
-
clipboard.writeImage(image);
|
|
641
|
-
const size = image.getSize();
|
|
642
|
-
return { success: true, width: size.width, height: size.height };
|
|
643
|
-
}
|
|
644
|
-
catch (err) {
|
|
645
|
-
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
ipcMain.handle('local:exec-shell', async (_event, request) => {
|
|
649
|
-
const viaTerminal = await maybeExecInTerminal(request);
|
|
650
|
-
if (viaTerminal)
|
|
651
|
-
return viaTerminal;
|
|
652
|
-
return localExecutor.executeShell(request);
|
|
653
|
-
});
|
|
654
|
-
ipcMain.handle('local:read-file', async (_event, request) => localExecutor.readLocalFile(request));
|
|
655
|
-
ipcMain.handle('local:write-file', async (_event, request) => localExecutor.writeLocalFile(request));
|
|
656
|
-
ipcMain.handle('local:open-path', async (_event, request) => {
|
|
657
|
-
assertHostIpcAllowed('local:open-path');
|
|
658
|
-
return localExecutor.openLocalTarget(request);
|
|
659
|
-
});
|
|
660
|
-
ipcMain.handle('local:list-scripts', async () => localScriptRegistry.list());
|
|
661
|
-
ipcMain.handle('local:add-script', async (_event, input) => localScriptRegistry.create(input));
|
|
662
|
-
ipcMain.handle('local:update-script', async (_event, payload) => localScriptRegistry.update(payload.id, payload.updates));
|
|
663
|
-
ipcMain.handle('local:delete-script', async (_event, id) => {
|
|
664
|
-
localScriptRegistry.delete(id);
|
|
665
|
-
return { success: true };
|
|
666
|
-
});
|
|
667
|
-
ipcMain.handle('local:run-script', async (_event, id) => {
|
|
668
|
-
const script = localScriptRegistry.getById(id);
|
|
669
|
-
if (!script)
|
|
670
|
-
return { success: false, error: `Script not found: ${id}` };
|
|
671
|
-
return await localExecutor.executeShell({
|
|
672
|
-
command: script.command,
|
|
673
|
-
cwd: script.cwd,
|
|
674
|
-
timeout: script.timeout,
|
|
675
|
-
});
|
|
676
|
-
});
|
|
677
|
-
// ─── 包 IPC(2.3):各包的通道贡献由装配产物给出,宿主循环注册;
|
|
678
|
-
// 通道命名空间(<packId>:*)由 registerPackIpc 校验。开独立路由窗口
|
|
679
|
-
// 是宿主窗口层能力,经 caps 注入给包。
|
|
680
|
-
for (const [packId, handle] of packHandles) {
|
|
681
|
-
if (handle.ipc) {
|
|
682
|
-
registerPackIpc(packId, handle.ipc({ openRouteWindow }));
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
ipcMain.handle('local-backend:request', async (_event, request) => {
|
|
686
|
-
try {
|
|
687
|
-
const response = await localBackendRouter.handle(request);
|
|
688
|
-
log.info(`[local-backend] ${request.method} ${request.path} -> ${response.status}`);
|
|
689
|
-
return {
|
|
690
|
-
ok: response.status < 400,
|
|
691
|
-
status: response.status,
|
|
692
|
-
data: response.data,
|
|
693
|
-
error: response.status >= 400 ? JSON.stringify(response.data) : undefined,
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
catch (error) {
|
|
697
|
-
log.error(`[local-backend] ${request.method} ${request.path} threw`, error);
|
|
698
|
-
return {
|
|
699
|
-
ok: false,
|
|
700
|
-
status: 500,
|
|
701
|
-
error: error instanceof Error ? error.message : String(error),
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
// 真正的流式 IPC:invoke 立刻返回一个 streamId,
|
|
706
|
-
// 主进程在 router 每收到一个 SSE chunk 时通过 webContents.send 把它推到渲染端。
|
|
707
|
-
// 渲染端 preload 把这些事件喂进 ReadableStream.controller。
|
|
708
|
-
//
|
|
709
|
-
// 取消机制:每个 stream 配一个 AbortController。以下任一情况触发 abort,
|
|
710
|
-
// router 的 agent 循环会在下一个 LLM turn / tool call 边界停下来:
|
|
711
|
-
// 1. renderer 主动调 cancelStream(streamId)(Stop 按钮 / 切换对话)
|
|
712
|
-
// 2. 发起流的 webContents 被销毁(窗口关闭 / 页面刷新)
|
|
713
|
-
// 3. 应用退出(before-quit 里统一 abort)
|
|
714
|
-
// 没有这一层时,agent 循环会在用户关闭对话后继续跑完全部 32 轮,
|
|
715
|
-
// 终端窗口被每条命令反复唤起——这正是"关了还在执行"的根因。
|
|
716
|
-
ipcMain.handle('local-backend:stream', async (event, request) => {
|
|
717
|
-
const streamId = randomUUID();
|
|
718
|
-
const channel = `local-backend:stream:${streamId}`;
|
|
719
|
-
const cancelChannel = `${channel}:cancel`;
|
|
720
|
-
const controller = new AbortController();
|
|
721
|
-
activeStreamControllers.set(streamId, controller);
|
|
722
|
-
const onCancel = () => {
|
|
723
|
-
log.info(`[local-backend stream] cancel requested`, { streamId });
|
|
724
|
-
controller.abort();
|
|
725
|
-
};
|
|
726
|
-
ipcMain.once(cancelChannel, onCancel);
|
|
727
|
-
const sender = event.sender;
|
|
728
|
-
const onSenderDestroyed = () => {
|
|
729
|
-
log.info(`[local-backend stream] sender destroyed, aborting`, { streamId });
|
|
730
|
-
controller.abort();
|
|
731
|
-
};
|
|
732
|
-
sender.once('destroyed', onSenderDestroyed);
|
|
733
|
-
const cleanup = () => {
|
|
734
|
-
activeStreamControllers.delete(streamId);
|
|
735
|
-
ipcMain.removeListener(cancelChannel, onCancel);
|
|
736
|
-
if (!sender.isDestroyed())
|
|
737
|
-
sender.removeListener('destroyed', onSenderDestroyed);
|
|
738
|
-
};
|
|
739
|
-
// 异步启动 router,避免阻塞 invoke 返回(前端拿到 streamId 才能开始监听)。
|
|
740
|
-
queueMicrotask(async () => {
|
|
741
|
-
const safeSend = (payload) => {
|
|
742
|
-
if (!sender.isDestroyed())
|
|
743
|
-
sender.send(channel, payload);
|
|
744
|
-
};
|
|
745
|
-
try {
|
|
746
|
-
const result = await localBackendRouter.handleStream(request, chunk => {
|
|
747
|
-
safeSend({ type: 'data', chunk });
|
|
748
|
-
}, { signal: controller.signal });
|
|
749
|
-
log.info(`[local-backend stream] ${request.method} ${request.path} -> ${result.status}`);
|
|
750
|
-
safeSend({ type: 'end', status: result.status });
|
|
751
|
-
}
|
|
752
|
-
catch (err) {
|
|
753
|
-
log.error(`[local-backend stream] ${request.method} ${request.path} threw`, err);
|
|
754
|
-
safeSend({
|
|
755
|
-
type: 'error',
|
|
756
|
-
error: err instanceof Error ? err.message : String(err),
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
finally {
|
|
760
|
-
cleanup();
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
return { ok: true, streamId };
|
|
764
|
-
});
|
|
765
|
-
// 轮中转向:把用户消息注入正在运行的 CoreLoop 回合(sidecar 侧 drain 进
|
|
766
|
-
// transcript)。仅 CoreLoop 路径支持;无活动回合时软失败 { ok: false },
|
|
767
|
-
// 渲染端保留草稿,等回合结束后按普通消息重发。
|
|
768
|
-
ipcMain.handle('local-backend:steer', async (_event, payload) => {
|
|
769
|
-
const chatId = typeof payload?.chatId === 'string' ? payload.chatId : '';
|
|
770
|
-
const content = typeof payload?.content === 'string' ? payload.content : '';
|
|
771
|
-
if (!chatId || !content.trim()) {
|
|
772
|
-
return { ok: false, reason: 'invalid_params' };
|
|
773
|
-
}
|
|
774
|
-
const streamId = getActiveCoreLoopStreamId(chatId);
|
|
775
|
-
const supervisor = getSidecarSupervisor();
|
|
776
|
-
if (!streamId || !supervisor) {
|
|
777
|
-
return { ok: false, reason: 'no_active_coreloop_turn' };
|
|
778
|
-
}
|
|
779
|
-
const ok = await supervisor.steerChat(streamId, content);
|
|
780
|
-
if (ok) {
|
|
781
|
-
// The framework's record logs the injected message (the loop drains it
|
|
782
|
-
// into the transcript), but the desktop store is what the chat renders
|
|
783
|
-
// on reopen — without this the user comes back to an answer whose
|
|
784
|
-
// question is missing. Written at steer time, so `created_at` places it
|
|
785
|
-
// between the turn's user message and the assistant reply persisted at
|
|
786
|
-
// turn end. Only on acceptance: a soft failure degrades into the
|
|
787
|
-
// follow-up queue, which persists through the normal send path.
|
|
788
|
-
await hostStore.addMessage(chatId, 'user', content);
|
|
789
|
-
}
|
|
790
|
-
return { ok, ...(ok ? {} : { reason: 'stream_not_active' }) };
|
|
791
|
-
});
|
|
792
|
-
// ---------------------------------------------------------------------------
|
|
793
|
-
// Terminal IPC: visible PTY embedded in the main window's /terminal route
|
|
794
|
-
// ---------------------------------------------------------------------------
|
|
795
|
-
ipcMain.handle('terminal:list', async () => {
|
|
796
|
-
assertHostIpcAllowed('terminal:list');
|
|
797
|
-
return terminalManager.list();
|
|
798
|
-
});
|
|
799
|
-
ipcMain.handle('terminal:spawn', async (_event, options = {}) => {
|
|
800
|
-
assertHostIpcAllowed('terminal:spawn');
|
|
801
|
-
return terminalManager.spawn(options);
|
|
802
|
-
});
|
|
803
|
-
ipcMain.handle('terminal:ensure', async (event, options = {}) => {
|
|
804
|
-
assertHostIpcAllowed('terminal:ensure');
|
|
805
|
-
const session = terminalManager.ensurePrimary(options);
|
|
806
|
-
// Replay any output buffered before the renderer was attached, so the
|
|
807
|
-
// window doesn't look empty if the agent kicked off a command before the
|
|
808
|
-
// user had the terminal open. Send to the requesting webContents directly,
|
|
809
|
-
// not via broadcast, to avoid double-writing to other listeners.
|
|
810
|
-
const replay = terminalManager.getReplayBuffer(session.id);
|
|
811
|
-
if (replay) {
|
|
812
|
-
try {
|
|
813
|
-
event.sender.send('terminal:data', { sessionId: session.id, chunk: replay });
|
|
814
|
-
}
|
|
815
|
-
catch {
|
|
816
|
-
// ignore — sender may have gone away
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
return session;
|
|
820
|
-
});
|
|
821
|
-
ipcMain.handle('terminal:write', async (_event, payload) => {
|
|
822
|
-
assertHostIpcAllowed('terminal:write');
|
|
823
|
-
return terminalManager.write(payload.id, payload.data);
|
|
824
|
-
});
|
|
825
|
-
ipcMain.handle('terminal:resize', async (_event, payload) => {
|
|
826
|
-
assertHostIpcAllowed('terminal:resize');
|
|
827
|
-
return terminalManager.resize(payload.id, payload.cols, payload.rows);
|
|
828
|
-
});
|
|
829
|
-
ipcMain.handle('terminal:kill', async (_event, id) => {
|
|
830
|
-
assertHostIpcAllowed('terminal:kill');
|
|
831
|
-
return terminalManager.kill(id);
|
|
832
|
-
});
|
|
833
|
-
ipcMain.handle('terminal:exec', async (_event, payload) => {
|
|
834
|
-
assertHostIpcAllowed('terminal:exec');
|
|
835
|
-
let id = payload.id;
|
|
836
|
-
if (!id) {
|
|
837
|
-
const session = terminalManager.ensurePrimary();
|
|
838
|
-
id = session.id;
|
|
839
|
-
}
|
|
840
|
-
try {
|
|
841
|
-
return await terminalManager.exec(id, payload.command, payload.timeoutMs);
|
|
842
|
-
}
|
|
843
|
-
catch (err) {
|
|
844
|
-
return {
|
|
845
|
-
success: false,
|
|
846
|
-
exitCode: -1,
|
|
847
|
-
stdout: '',
|
|
848
|
-
stderr: err instanceof Error ? err.message : String(err),
|
|
849
|
-
truncated: false,
|
|
850
|
-
durationMs: 0,
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
let appUpdates = null;
|
|
856
|
-
function startDesktopUpdates() {
|
|
857
|
-
let previous = 'disabled';
|
|
858
|
-
appUpdates = new AppUpdateController({
|
|
859
|
-
feedUrl: getProductConfig().updates?.feedUrl,
|
|
860
|
-
packaged: app.isPackaged,
|
|
861
|
-
currentVersion: app.getVersion(),
|
|
862
|
-
env: process.env,
|
|
863
|
-
updater: createElectronAppUpdater(),
|
|
864
|
-
onState: state => {
|
|
865
|
-
log.info('[app-update]', state.phase, state.version ?? '', state.message ?? '');
|
|
866
|
-
broadcastRelease(state);
|
|
867
|
-
if (shouldNotifyUpdateReady(previous, state) && state.version) {
|
|
868
|
-
const notification = new ElectronNotification({
|
|
869
|
-
title: getBrand().displayName,
|
|
870
|
-
body: `新版本 ${state.version} 已下载,点击重启并安装`,
|
|
871
|
-
});
|
|
872
|
-
notification.on('click', () => {
|
|
873
|
-
appUpdates?.installNow();
|
|
874
|
-
});
|
|
875
|
-
notification.show();
|
|
876
|
-
}
|
|
877
|
-
const actionChanged = appUpdateMenuAction({ phase: previous }) !== appUpdateMenuAction(state);
|
|
878
|
-
previous = state.phase;
|
|
879
|
-
if (actionChanged)
|
|
880
|
-
createMenu();
|
|
881
|
-
},
|
|
882
|
-
});
|
|
883
|
-
void appUpdates.start();
|
|
884
|
-
}
|
|
885
|
-
app.whenReady().then(async () => {
|
|
886
|
-
startDesktopUpdates();
|
|
887
|
-
applyStrictCsp();
|
|
888
|
-
runtime = await createHostRuntime({
|
|
889
|
-
broadcast: (channel, payload) => broadcastTerminalEvent(channel, payload),
|
|
890
|
-
broadcastMain: (eventName, payload) => {
|
|
891
|
-
if (!mainWindow || mainWindow.isDestroyed())
|
|
892
|
-
return;
|
|
893
|
-
mainWindow.webContents.send(eventName, payload);
|
|
894
|
-
},
|
|
895
|
-
hasWindow: () => BrowserWindow.getAllWindows().some((win) => !win.isDestroyed()),
|
|
896
|
-
onLog: (line) => log.info('[sidecar]', line),
|
|
897
|
-
taskSweepReason: '应用重启,任务流已中断',
|
|
898
|
-
});
|
|
899
|
-
({
|
|
900
|
-
store: hostStore,
|
|
901
|
-
localExecutor,
|
|
902
|
-
localScriptRegistry,
|
|
903
|
-
terminalManager,
|
|
904
|
-
packHandles,
|
|
905
|
-
localBackendRouter,
|
|
906
|
-
approvalBridge,
|
|
907
|
-
askUserBridge,
|
|
908
|
-
maybeExecInTerminal,
|
|
909
|
-
} = runtime);
|
|
910
|
-
terminalManager.on('data', (sessionId, chunk) => {
|
|
911
|
-
broadcastTerminalEvent('terminal:data', { sessionId, chunk });
|
|
912
|
-
});
|
|
913
|
-
terminalManager.on('exit', (sessionId, code, signal) => {
|
|
914
|
-
broadcastTerminalEvent('terminal:exit', { sessionId, code, signal });
|
|
915
|
-
});
|
|
916
|
-
terminalManager.on('spawned', session => {
|
|
917
|
-
broadcastTerminalEvent('terminal:spawned', session);
|
|
918
|
-
});
|
|
919
|
-
setupIpcHandlers();
|
|
920
|
-
// 任务清扫 / mock 广播 / sidecar 启动 / MCP 刷新 / 终端预热都在
|
|
921
|
-
// runtime.start() 里(与 BS 同一份)。
|
|
922
|
-
await runtime.start();
|
|
923
|
-
createWindow();
|
|
924
|
-
setSecondInstanceHandler(() => {
|
|
925
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
926
|
-
if (mainWindow.isMinimized())
|
|
927
|
-
mainWindow.restore();
|
|
928
|
-
mainWindow.show();
|
|
929
|
-
mainWindow.focus();
|
|
930
|
-
}
|
|
931
|
-
});
|
|
932
|
-
app.on('activate', () => {
|
|
933
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
934
|
-
mainWindow.show();
|
|
935
|
-
mainWindow.focus();
|
|
936
|
-
}
|
|
937
|
-
else {
|
|
938
|
-
createWindow();
|
|
939
|
-
}
|
|
940
|
-
});
|
|
941
|
-
});
|
|
942
|
-
app.on('window-all-closed', () => {
|
|
943
|
-
// macOS 惯例:所有窗口关闭后应用驻留 Dock。其他平台直接退出,
|
|
944
|
-
// 避免无窗口的后台残留进程。
|
|
945
|
-
if (process.platform !== 'darwin') {
|
|
946
|
-
app.quit();
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
app.on('before-quit', () => {
|
|
950
|
-
isQuitting = true;
|
|
951
|
-
if (retryTimer) {
|
|
952
|
-
clearTimeout(retryTimer);
|
|
953
|
-
retryTimer = null;
|
|
954
|
-
}
|
|
955
|
-
// 中止所有仍在跑的 agent 流,防止退出后 LLM 循环 / 工具调用继续执行。
|
|
956
|
-
for (const controller of activeStreamControllers.values()) {
|
|
957
|
-
controller.abort();
|
|
958
|
-
}
|
|
959
|
-
activeStreamControllers.clear();
|
|
960
|
-
// 终端 / 场景包进程 / sidecar 的关停统一走 runtime。
|
|
961
|
-
void runtime.shutdown();
|
|
962
|
-
});
|
|
963
|
-
nativeTheme.on('updated', () => {
|
|
964
|
-
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
965
|
-
mainWindow.webContents.send('theme-changed', nativeTheme.shouldUseDarkColors);
|
|
966
|
-
}
|
|
967
|
-
});
|