@steerable/agent-shell 0.6.20 → 0.6.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/auth/index.d.ts +38 -0
- package/dist/auth/index.js +20 -0
- package/dist/host/pack-assembly.d.ts +7 -7
- package/dist/host/runtime.d.ts +6 -2
- package/dist/host/runtime.js +54 -15
- package/dist/host/visible-terminal-exec.d.ts +0 -2
- package/dist/host/visible-terminal-exec.js +4 -8
- package/dist/insights/flush.d.ts +5 -4
- package/dist/insights/flush.js +14 -15
- package/dist/insights/record.d.ts +6 -5
- package/dist/insights/record.js +9 -10
- package/dist/llm/index.d.ts +6 -2
- package/dist/llm/index.js +19 -9
- package/dist/local-backend/ai-suggestions.d.ts +32 -0
- package/dist/local-backend/ai-suggestions.js +160 -0
- package/dist/local-backend/coreloop-stream.js +1 -1
- package/dist/local-backend/mentioned-paths.d.ts +28 -0
- package/dist/local-backend/mentioned-paths.js +67 -0
- package/dist/local-backend/prompt-builder.d.ts +10 -4
- package/dist/local-backend/prompt-builder.js +13 -12
- package/dist/local-backend/router.d.ts +26 -4
- package/dist/local-backend/router.js +243 -96
- package/dist/local-backend/skills/00-identity/SKILL.md +1 -1
- package/dist/local-backend/task-service.d.ts +14 -13
- package/dist/local-backend/task-service.js +32 -29
- package/dist/local-backend/turn-files.d.ts +57 -0
- package/dist/local-backend/turn-files.js +285 -0
- package/dist/local-backend/worktree-service.d.ts +1 -1
- package/dist/local-backend/worktree-service.js +6 -6
- package/dist/main.js +45 -26
- package/dist/preload.cjs +30 -10
- package/dist/preload.js +33 -10
- package/dist/server/http-server.d.ts +13 -1
- package/dist/server/http-server.js +45 -8
- package/dist/server/index.js +4 -90
- package/dist/server/sse-bus.d.ts +1 -1
- package/dist/server/start.d.ts +26 -0
- package/dist/server/start.js +111 -0
- package/dist/sidecar/boot.d.ts +3 -1
- package/dist/sidecar/boot.js +6 -7
- package/dist/sidecar/reverse-approval.d.ts +2 -0
- package/dist/sidecar/reverse-approval.js +2 -1
- package/dist/sidecar/reverse-ask-user.d.ts +2 -0
- package/dist/sidecar/reverse-ask-user.js +2 -1
- package/dist/sidecar/reverse-spawn.js +12 -4
- package/dist/sidecar/reverse-tools.d.ts +1 -1
- package/dist/sidecar/reverse-tools.js +1 -1
- package/dist/sidecar/storage-path.js +2 -3
- package/dist/storage/driver.d.ts +57 -0
- package/dist/storage/driver.js +104 -0
- package/dist/storage/index.d.ts +80 -67
- package/dist/storage/index.js +528 -296
- package/dist/storage/pack-migrations.d.ts +2 -4
- package/dist/storage/pack-migrations.js +2 -4
- package/dist/storage/scoped-store.d.ts +125 -0
- package/dist/storage/scoped-store.js +1 -0
- package/dist/storage/sqlite-driver.d.ts +16 -0
- package/dist/storage/sqlite-driver.js +135 -0
- package/dist/storage/write-lease-error.d.ts +11 -0
- package/dist/storage/write-lease-error.js +18 -0
- package/package.json +14 -5
- package/dist/storage/local-store-singleton.d.ts +0 -28
- package/dist/storage/local-store-singleton.js +0 -38
|
@@ -49,8 +49,8 @@ export class WorktreeService {
|
|
|
49
49
|
managedRoot(projectRoot) {
|
|
50
50
|
return path.join(projectRoot, '.steerable', 'worktrees');
|
|
51
51
|
}
|
|
52
|
-
requireProject(chatId) {
|
|
53
|
-
const project = this.deps.resolveProject(chatId);
|
|
52
|
+
async requireProject(chatId) {
|
|
53
|
+
const project = await this.deps.resolveProject(chatId);
|
|
54
54
|
if (!project) {
|
|
55
55
|
throw new Error('worktree 需要对话绑定项目(git 仓库)——请先把本对话关联到一个项目。');
|
|
56
56
|
}
|
|
@@ -91,7 +91,7 @@ export class WorktreeService {
|
|
|
91
91
|
* 同名已存在时直接复用(幂等——模型重试不该炸掉已建好的工作区)。
|
|
92
92
|
*/
|
|
93
93
|
async createWorktree(chatId, rawName) {
|
|
94
|
-
const project = this.requireProject(chatId);
|
|
94
|
+
const project = await this.requireProject(chatId);
|
|
95
95
|
await this.requireGitRepo(project.folderPath);
|
|
96
96
|
const name = slugifyWorktreeName(rawName ?? '');
|
|
97
97
|
const suffix = name === 'wt' ? `-${Date.now().toString(36)}` : '';
|
|
@@ -117,7 +117,7 @@ export class WorktreeService {
|
|
|
117
117
|
}
|
|
118
118
|
/** 列出本项目的托管 worktree(只认 .steerable/worktrees 下的)。 */
|
|
119
119
|
async listWorktrees(chatId) {
|
|
120
|
-
const project = this.requireProject(chatId);
|
|
120
|
+
const project = await this.requireProject(chatId);
|
|
121
121
|
await this.requireGitRepo(project.folderPath);
|
|
122
122
|
const { stdout } = await this.git(project.folderPath, [
|
|
123
123
|
'worktree',
|
|
@@ -168,7 +168,7 @@ export class WorktreeService {
|
|
|
168
168
|
* 但默认删掉保持分支列表干净——已合并的分支删除是安全的)。
|
|
169
169
|
*/
|
|
170
170
|
async removeWorktree(chatId, name, options = {}) {
|
|
171
|
-
const project = this.requireProject(chatId);
|
|
171
|
+
const project = await this.requireProject(chatId);
|
|
172
172
|
const worktrees = await this.listWorktrees(chatId);
|
|
173
173
|
const target = worktrees.find((w) => w.name === name || w.path === name);
|
|
174
174
|
if (!target) {
|
|
@@ -203,7 +203,7 @@ export class WorktreeService {
|
|
|
203
203
|
* 可稍后重试或手工处理。
|
|
204
204
|
*/
|
|
205
205
|
async mergeWorktree(chatId, name, commitMessage) {
|
|
206
|
-
const project = this.requireProject(chatId);
|
|
206
|
+
const project = await this.requireProject(chatId);
|
|
207
207
|
const worktrees = await this.listWorktrees(chatId);
|
|
208
208
|
const target = worktrees.find((w) => w.name === name || w.path === name);
|
|
209
209
|
if (!target) {
|
package/dist/main.js
CHANGED
|
@@ -39,7 +39,6 @@ const __dirname = path.dirname(__filename);
|
|
|
39
39
|
app.setPath('userData', getUserDataDir());
|
|
40
40
|
import { getActiveCoreLoopStreamId } from './local-backend/coreloop-stream.js';
|
|
41
41
|
import { getSidecarSupervisor } from './llm/index.js';
|
|
42
|
-
import { localStore } from './storage/index.js';
|
|
43
42
|
import { createHostRuntime } from './host/runtime.js';
|
|
44
43
|
import { registerPackIpc } from './host/ipc.js';
|
|
45
44
|
const store = createJsonStore({ name: 'config' });
|
|
@@ -62,20 +61,16 @@ const RETRY_DELAYS = [2000, 5000, 10000, 30000];
|
|
|
62
61
|
// 全部宿主服务由共享 HostRuntime 装配(与 BS server 同一份,见
|
|
63
62
|
// src/host/runtime.ts 模块头——历史上两处逐行重复,BS 曾漏接 TaskService
|
|
64
63
|
// 导致模型退化成"假后台")。CS 的差异只剩广播(IPC)与生命周期。
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
onLog: (line) => log.info('[sidecar]', line),
|
|
76
|
-
taskSweepReason: '应用重启,任务流已中断',
|
|
77
|
-
});
|
|
78
|
-
const { localExecutor, localScriptRegistry, terminalManager, packHandles, localBackendRouter, approvalBridge, askUserBridge, maybeExecInTerminal, } = runtime;
|
|
64
|
+
let runtime;
|
|
65
|
+
let hostStore;
|
|
66
|
+
let localExecutor;
|
|
67
|
+
let localScriptRegistry;
|
|
68
|
+
let terminalManager;
|
|
69
|
+
let packHandles;
|
|
70
|
+
let localBackendRouter;
|
|
71
|
+
let approvalBridge;
|
|
72
|
+
let askUserBridge;
|
|
73
|
+
let maybeExecInTerminal;
|
|
79
74
|
let routeWindow = null;
|
|
80
75
|
// 进行中的 agent 流(streamId → AbortController)。cancelStream IPC、
|
|
81
76
|
// 窗口销毁、应用退出时统一 abort,保证不会有"关了还在跑"的幽灵循环。
|
|
@@ -90,15 +85,6 @@ function broadcastTerminalEvent(channel, payload) {
|
|
|
90
85
|
win.webContents.send(channel, payload);
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
|
-
terminalManager.on('data', (sessionId, chunk) => {
|
|
94
|
-
broadcastTerminalEvent('terminal:data', { sessionId, chunk });
|
|
95
|
-
});
|
|
96
|
-
terminalManager.on('exit', (sessionId, code, signal) => {
|
|
97
|
-
broadcastTerminalEvent('terminal:exit', { sessionId, code, signal });
|
|
98
|
-
});
|
|
99
|
-
terminalManager.on('spawned', session => {
|
|
100
|
-
broadcastTerminalEvent('terminal:spawned', session);
|
|
101
|
-
});
|
|
102
88
|
function getStartPath() {
|
|
103
89
|
if (!START_PATH)
|
|
104
90
|
return '/agent';
|
|
@@ -544,9 +530,11 @@ function setupIpcHandlers() {
|
|
|
544
530
|
// validated and routed to the pending sidecar `approval.request` call;
|
|
545
531
|
// unknown requestIds (late/stale answers) are dropped.
|
|
546
532
|
ipcMain.handle('approval:decide', (_event, payload) => approvalBridge.decide(payload));
|
|
533
|
+
ipcMain.handle('approval:pending', () => approvalBridge.pending());
|
|
547
534
|
// W8: the renderer's ask-user card answers here; routed to the pending
|
|
548
535
|
// sidecar `ask_user.request` call (fail-open empty answers on mismatch).
|
|
549
536
|
ipcMain.handle('ask-user:answer', (_event, payload) => askUserBridge.answer(payload));
|
|
537
|
+
ipcMain.handle('ask-user:pending', () => askUserBridge.pending());
|
|
550
538
|
ipcMain.handle('local:update-safety-config', async (_event, config) => {
|
|
551
539
|
localExecutor.updateSafetyConfig(config);
|
|
552
540
|
return { success: true };
|
|
@@ -746,7 +734,7 @@ function setupIpcHandlers() {
|
|
|
746
734
|
// between the turn's user message and the assistant reply persisted at
|
|
747
735
|
// turn end. Only on acceptance: a soft failure degrades into the
|
|
748
736
|
// follow-up queue, which persists through the normal send path.
|
|
749
|
-
|
|
737
|
+
await hostStore.addMessage(chatId, 'user', content);
|
|
750
738
|
}
|
|
751
739
|
return { ok, ...(ok ? {} : { reason: 'stream_not_active' }) };
|
|
752
740
|
});
|
|
@@ -804,10 +792,41 @@ function setupIpcHandlers() {
|
|
|
804
792
|
}
|
|
805
793
|
app.whenReady().then(async () => {
|
|
806
794
|
applyStrictCsp();
|
|
795
|
+
runtime = await createHostRuntime({
|
|
796
|
+
broadcast: (channel, payload) => broadcastTerminalEvent(channel, payload),
|
|
797
|
+
broadcastMain: (eventName, payload) => {
|
|
798
|
+
if (!mainWindow || mainWindow.isDestroyed())
|
|
799
|
+
return;
|
|
800
|
+
mainWindow.webContents.send(eventName, payload);
|
|
801
|
+
},
|
|
802
|
+
hasWindow: () => BrowserWindow.getAllWindows().some((win) => !win.isDestroyed()),
|
|
803
|
+
onLog: (line) => log.info('[sidecar]', line),
|
|
804
|
+
taskSweepReason: '应用重启,任务流已中断',
|
|
805
|
+
});
|
|
806
|
+
({
|
|
807
|
+
store: hostStore,
|
|
808
|
+
localExecutor,
|
|
809
|
+
localScriptRegistry,
|
|
810
|
+
terminalManager,
|
|
811
|
+
packHandles,
|
|
812
|
+
localBackendRouter,
|
|
813
|
+
approvalBridge,
|
|
814
|
+
askUserBridge,
|
|
815
|
+
maybeExecInTerminal,
|
|
816
|
+
} = runtime);
|
|
817
|
+
terminalManager.on('data', (sessionId, chunk) => {
|
|
818
|
+
broadcastTerminalEvent('terminal:data', { sessionId, chunk });
|
|
819
|
+
});
|
|
820
|
+
terminalManager.on('exit', (sessionId, code, signal) => {
|
|
821
|
+
broadcastTerminalEvent('terminal:exit', { sessionId, code, signal });
|
|
822
|
+
});
|
|
823
|
+
terminalManager.on('spawned', session => {
|
|
824
|
+
broadcastTerminalEvent('terminal:spawned', session);
|
|
825
|
+
});
|
|
807
826
|
setupIpcHandlers();
|
|
808
827
|
// 任务清扫 / mock 广播 / sidecar 启动 / MCP 刷新 / 终端预热都在
|
|
809
828
|
// runtime.start() 里(与 BS 同一份)。
|
|
810
|
-
runtime.start();
|
|
829
|
+
await runtime.start();
|
|
811
830
|
createWindow();
|
|
812
831
|
setSecondInstanceHandler(() => {
|
|
813
832
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
package/dist/preload.cjs
CHANGED
|
@@ -87,6 +87,25 @@ function buildPreloadApi(packContributions = {}) {
|
|
|
87
87
|
import_electron.ipcRenderer.removeListener("chat-title-updated", handler);
|
|
88
88
|
};
|
|
89
89
|
},
|
|
90
|
+
/**
|
|
91
|
+
* 回合追问建议就绪。Backend 在助手回复完成后 fire-and-forget 生成 3 条
|
|
92
|
+
* 下一轮用户输入;启发式兜底会立刻推一次,LLM 成功后再替换。返回 unsubscribe。
|
|
93
|
+
*/
|
|
94
|
+
onSuggestedReplies: (callback) => {
|
|
95
|
+
const handler = (_event, payload) => {
|
|
96
|
+
if (typeof payload?.chatId === "string" && typeof payload?.messageId === "string" && Array.isArray(payload.suggestions) && payload.suggestions.every((item) => typeof item === "string")) {
|
|
97
|
+
callback({
|
|
98
|
+
chatId: payload.chatId,
|
|
99
|
+
messageId: payload.messageId,
|
|
100
|
+
suggestions: payload.suggestions
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
import_electron.ipcRenderer.on("suggested-replies", handler);
|
|
105
|
+
return () => {
|
|
106
|
+
import_electron.ipcRenderer.removeListener("suggested-replies", handler);
|
|
107
|
+
};
|
|
108
|
+
},
|
|
90
109
|
/**
|
|
91
110
|
* 会话补建通知:Backend 在向本地不存在的 chatId 首次发送时按 URL 里的 id
|
|
92
111
|
* 现场补建会话,并广播 `chat-created`({chatId, agentId})。返回 unsubscribe
|
|
@@ -154,9 +173,9 @@ function buildPreloadApi(packContributions = {}) {
|
|
|
154
173
|
},
|
|
155
174
|
/**
|
|
156
175
|
* W4-1 审批代数:sidecar 的 ApprovalExecutor 经反向通道请示,主进程把
|
|
157
|
-
* 请求广播到 renderer(approval:request
|
|
158
|
-
* approval:decide
|
|
159
|
-
*
|
|
176
|
+
* 请求广播到 renderer(approval:request),输入区审批菜单应答后走
|
|
177
|
+
* approval:decide 回到主进程。pending 供刷新后的 renderer 恢复仍在等待
|
|
178
|
+
* 的请求。
|
|
160
179
|
*/
|
|
161
180
|
approval: {
|
|
162
181
|
onRequest: (callback) => {
|
|
@@ -173,13 +192,16 @@ function buildPreloadApi(packContributions = {}) {
|
|
|
173
192
|
},
|
|
174
193
|
decide: async (decision) => {
|
|
175
194
|
await import_electron.ipcRenderer.invoke("approval:decide", decision);
|
|
195
|
+
},
|
|
196
|
+
pending: async () => {
|
|
197
|
+
return await import_electron.ipcRenderer.invoke("approval:pending");
|
|
176
198
|
}
|
|
177
199
|
},
|
|
178
200
|
/**
|
|
179
201
|
* W8 结构化提问:sidecar 的 ask_user 工具经反向通道请示,主进程把
|
|
180
202
|
* 请求广播到 renderer(ask-user:request),问题卡片应答后走
|
|
181
|
-
* ask-user:answer
|
|
182
|
-
*
|
|
203
|
+
* ask-user:answer 回到主进程。pending 供刷新后的 renderer 恢复仍在等待
|
|
204
|
+
* 的请求。
|
|
183
205
|
*/
|
|
184
206
|
askUser: {
|
|
185
207
|
onRequest: (callback) => {
|
|
@@ -196,6 +218,9 @@ function buildPreloadApi(packContributions = {}) {
|
|
|
196
218
|
},
|
|
197
219
|
answer: async (reply) => {
|
|
198
220
|
await import_electron.ipcRenderer.invoke("ask-user:answer", reply);
|
|
221
|
+
},
|
|
222
|
+
pending: async () => {
|
|
223
|
+
return await import_electron.ipcRenderer.invoke("ask-user:pending");
|
|
199
224
|
}
|
|
200
225
|
},
|
|
201
226
|
local: {
|
|
@@ -380,11 +405,6 @@ function buildPreloadApi(packContributions = {}) {
|
|
|
380
405
|
const fn = (_event, session) => callback(session);
|
|
381
406
|
import_electron.ipcRenderer.on("terminal:spawned", fn);
|
|
382
407
|
return () => import_electron.ipcRenderer.removeListener("terminal:spawned", fn);
|
|
383
|
-
},
|
|
384
|
-
onReveal: (callback) => {
|
|
385
|
-
const fn = (_event, payload) => callback(payload);
|
|
386
|
-
import_electron.ipcRenderer.on("terminal:reveal", fn);
|
|
387
|
-
return () => import_electron.ipcRenderer.removeListener("terminal:reveal", fn);
|
|
388
408
|
}
|
|
389
409
|
}
|
|
390
410
|
};
|
package/dist/preload.js
CHANGED
|
@@ -103,6 +103,28 @@ export function buildPreloadApi(packContributions = {}) {
|
|
|
103
103
|
ipcRenderer.removeListener('chat-title-updated', handler);
|
|
104
104
|
};
|
|
105
105
|
},
|
|
106
|
+
/**
|
|
107
|
+
* 回合追问建议就绪。Backend 在助手回复完成后 fire-and-forget 生成 3 条
|
|
108
|
+
* 下一轮用户输入;启发式兜底会立刻推一次,LLM 成功后再替换。返回 unsubscribe。
|
|
109
|
+
*/
|
|
110
|
+
onSuggestedReplies: (callback) => {
|
|
111
|
+
const handler = (_event, payload) => {
|
|
112
|
+
if (typeof payload?.chatId === 'string' &&
|
|
113
|
+
typeof payload?.messageId === 'string' &&
|
|
114
|
+
Array.isArray(payload.suggestions) &&
|
|
115
|
+
payload.suggestions.every((item) => typeof item === 'string')) {
|
|
116
|
+
callback({
|
|
117
|
+
chatId: payload.chatId,
|
|
118
|
+
messageId: payload.messageId,
|
|
119
|
+
suggestions: payload.suggestions,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
ipcRenderer.on('suggested-replies', handler);
|
|
124
|
+
return () => {
|
|
125
|
+
ipcRenderer.removeListener('suggested-replies', handler);
|
|
126
|
+
};
|
|
127
|
+
},
|
|
106
128
|
/**
|
|
107
129
|
* 会话补建通知:Backend 在向本地不存在的 chatId 首次发送时按 URL 里的 id
|
|
108
130
|
* 现场补建会话,并广播 `chat-created`({chatId, agentId})。返回 unsubscribe
|
|
@@ -171,9 +193,9 @@ export function buildPreloadApi(packContributions = {}) {
|
|
|
171
193
|
},
|
|
172
194
|
/**
|
|
173
195
|
* W4-1 审批代数:sidecar 的 ApprovalExecutor 经反向通道请示,主进程把
|
|
174
|
-
* 请求广播到 renderer(approval:request
|
|
175
|
-
* approval:decide
|
|
176
|
-
*
|
|
196
|
+
* 请求广播到 renderer(approval:request),输入区审批菜单应答后走
|
|
197
|
+
* approval:decide 回到主进程。pending 供刷新后的 renderer 恢复仍在等待
|
|
198
|
+
* 的请求。
|
|
177
199
|
*/
|
|
178
200
|
approval: {
|
|
179
201
|
onRequest: (callback) => {
|
|
@@ -191,12 +213,15 @@ export function buildPreloadApi(packContributions = {}) {
|
|
|
191
213
|
decide: async (decision) => {
|
|
192
214
|
await ipcRenderer.invoke('approval:decide', decision);
|
|
193
215
|
},
|
|
216
|
+
pending: async () => {
|
|
217
|
+
return await ipcRenderer.invoke('approval:pending');
|
|
218
|
+
},
|
|
194
219
|
},
|
|
195
220
|
/**
|
|
196
221
|
* W8 结构化提问:sidecar 的 ask_user 工具经反向通道请示,主进程把
|
|
197
222
|
* 请求广播到 renderer(ask-user:request),问题卡片应答后走
|
|
198
|
-
* ask-user:answer
|
|
199
|
-
*
|
|
223
|
+
* ask-user:answer 回到主进程。pending 供刷新后的 renderer 恢复仍在等待
|
|
224
|
+
* 的请求。
|
|
200
225
|
*/
|
|
201
226
|
askUser: {
|
|
202
227
|
onRequest: (callback) => {
|
|
@@ -214,6 +239,9 @@ export function buildPreloadApi(packContributions = {}) {
|
|
|
214
239
|
answer: async (reply) => {
|
|
215
240
|
await ipcRenderer.invoke('ask-user:answer', reply);
|
|
216
241
|
},
|
|
242
|
+
pending: async () => {
|
|
243
|
+
return await ipcRenderer.invoke('ask-user:pending');
|
|
244
|
+
},
|
|
217
245
|
},
|
|
218
246
|
local: {
|
|
219
247
|
selectDirectory: async (options) => {
|
|
@@ -398,11 +426,6 @@ export function buildPreloadApi(packContributions = {}) {
|
|
|
398
426
|
ipcRenderer.on('terminal:spawned', fn);
|
|
399
427
|
return () => ipcRenderer.removeListener('terminal:spawned', fn);
|
|
400
428
|
},
|
|
401
|
-
onReveal: (callback) => {
|
|
402
|
-
const fn = (_event, payload) => callback(payload);
|
|
403
|
-
ipcRenderer.on('terminal:reveal', fn);
|
|
404
|
-
return () => ipcRenderer.removeListener('terminal:reveal', fn);
|
|
405
|
-
},
|
|
406
429
|
},
|
|
407
430
|
};
|
|
408
431
|
contextBridge.exposeInMainWorld('electron', { ...electronAPI, ...packContributions });
|
|
@@ -23,15 +23,21 @@
|
|
|
23
23
|
* 不要绑到非回环地址,那会把 shell / 文件读写 / 场景包工具控制暴露给整个
|
|
24
24
|
* 网络(token 只能挡没有 token 的人)。
|
|
25
25
|
*/
|
|
26
|
-
import { type Server } from 'node:http';
|
|
26
|
+
import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
27
27
|
import type { LocalBackendRouter } from '../local-backend/router.js';
|
|
28
|
+
import type { ScopedStore } from '../storage/scoped-store.js';
|
|
28
29
|
import type { LocalExecutor, LocalExecRequest, LocalExecResult } from '../local-executor.js';
|
|
29
30
|
import type { LocalScriptRegistry } from '../local-script-registry.js';
|
|
30
31
|
import type { TerminalManager } from '../terminal-manager.js';
|
|
31
32
|
import type { createApprovalBridge } from '../sidecar/reverse-approval.js';
|
|
32
33
|
import type { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
|
|
33
34
|
import type { SseBus } from './sse-bus.js';
|
|
35
|
+
/** Runs before built-in routing; `handled` short-circuits the request. */
|
|
36
|
+
export interface BsMiddleware {
|
|
37
|
+
(req: IncomingMessage, res: ServerResponse): Promise<'handled' | 'pass'>;
|
|
38
|
+
}
|
|
34
39
|
export interface BsServerDeps {
|
|
40
|
+
store: ScopedStore;
|
|
35
41
|
localBackendRouter: LocalBackendRouter;
|
|
36
42
|
localExecutor: LocalExecutor;
|
|
37
43
|
localScriptRegistry: LocalScriptRegistry;
|
|
@@ -44,5 +50,11 @@ export interface BsServerDeps {
|
|
|
44
50
|
webDistDir: string;
|
|
45
51
|
/** Bearer token;缺省每次启动随机生成。测试注入固定值。 */
|
|
46
52
|
authToken?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Trusted product middleware, invoked in order after Host validation and
|
|
55
|
+
* before authentication. This position lets a product own public sign-in
|
|
56
|
+
* and identity-provider callback routes without weakening built-in routes.
|
|
57
|
+
*/
|
|
58
|
+
middleware?: readonly BsMiddleware[];
|
|
47
59
|
}
|
|
48
60
|
export declare function createBsServer(deps: BsServerDeps): Server;
|
|
@@ -31,10 +31,10 @@ import path from 'node:path';
|
|
|
31
31
|
import log from 'electron-log';
|
|
32
32
|
import { getActiveCoreLoopStreamId } from '../local-backend/coreloop-stream.js';
|
|
33
33
|
import { getSidecarSupervisor } from '../llm/index.js';
|
|
34
|
-
import { localStore } from '../storage/index.js';
|
|
35
34
|
import { matchPackHttpRoute } from '../host/http-routes.js';
|
|
36
35
|
import { getBrand } from '../brand.js';
|
|
37
36
|
import { saveAttachmentFiles } from '../attachments.js';
|
|
37
|
+
import { getAuthProvider } from '../auth/index.js';
|
|
38
38
|
/** 与 router.handleStream 内部的路由正则保持一致——只有这些路径是流式。 */
|
|
39
39
|
const STREAM_PATH_PATTERNS = [
|
|
40
40
|
/^\/api\/v2\/chats\/[^/]+\/(send|run|agent)$/,
|
|
@@ -91,6 +91,7 @@ function asRecord(body) {
|
|
|
91
91
|
}
|
|
92
92
|
export function createBsServer(deps) {
|
|
93
93
|
const { localBackendRouter, localExecutor, localScriptRegistry, terminalManager, approvalBridge, askUserBridge, maybeExecInTerminal, bus, webDistDir, } = deps;
|
|
94
|
+
const middleware = deps.middleware ?? [];
|
|
94
95
|
const authToken = deps.authToken ?? randomBytes(24).toString('base64url');
|
|
95
96
|
/** `/api/v2/*` 与 `/host/*` 的 Bearer 门;EventSource 不能设头,收 query token。 */
|
|
96
97
|
function isAuthorized(req, url) {
|
|
@@ -98,13 +99,18 @@ export function createBsServer(deps) {
|
|
|
98
99
|
return true;
|
|
99
100
|
return url.searchParams.get('token') === authToken;
|
|
100
101
|
}
|
|
101
|
-
async function handleApi(req, res, pathname) {
|
|
102
|
+
async function handleApi(req, res, pathname, principal) {
|
|
102
103
|
const isStream = req.method === 'POST' && STREAM_PATH_PATTERNS.some((p) => p.test(pathname));
|
|
103
104
|
const body = req.method === 'POST' || req.method === 'PATCH' || req.method === 'PUT'
|
|
104
105
|
? await readJsonBody(req)
|
|
105
106
|
: undefined;
|
|
106
107
|
const search = new URL(req.url ?? '/', 'http://bs.local').search;
|
|
107
|
-
const request = {
|
|
108
|
+
const request = {
|
|
109
|
+
method: req.method ?? 'GET',
|
|
110
|
+
path: `${pathname}${search}`,
|
|
111
|
+
body,
|
|
112
|
+
...(principal ? { principal } : {}),
|
|
113
|
+
};
|
|
108
114
|
if (isStream) {
|
|
109
115
|
const controller = new AbortController();
|
|
110
116
|
// 浏览器关页/取消 fetch → 中止 agent 循环(对齐 Electron 的 cancelStream)。
|
|
@@ -221,7 +227,7 @@ export function createBsServer(deps) {
|
|
|
221
227
|
const ok = await supervisor.steerChat(streamId, content);
|
|
222
228
|
// 与 main.ts 同理:接受即落库,重开对话能看到这条注入的用户消息。
|
|
223
229
|
if (ok)
|
|
224
|
-
|
|
230
|
+
await deps.store.addMessage(chatId, 'user', content);
|
|
225
231
|
sendJson(res, 200, { ok, ...(ok ? {} : { reason: 'stream_not_active' }) });
|
|
226
232
|
return;
|
|
227
233
|
}
|
|
@@ -230,11 +236,19 @@ export function createBsServer(deps) {
|
|
|
230
236
|
sendJson(res, 200, await approvalBridge.decide(body));
|
|
231
237
|
return;
|
|
232
238
|
}
|
|
239
|
+
if (method === 'GET' && pathname === '/host/approval/pending') {
|
|
240
|
+
sendJson(res, 200, approvalBridge.pending());
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
233
243
|
// ─── ask_user(对齐 ask-user:answer IPC) ───
|
|
234
244
|
if (method === 'POST' && pathname === '/host/ask-user/answer') {
|
|
235
245
|
sendJson(res, 200, askUserBridge.answer(body));
|
|
236
246
|
return;
|
|
237
247
|
}
|
|
248
|
+
if (method === 'GET' && pathname === '/host/ask-user/pending') {
|
|
249
|
+
sendJson(res, 200, askUserBridge.pending());
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
238
252
|
// ─── local(对齐 local:* IPC) ───
|
|
239
253
|
if (method === 'POST' && pathname === '/host/local/exec-shell') {
|
|
240
254
|
const request = body;
|
|
@@ -357,19 +371,42 @@ export function createBsServer(deps) {
|
|
|
357
371
|
}
|
|
358
372
|
const url = new URL(req.url ?? '/', 'http://bs.local');
|
|
359
373
|
const pathname = url.pathname;
|
|
374
|
+
for (const handle of middleware) {
|
|
375
|
+
const result = await handle(req, res);
|
|
376
|
+
if (result !== 'handled' && result !== 'pass') {
|
|
377
|
+
throw new Error(`BS middleware returned invalid result: ${String(result)}`);
|
|
378
|
+
}
|
|
379
|
+
if (result === 'handled' || res.writableEnded)
|
|
380
|
+
return;
|
|
381
|
+
if (res.headersSent) {
|
|
382
|
+
throw new Error('BS middleware returned pass after starting a response');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
360
385
|
// 门 2:API 与宿主能力要 Bearer token;静态资源不验(浏览器靠它
|
|
361
386
|
// 拿 token),静态面不暴露任何能力。
|
|
362
387
|
const needsAuth = pathname.startsWith('/api/v2/') || pathname.startsWith('/host/');
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
388
|
+
let principal;
|
|
389
|
+
if (needsAuth) {
|
|
390
|
+
const authProvider = getAuthProvider();
|
|
391
|
+
if (authProvider) {
|
|
392
|
+
const decision = await authProvider.authenticate(req.headers);
|
|
393
|
+
if (!decision.ok) {
|
|
394
|
+
sendJson(res, decision.status, decision.body ?? { detail: decision.status === 403 ? 'forbidden' : 'unauthorized' });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
principal = decision.principal;
|
|
398
|
+
}
|
|
399
|
+
else if (!isAuthorized(req, url)) {
|
|
400
|
+
sendJson(res, 401, { detail: 'unauthorized' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
366
403
|
}
|
|
367
404
|
if (pathname === '/api/v2/events' && req.method === 'GET') {
|
|
368
405
|
bus.attach(res);
|
|
369
406
|
return;
|
|
370
407
|
}
|
|
371
408
|
if (pathname.startsWith('/api/v2/')) {
|
|
372
|
-
await handleApi(req, res, pathname);
|
|
409
|
+
await handleApi(req, res, pathname, principal);
|
|
373
410
|
return;
|
|
374
411
|
}
|
|
375
412
|
if (pathname.startsWith('/host/')) {
|
package/dist/server/index.js
CHANGED
|
@@ -1,100 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* 装配与 Electron 主进程(main.ts)相同的一组服务——同一份装配住在
|
|
5
|
-
* src/host/runtime.ts(HostRuntime),这里只保留 BS 特有的部分:SSE
|
|
6
|
-
* 总线广播、HTTP 出口(http-server.ts)、SIGINT/SIGTERM 生命周期。
|
|
7
|
-
* 数据目录、sidecar、工具链、审批流全部共用;两种模式因此操作同一份
|
|
8
|
-
* SQLite / JSON 配置(不要同时启动两个宿主——write-lease 会拒绝第二个写者)。
|
|
9
|
-
*
|
|
10
|
-
* 默认绑定 127.0.0.1:4787(DEEPPATH_BS_HOST / DEEPPATH_BS_PORT 可改)。
|
|
11
|
-
* 无认证:与桌面版同一信任模型,不要暴露到非回环地址。
|
|
12
|
-
*/
|
|
13
|
-
// 产品组装根必须是第一个 import:包迁移注册要先于 storage 单例构造(0.3b)。
|
|
14
|
-
// 产品组装根(products/<id>/server.ts 或 devtools/dev-server.ts)必须先于
|
|
15
|
-
// 本模块完成 import:包迁移注册要先于 storage 单例构造(0.3b / 2.3)。
|
|
16
|
-
import { existsSync } from 'node:fs';
|
|
17
|
-
import path from 'node:path';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import dotenv from 'dotenv';
|
|
20
|
-
import log from 'electron-log';
|
|
21
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
// 与 main.ts 相同的 .env 加载顺序(.env.local 优先;已有环境变量胜出)。
|
|
23
|
-
(function loadDotenvFiles() {
|
|
24
|
-
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
25
|
-
for (const file of ['.env.local', '.env']) {
|
|
26
|
-
const fullPath = path.join(repoRoot, file);
|
|
27
|
-
if (existsSync(fullPath))
|
|
28
|
-
dotenv.config({ path: fullPath, override: false });
|
|
29
|
-
}
|
|
30
|
-
})();
|
|
31
|
-
import { getBrand } from '../brand.js';
|
|
32
|
-
import { getUserDataDir } from '../runtime.js';
|
|
33
|
-
import { createHostRuntime } from '../host/runtime.js';
|
|
34
|
-
import { registerPackHttpRoutes } from '../host/http-routes.js';
|
|
35
|
-
import { SseBus } from './sse-bus.js';
|
|
36
|
-
import { createBsServer } from './http-server.js';
|
|
37
|
-
// BS 模式没有 Electron 的 userData 概念,electron-log 默认会写
|
|
38
|
-
// ~/.config/<name>/logs;在 HOME 只读(容器/只读环境)或用户显式指定
|
|
39
|
-
// DEEPPATH_USER_DATA_DIR 时,日志应跟随应用数据目录走。
|
|
40
|
-
log.transports.file.resolvePathFn = () => path.join(getUserDataDir(), 'logs', 'main.log');
|
|
1
|
+
/** Executable BS entry; reusable assembly lives in `start.ts`. */
|
|
2
|
+
import { startBsHost } from './start.js';
|
|
41
3
|
async function main() {
|
|
42
|
-
const
|
|
43
|
-
const bus = new SseBus();
|
|
44
|
-
const broadcast = (channel, payload) => bus.broadcast(channel, payload);
|
|
45
|
-
// 与 CS 同一份服务装配;BS 的差异只有广播(SSE 总线)与"有浏览器连着
|
|
46
|
-
// 事件总线 = 有人能应答审批/提问"的 hasWindow 判定。
|
|
47
|
-
const runtime = createHostRuntime({
|
|
48
|
-
broadcast,
|
|
49
|
-
hasWindow: () => bus.size > 0,
|
|
50
|
-
onLog: (line) => log.info('[sidecar]', line),
|
|
51
|
-
taskSweepReason: '服务重启,任务流已中断',
|
|
52
|
-
});
|
|
53
|
-
const { localExecutor, localScriptRegistry, terminalManager, packHandles, localBackendRouter, approvalBridge, askUserBridge, maybeExecInTerminal, } = runtime;
|
|
54
|
-
// 2.3 起 web 产物按产品构建:产品入口注入 DEEPPATH_WEB_DIST
|
|
55
|
-
//(products/<id>/web/dist);缺省回退到 shell 包自带的 web/dist
|
|
56
|
-
//(packages/agent-shell/web/dist),仅用于给出可读的报错。
|
|
57
|
-
const webDistDir = process.env.DEEPPATH_WEB_DIST ??
|
|
58
|
-
path.join(__dirname, '..', '..', 'web', 'dist');
|
|
59
|
-
if (!existsSync(path.join(webDistDir, 'index.html'))) {
|
|
60
|
-
console.error(`[bs] web build not found at ${webDistDir} — run \`pnpm build\` first.`);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
// 场景包 HTTP 路由(/host/<packId>/*)由装配产物给出,宿主循环注册
|
|
64
|
-
//(2.3;路由路径命名空间由 registerPackHttpRoutes 校验)。
|
|
65
|
-
for (const [packId, handle] of packHandles) {
|
|
66
|
-
const routes = handle.httpRoutes?.();
|
|
67
|
-
if (routes)
|
|
68
|
-
registerPackHttpRoutes(packId, routes);
|
|
69
|
-
}
|
|
70
|
-
const server = createBsServer({
|
|
71
|
-
localBackendRouter,
|
|
72
|
-
localExecutor,
|
|
73
|
-
localScriptRegistry,
|
|
74
|
-
terminalManager,
|
|
75
|
-
approvalBridge,
|
|
76
|
-
askUserBridge,
|
|
77
|
-
maybeExecInTerminal,
|
|
78
|
-
bus,
|
|
79
|
-
webDistDir,
|
|
80
|
-
});
|
|
81
|
-
const host = process.env.DEEPPATH_BS_HOST || '127.0.0.1';
|
|
82
|
-
const port = Number(process.env.DEEPPATH_BS_PORT || 4787);
|
|
83
|
-
await new Promise((resolve, reject) => {
|
|
84
|
-
server.once('error', reject);
|
|
85
|
-
server.listen(port, host, () => resolve());
|
|
86
|
-
});
|
|
87
|
-
console.log(`[bs] ${brand.displayName} server listening at http://${host}:${port} (flavor=${brand.flavor})`);
|
|
88
|
-
// 任务清扫 / mock 广播 / sidecar 启动 / MCP 刷新 / 终端预热。不阻塞
|
|
89
|
-
// HTTP 就绪;竞速的回合由 router 回退处理(同 main.ts)。
|
|
90
|
-
runtime.start();
|
|
4
|
+
const handle = await startBsHost();
|
|
91
5
|
let shuttingDown = false;
|
|
92
6
|
const shutdown = () => {
|
|
93
7
|
if (shuttingDown)
|
|
94
8
|
return;
|
|
95
9
|
shuttingDown = true;
|
|
96
10
|
console.log('[bs] shutting down…');
|
|
97
|
-
void
|
|
11
|
+
void handle.shutdown().finally(() => process.exit(0));
|
|
98
12
|
// 兜底:sidecar 卡住也不拖住退出。
|
|
99
13
|
setTimeout(() => process.exit(0), 5_000).unref();
|
|
100
14
|
};
|
package/dist/server/sse-bus.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BS 模式的 SSE 事件总线:替代 Electron 的 `webContents.send(channel, payload)`
|
|
3
3
|
* 广播。所有宿主事件(terminal:data、approval:request、chat-title-updated、
|
|
4
|
-
*
|
|
4
|
+
* suggested-replies、场景包事件……)经这一条 `GET /api/v2/events` 长连接推给浏览器,
|
|
5
5
|
* channel 名与 IPC 通道保持一致,前端 http-bridge 按名分发。
|
|
6
6
|
*/
|
|
7
7
|
import type { ServerResponse } from 'node:http';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
import type { TenantScope } from '../storage/driver.js';
|
|
3
|
+
import { type BsMiddleware } from './http-server.js';
|
|
4
|
+
export interface BsHostOptions {
|
|
5
|
+
host?: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
authToken?: string;
|
|
8
|
+
middleware?: readonly BsMiddleware[];
|
|
9
|
+
/** Fixed storage owner for a per-user host process. */
|
|
10
|
+
scope?: TenantScope;
|
|
11
|
+
}
|
|
12
|
+
export interface BsHostHandle {
|
|
13
|
+
readonly server: Server;
|
|
14
|
+
readonly host: string;
|
|
15
|
+
readonly port: number;
|
|
16
|
+
/** Stops accepting requests and shuts down all host runtime services. */
|
|
17
|
+
shutdown(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Assembles and starts the BS HTTP host.
|
|
21
|
+
*
|
|
22
|
+
* Product composition roots may register providers, package contributions,
|
|
23
|
+
* and middleware before calling this function.
|
|
24
|
+
*/
|
|
25
|
+
export declare function startBsHost(options?: BsHostOptions): Promise<BsHostHandle>;
|
|
26
|
+
export type { BsMiddleware };
|