@steerable/agent-shell 0.6.15
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/LICENSE +91 -0
- package/contracts/tool-contract.json +326 -0
- package/dist/attachments.d.ts +41 -0
- package/dist/attachments.js +147 -0
- package/dist/brand.d.ts +24 -0
- package/dist/brand.js +92 -0
- package/dist/host/http-routes.d.ts +21 -0
- package/dist/host/http-routes.js +55 -0
- package/dist/host/ipc.d.ts +11 -0
- package/dist/host/ipc.js +20 -0
- package/dist/host/pack-assembly.d.ts +86 -0
- package/dist/host/pack-assembly.js +32 -0
- package/dist/host/runtime.d.ts +69 -0
- package/dist/host/runtime.js +207 -0
- package/dist/host/visible-terminal-exec.d.ts +21 -0
- package/dist/host/visible-terminal-exec.js +151 -0
- package/dist/hosted-web-search.d.ts +13 -0
- package/dist/hosted-web-search.js +82 -0
- package/dist/image-attachment.d.ts +39 -0
- package/dist/image-attachment.js +133 -0
- package/dist/insights/flush.d.ts +10 -0
- package/dist/insights/flush.js +139 -0
- package/dist/insights/record.d.ts +17 -0
- package/dist/insights/record.js +44 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.js +22 -0
- package/dist/llm/index.d.ts +23 -0
- package/dist/llm/index.js +105 -0
- package/dist/llm/ollama.d.ts +23 -0
- package/dist/llm/ollama.js +242 -0
- package/dist/llm/openai-compat.d.ts +20 -0
- package/dist/llm/openai-compat.js +199 -0
- package/dist/llm/sidecar-provider.d.ts +37 -0
- package/dist/llm/sidecar-provider.js +163 -0
- package/dist/llm/tool-choice.d.ts +35 -0
- package/dist/llm/tool-choice.js +85 -0
- package/dist/llm/types.d.ts +122 -0
- package/dist/llm/types.js +1 -0
- package/dist/local-backend/agent-capability.d.ts +101 -0
- package/dist/local-backend/agent-capability.js +174 -0
- package/dist/local-backend/ai-title.d.ts +43 -0
- package/dist/local-backend/ai-title.js +173 -0
- package/dist/local-backend/auto-continue-helper.d.ts +80 -0
- package/dist/local-backend/auto-continue-helper.js +83 -0
- package/dist/local-backend/branch-helper.d.ts +24 -0
- package/dist/local-backend/branch-helper.js +27 -0
- package/dist/local-backend/context-compactor.d.ts +81 -0
- package/dist/local-backend/context-compactor.js +213 -0
- package/dist/local-backend/coreloop-stream.d.ts +245 -0
- package/dist/local-backend/coreloop-stream.js +277 -0
- package/dist/local-backend/deferred-detector.d.ts +15 -0
- package/dist/local-backend/deferred-detector.js +124 -0
- package/dist/local-backend/history-helper.d.ts +30 -0
- package/dist/local-backend/history-helper.js +34 -0
- package/dist/local-backend/interrupted-helper.d.ts +29 -0
- package/dist/local-backend/interrupted-helper.js +25 -0
- package/dist/local-backend/live-stream.d.ts +36 -0
- package/dist/local-backend/live-stream.js +23 -0
- package/dist/local-backend/llm-diagnose.d.ts +37 -0
- package/dist/local-backend/llm-diagnose.js +284 -0
- package/dist/local-backend/message-triggers.d.ts +27 -0
- package/dist/local-backend/message-triggers.js +67 -0
- package/dist/local-backend/pack-backend-routes.d.ts +31 -0
- package/dist/local-backend/pack-backend-routes.js +62 -0
- package/dist/local-backend/pack-turn-hooks.d.ts +37 -0
- package/dist/local-backend/pack-turn-hooks.js +67 -0
- package/dist/local-backend/prompt-builder.d.ts +101 -0
- package/dist/local-backend/prompt-builder.js +246 -0
- package/dist/local-backend/regenerate-helper.d.ts +62 -0
- package/dist/local-backend/regenerate-helper.js +75 -0
- package/dist/local-backend/router.d.ts +175 -0
- package/dist/local-backend/router.js +3139 -0
- package/dist/local-backend/skill-install.d.ts +19 -0
- package/dist/local-backend/skill-install.js +71 -0
- package/dist/local-backend/skill-loader.d.ts +92 -0
- package/dist/local-backend/skill-loader.js +146 -0
- package/dist/local-backend/skills/00-identity/SKILL.md +32 -0
- package/dist/local-backend/skills/10-goal/SKILL.md +59 -0
- package/dist/local-backend/skills/11-loop/SKILL.md +71 -0
- package/dist/local-backend/skills/12-create-skill/SKILL.md +88 -0
- package/dist/local-backend/skills/70-plan-mode/SKILL.md +58 -0
- package/dist/local-backend/skills/80-tool-usage/SKILL.md +70 -0
- package/dist/local-backend/skills/81-anti-deferred/SKILL.md +53 -0
- package/dist/local-backend/skills/82-data-grounding/SKILL.md +56 -0
- package/dist/local-backend/skills/85-local-exec/SKILL.md +86 -0
- package/dist/local-backend/skills/86-proactive-coding/SKILL.md +51 -0
- package/dist/local-backend/subagent-profiles.d.ts +30 -0
- package/dist/local-backend/subagent-profiles.js +74 -0
- package/dist/local-backend/task-process.d.ts +12 -0
- package/dist/local-backend/task-process.js +176 -0
- package/dist/local-backend/task-service.d.ts +135 -0
- package/dist/local-backend/task-service.js +565 -0
- package/dist/local-backend/turn-duration.d.ts +2 -0
- package/dist/local-backend/turn-duration.js +9 -0
- package/dist/local-backend/turn-timeline.d.ts +16 -0
- package/dist/local-backend/turn-timeline.js +42 -0
- package/dist/local-backend/worktree-service.d.ts +84 -0
- package/dist/local-backend/worktree-service.js +243 -0
- package/dist/local-edit.d.ts +48 -0
- package/dist/local-edit.js +44 -0
- package/dist/local-executor.d.ts +255 -0
- package/dist/local-executor.js +881 -0
- package/dist/local-script-registry.d.ts +28 -0
- package/dist/local-script-registry.js +63 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.js +12 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +855 -0
- package/dist/mcp-executor.d.ts +45 -0
- package/dist/mcp-executor.js +241 -0
- package/dist/mcp-server-registry.d.ts +104 -0
- package/dist/mcp-server-registry.js +234 -0
- package/dist/preload-default.d.ts +1 -0
- package/dist/preload-default.js +9 -0
- package/dist/preload.cjs +395 -0
- package/dist/preload.d.ts +20 -0
- package/dist/preload.js +411 -0
- package/dist/product-config.d.ts +43 -0
- package/dist/product-config.js +26 -0
- package/dist/project-registry.d.ts +55 -0
- package/dist/project-registry.js +106 -0
- package/dist/project-rules.d.ts +15 -0
- package/dist/project-rules.js +102 -0
- package/dist/runtime.d.ts +62 -0
- package/dist/runtime.js +217 -0
- package/dist/scenario/pack.d.ts +8 -0
- package/dist/scenario/pack.js +1 -0
- package/dist/scenario/registry.d.ts +24 -0
- package/dist/scenario/registry.js +31 -0
- package/dist/server/http-server.d.ts +39 -0
- package/dist/server/http-server.js +361 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +107 -0
- package/dist/server/sse-bus.d.ts +14 -0
- package/dist/server/sse-bus.js +31 -0
- package/dist/shell-adapt.d.ts +21 -0
- package/dist/shell-adapt.js +104 -0
- package/dist/sidecar/boot.d.ts +36 -0
- package/dist/sidecar/boot.js +343 -0
- package/dist/sidecar/egress-hint.d.ts +15 -0
- package/dist/sidecar/egress-hint.js +46 -0
- package/dist/sidecar/egress-proxy.d.ts +183 -0
- package/dist/sidecar/egress-proxy.js +419 -0
- package/dist/sidecar/errors.d.ts +22 -0
- package/dist/sidecar/errors.js +38 -0
- package/dist/sidecar/exec-sandbox.d.ts +48 -0
- package/dist/sidecar/exec-sandbox.js +94 -0
- package/dist/sidecar/handle.d.ts +32 -0
- package/dist/sidecar/handle.js +53 -0
- package/dist/sidecar/index.d.ts +14 -0
- package/dist/sidecar/index.js +13 -0
- package/dist/sidecar/proxy-detect.d.ts +50 -0
- package/dist/sidecar/proxy-detect.js +182 -0
- package/dist/sidecar/reverse-approval.d.ts +55 -0
- package/dist/sidecar/reverse-approval.js +86 -0
- package/dist/sidecar/reverse-ask-user.d.ts +34 -0
- package/dist/sidecar/reverse-ask-user.js +59 -0
- package/dist/sidecar/reverse-spawn.d.ts +19 -0
- package/dist/sidecar/reverse-spawn.js +161 -0
- package/dist/sidecar/reverse-tools.d.ts +29 -0
- package/dist/sidecar/reverse-tools.js +106 -0
- package/dist/sidecar/safety-patterns.d.ts +41 -0
- package/dist/sidecar/safety-patterns.js +157 -0
- package/dist/sidecar/storage-path.d.ts +14 -0
- package/dist/sidecar/storage-path.js +35 -0
- package/dist/sidecar/supervisor.d.ts +218 -0
- package/dist/sidecar/supervisor.js +932 -0
- package/dist/sidecar/types.d.ts +601 -0
- package/dist/sidecar/types.js +1 -0
- package/dist/single-instance.d.ts +11 -0
- package/dist/single-instance.js +21 -0
- package/dist/storage/empty-chats.d.ts +9 -0
- package/dist/storage/empty-chats.js +16 -0
- package/dist/storage/index.d.ts +373 -0
- package/dist/storage/index.js +1158 -0
- package/dist/storage/insights-redact.d.ts +2 -0
- package/dist/storage/insights-redact.js +30 -0
- package/dist/storage/insights-settings.d.ts +53 -0
- package/dist/storage/insights-settings.js +92 -0
- package/dist/storage/llm-settings.d.ts +120 -0
- package/dist/storage/llm-settings.js +233 -0
- package/dist/storage/local-store-singleton.d.ts +28 -0
- package/dist/storage/local-store-singleton.js +38 -0
- package/dist/storage/message-order.d.ts +25 -0
- package/dist/storage/message-order.js +27 -0
- package/dist/storage/pack-migrations.d.ts +22 -0
- package/dist/storage/pack-migrations.js +24 -0
- package/dist/storage/pack-seeds.d.ts +36 -0
- package/dist/storage/pack-seeds.js +42 -0
- package/dist/storage/telemetry-settings.d.ts +38 -0
- package/dist/storage/telemetry-settings.js +59 -0
- package/dist/storage/usage-summary.d.ts +55 -0
- package/dist/storage/usage-summary.js +38 -0
- package/dist/storage/web-search-settings.d.ts +38 -0
- package/dist/storage/web-search-settings.js +74 -0
- package/dist/storage/write-lease.d.ts +26 -0
- package/dist/storage/write-lease.js +74 -0
- package/dist/terminal-manager.d.ts +83 -0
- package/dist/terminal-manager.js +506 -0
- package/dist/tool-router.d.ts +228 -0
- package/dist/tool-router.js +930 -0
- package/dist/tool-search-rank.d.ts +42 -0
- package/dist/tool-search-rank.js +96 -0
- package/package.json +67 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BS 模式 HTTP server:把 CS(Electron)模式下挂在 IPC 后面的同一组服务
|
|
3
|
+
* 暴露成真正的 HTTP 接口,并托管 apps/web/dist 静态产物。
|
|
4
|
+
*
|
|
5
|
+
* 路由分三层:
|
|
6
|
+
* - `/api/v2/*` → LocalBackendRouter(chat / 设置 / 技能 / MCP …),
|
|
7
|
+
* 与 Electron 下 `local-backend:request` 同一入口;
|
|
8
|
+
* 流式路径(chats/:id/run|send|agent、…/regenerate)
|
|
9
|
+
* 走 handleStream,响应即标准 SSE。
|
|
10
|
+
* - `GET /api/v2/events` → SseBus 事件总线(替代 webContents.send 广播)。
|
|
11
|
+
* - `/host/*` → preload 里 direct-IPC 能力的 HTTP 等价物
|
|
12
|
+
* (terminal / local / approval 为宿主自带;
|
|
13
|
+
* 场景包路由经 host/http-routes 注册表匹配)。
|
|
14
|
+
*
|
|
15
|
+
* 安全约定:默认绑定 127.0.0.1、无认证——本机自用定位(与 Electron 版相同
|
|
16
|
+
* 的信任模型:能到端口 = 本机用户)。不要绑到非回环地址,那会把 shell /
|
|
17
|
+
* 文件读写 / 场景包工具控制暴露给整个网络。
|
|
18
|
+
*/
|
|
19
|
+
import { createServer } from 'node:http';
|
|
20
|
+
import { readFile } from 'node:fs/promises';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import log from 'electron-log';
|
|
24
|
+
import { getActiveCoreLoopStreamId } from '../local-backend/coreloop-stream.js';
|
|
25
|
+
import { getSidecarSupervisor } from '../llm/index.js';
|
|
26
|
+
import { localStore } from '../storage/index.js';
|
|
27
|
+
import { matchPackHttpRoute } from '../host/http-routes.js';
|
|
28
|
+
import { getBrand } from '../brand.js';
|
|
29
|
+
import { saveAttachmentFiles } from '../attachments.js';
|
|
30
|
+
/** 与 router.handleStream 内部的路由正则保持一致——只有这些路径是流式。 */
|
|
31
|
+
const STREAM_PATH_PATTERNS = [
|
|
32
|
+
/^\/api\/v2\/chats\/[^/]+\/(send|run|agent)$/,
|
|
33
|
+
/^\/api\/v2\/chats\/[^/]+\/messages\/[^/]+\/regenerate$/,
|
|
34
|
+
];
|
|
35
|
+
const MAX_BODY_BYTES = 25 * 1024 * 1024;
|
|
36
|
+
const MIME = {
|
|
37
|
+
'.html': 'text/html; charset=utf-8',
|
|
38
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
39
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
40
|
+
'.css': 'text/css; charset=utf-8',
|
|
41
|
+
'.json': 'application/json; charset=utf-8',
|
|
42
|
+
'.map': 'application/json; charset=utf-8',
|
|
43
|
+
'.png': 'image/png',
|
|
44
|
+
'.jpg': 'image/jpeg',
|
|
45
|
+
'.jpeg': 'image/jpeg',
|
|
46
|
+
'.gif': 'image/gif',
|
|
47
|
+
'.svg': 'image/svg+xml',
|
|
48
|
+
'.ico': 'image/x-icon',
|
|
49
|
+
'.webp': 'image/webp',
|
|
50
|
+
'.woff': 'font/woff',
|
|
51
|
+
'.woff2': 'font/woff2',
|
|
52
|
+
'.ttf': 'font/ttf',
|
|
53
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
54
|
+
};
|
|
55
|
+
function sendJson(res, status, data) {
|
|
56
|
+
const body = JSON.stringify(data ?? null);
|
|
57
|
+
res.writeHead(status, {
|
|
58
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
59
|
+
'Cache-Control': 'no-store',
|
|
60
|
+
});
|
|
61
|
+
res.end(body);
|
|
62
|
+
}
|
|
63
|
+
async function readJsonBody(req) {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
let size = 0;
|
|
66
|
+
for await (const chunk of req) {
|
|
67
|
+
size += chunk.length;
|
|
68
|
+
if (size > MAX_BODY_BYTES)
|
|
69
|
+
throw new Error('request body too large');
|
|
70
|
+
chunks.push(chunk);
|
|
71
|
+
}
|
|
72
|
+
if (chunks.length === 0)
|
|
73
|
+
return undefined;
|
|
74
|
+
const text = Buffer.concat(chunks).toString('utf8').trim();
|
|
75
|
+
if (!text)
|
|
76
|
+
return undefined;
|
|
77
|
+
return JSON.parse(text);
|
|
78
|
+
}
|
|
79
|
+
function asRecord(body) {
|
|
80
|
+
return body && typeof body === 'object' ? body : {};
|
|
81
|
+
}
|
|
82
|
+
export function createBsServer(deps) {
|
|
83
|
+
const { localBackendRouter, localExecutor, localScriptRegistry, terminalManager, approvalBridge, askUserBridge, maybeExecInTerminal, bus, webDistDir, } = deps;
|
|
84
|
+
async function handleApi(req, res, pathname) {
|
|
85
|
+
const isStream = req.method === 'POST' && STREAM_PATH_PATTERNS.some((p) => p.test(pathname));
|
|
86
|
+
const body = req.method === 'POST' || req.method === 'PATCH' || req.method === 'PUT'
|
|
87
|
+
? await readJsonBody(req)
|
|
88
|
+
: undefined;
|
|
89
|
+
const search = new URL(req.url ?? '/', 'http://bs.local').search;
|
|
90
|
+
const request = { method: req.method ?? 'GET', path: `${pathname}${search}`, body };
|
|
91
|
+
if (isStream) {
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
// 浏览器关页/取消 fetch → 中止 agent 循环(对齐 Electron 的 cancelStream)。
|
|
94
|
+
req.on('close', () => controller.abort());
|
|
95
|
+
res.writeHead(200, {
|
|
96
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
97
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
98
|
+
Connection: 'keep-alive',
|
|
99
|
+
'X-Accel-Buffering': 'no',
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
const result = await localBackendRouter.handleStream(request, (chunk) => res.write(chunk), { signal: controller.signal });
|
|
103
|
+
log.info(`[bs] stream ${request.method} ${pathname} -> ${result.status}`);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
107
|
+
log.error(`[bs] stream ${request.method} ${pathname} threw`, err);
|
|
108
|
+
try {
|
|
109
|
+
res.write(`event: error\ndata: ${JSON.stringify({ message })}\n\n`);
|
|
110
|
+
}
|
|
111
|
+
catch { /* client gone */ }
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
res.end();
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const response = await localBackendRouter.handle(request);
|
|
120
|
+
log.info(`[bs] ${request.method} ${pathname} -> ${response.status}`);
|
|
121
|
+
sendJson(res, response.status, response.data);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
log.error(`[bs] ${request.method} ${pathname} threw`, err);
|
|
125
|
+
sendJson(res, 500, { detail: err instanceof Error ? err.message : String(err) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** `/host/*`:preload direct-IPC 的 HTTP 等价物。全部 POST JSON,除标记 GET 的。 */
|
|
129
|
+
async function handleHost(req, res, pathname) {
|
|
130
|
+
const body = asRecord(await readJsonBody(req));
|
|
131
|
+
const method = req.method ?? 'GET';
|
|
132
|
+
if (method === 'GET' && pathname === '/host/info') {
|
|
133
|
+
const brand = getBrand();
|
|
134
|
+
sendJson(res, 200, {
|
|
135
|
+
runtime: 'bs',
|
|
136
|
+
platform: process.platform,
|
|
137
|
+
flavor: brand.flavor,
|
|
138
|
+
brandName: brand.displayName,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// ─── terminal(对齐 main.ts 的 terminal:* IPC) ───
|
|
143
|
+
if (method === 'GET' && pathname === '/host/terminal/list') {
|
|
144
|
+
sendJson(res, 200, terminalManager.list());
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (method === 'POST' && pathname === '/host/terminal/spawn') {
|
|
148
|
+
sendJson(res, 200, terminalManager.spawn(body));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (method === 'POST' && pathname === '/host/terminal/ensure') {
|
|
152
|
+
const session = terminalManager.ensurePrimary(body);
|
|
153
|
+
// 对齐 Electron ensure:附带 attach 前的 replay buffer,前端补写一次。
|
|
154
|
+
const replay = terminalManager.getReplayBuffer(session.id);
|
|
155
|
+
sendJson(res, 200, { session, replay: replay ?? '' });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (method === 'POST' && pathname === '/host/terminal/write') {
|
|
159
|
+
sendJson(res, 200, terminalManager.write(String(body.id ?? ''), String(body.data ?? '')));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (method === 'POST' && pathname === '/host/terminal/resize') {
|
|
163
|
+
sendJson(res, 200, terminalManager.resize(String(body.id ?? ''), Number(body.cols ?? 80), Number(body.rows ?? 24)));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (method === 'POST' && pathname === '/host/terminal/kill') {
|
|
167
|
+
sendJson(res, 200, terminalManager.kill(String(body.id ?? '')));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (method === 'POST' && pathname === '/host/terminal/exec') {
|
|
171
|
+
let id = typeof body.id === 'string' ? body.id : undefined;
|
|
172
|
+
if (!id)
|
|
173
|
+
id = terminalManager.ensurePrimary().id;
|
|
174
|
+
try {
|
|
175
|
+
const result = await terminalManager.exec(id, String(body.command ?? ''), body.timeoutMs);
|
|
176
|
+
sendJson(res, 200, result);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
sendJson(res, 200, {
|
|
180
|
+
success: false,
|
|
181
|
+
exitCode: -1,
|
|
182
|
+
stdout: '',
|
|
183
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
184
|
+
truncated: false,
|
|
185
|
+
durationMs: 0,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// ─── 轮中转向(对齐 local-backend:steer IPC;不走 router) ───
|
|
191
|
+
if (method === 'POST' && pathname === '/host/steer') {
|
|
192
|
+
const chatId = typeof body.chatId === 'string' ? body.chatId : '';
|
|
193
|
+
const content = typeof body.content === 'string' ? body.content : '';
|
|
194
|
+
if (!chatId || !content.trim()) {
|
|
195
|
+
sendJson(res, 200, { ok: false, reason: 'invalid_params' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const streamId = getActiveCoreLoopStreamId(chatId);
|
|
199
|
+
const supervisor = getSidecarSupervisor();
|
|
200
|
+
if (!streamId || !supervisor) {
|
|
201
|
+
sendJson(res, 200, { ok: false, reason: 'no_active_coreloop_turn' });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const ok = await supervisor.steerChat(streamId, content);
|
|
205
|
+
// 与 main.ts 同理:接受即落库,重开对话能看到这条注入的用户消息。
|
|
206
|
+
if (ok)
|
|
207
|
+
localStore.addMessage(chatId, 'user', content);
|
|
208
|
+
sendJson(res, 200, { ok, ...(ok ? {} : { reason: 'stream_not_active' }) });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// ─── approval(对齐 approval:decide IPC) ───
|
|
212
|
+
if (method === 'POST' && pathname === '/host/approval/decide') {
|
|
213
|
+
sendJson(res, 200, await approvalBridge.decide(body));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// ─── ask_user(对齐 ask-user:answer IPC) ───
|
|
217
|
+
if (method === 'POST' && pathname === '/host/ask-user/answer') {
|
|
218
|
+
sendJson(res, 200, askUserBridge.answer(body));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// ─── local(对齐 local:* IPC) ───
|
|
222
|
+
if (method === 'POST' && pathname === '/host/local/exec-shell') {
|
|
223
|
+
const request = body;
|
|
224
|
+
const viaTerminal = await maybeExecInTerminal(request);
|
|
225
|
+
sendJson(res, 200, viaTerminal ?? (await localExecutor.executeShell(request)));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (method === 'POST' && pathname === '/host/local/read-file') {
|
|
229
|
+
sendJson(res, 200, await localExecutor.readLocalFile(body));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (method === 'POST' && pathname === '/host/local/write-file') {
|
|
233
|
+
sendJson(res, 200, await localExecutor.writeLocalFile(body));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (method === 'POST' && pathname === '/host/local/open-path') {
|
|
237
|
+
sendJson(res, 200, await localExecutor.openLocalTarget(body));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (method === 'GET' && pathname === '/host/local/scripts') {
|
|
241
|
+
sendJson(res, 200, localScriptRegistry.list());
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (method === 'POST' && pathname === '/host/local/scripts/add') {
|
|
245
|
+
sendJson(res, 200, localScriptRegistry.create(body));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (method === 'POST' && pathname === '/host/local/scripts/update') {
|
|
249
|
+
sendJson(res, 200, localScriptRegistry.update(String(body.id ?? ''), (body.updates ?? {})));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (method === 'POST' && pathname === '/host/local/scripts/delete') {
|
|
253
|
+
localScriptRegistry.delete(String(body.id ?? ''));
|
|
254
|
+
sendJson(res, 200, { success: true });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (method === 'POST' && pathname === '/host/local/scripts/run') {
|
|
258
|
+
const script = localScriptRegistry.getById(String(body.id ?? ''));
|
|
259
|
+
if (!script) {
|
|
260
|
+
sendJson(res, 200, { success: false, error: `Script not found: ${String(body.id ?? '')}` });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
sendJson(res, 200, await localExecutor.executeShell({
|
|
264
|
+
command: script.command,
|
|
265
|
+
cwd: script.cwd,
|
|
266
|
+
timeout: script.timeout,
|
|
267
|
+
}));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (method === 'POST' && pathname === '/host/local/update-safety-config') {
|
|
271
|
+
localExecutor.updateSafetyConfig(body);
|
|
272
|
+
sendJson(res, 200, { success: true });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// ─── 场景包路由(0.4:/host/<pack>/* 由包经 host/http-routes 注册表
|
|
276
|
+
// 贡献;在宿主自带路由之后、404 之前统一匹配,包不能遮蔽宿主路由)。
|
|
277
|
+
// host 端点目前不用 query string,query 先传空表。 ───
|
|
278
|
+
const packMatch = matchPackHttpRoute(method, pathname);
|
|
279
|
+
if (packMatch) {
|
|
280
|
+
sendJson(res, 200, await packMatch.route.handler({
|
|
281
|
+
params: packMatch.params,
|
|
282
|
+
query: {},
|
|
283
|
+
body,
|
|
284
|
+
}));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// ─── 会话附件(对齐 attachments:save IPC;BS 用 base64 字节) ───
|
|
288
|
+
if (method === 'POST' && pathname === '/host/attachments/save') {
|
|
289
|
+
const chatId = typeof body.chatId === 'string' ? body.chatId : '';
|
|
290
|
+
const files = Array.isArray(body.files)
|
|
291
|
+
? body.files.filter((f) => f && (typeof f.path === 'string' || typeof f.data === 'string'))
|
|
292
|
+
: [];
|
|
293
|
+
sendJson(res, 200, await saveAttachmentFiles(chatId, files));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
sendJson(res, 404, { detail: `Unknown host endpoint: ${method} ${pathname}` });
|
|
297
|
+
}
|
|
298
|
+
/** 静态托管 + index.html 注入 BS 标记(前端据此切换 http-bridge)。 */
|
|
299
|
+
async function serveStatic(req, res, pathname) {
|
|
300
|
+
const rel = pathname === '/' ? '/index.html' : pathname;
|
|
301
|
+
const filePath = path.join(webDistDir, path.normalize(rel).replace(/^([/\\])+/, ''));
|
|
302
|
+
if (!filePath.startsWith(webDistDir)) {
|
|
303
|
+
sendJson(res, 403, { detail: 'forbidden' });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const target = existsSync(filePath) && !filePath.endsWith(path.sep) ? filePath : path.join(webDistDir, 'index.html');
|
|
307
|
+
try {
|
|
308
|
+
let content = await readFile(target);
|
|
309
|
+
const ext = path.extname(target).toLowerCase();
|
|
310
|
+
if (ext === '.html') {
|
|
311
|
+
// 注入 BS 引导信息:platform 是 preload 里的同步字段,http-bridge
|
|
312
|
+
// 需要同步可见;brand 让 <title>/favicon 在 bridge 就绪前就正确。
|
|
313
|
+
const brand = getBrand();
|
|
314
|
+
const bootstrap = `<script>window.__DEEPPATH_BS__=${JSON.stringify({
|
|
315
|
+
platform: process.platform,
|
|
316
|
+
flavor: brand.flavor,
|
|
317
|
+
brandName: brand.displayName,
|
|
318
|
+
})};</script>`;
|
|
319
|
+
content = content.toString('utf8').replace('<head>', `<head>${bootstrap}`);
|
|
320
|
+
}
|
|
321
|
+
res.writeHead(200, {
|
|
322
|
+
'Content-Type': MIME[ext] ?? 'application/octet-stream',
|
|
323
|
+
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600',
|
|
324
|
+
});
|
|
325
|
+
res.end(content);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
log.warn('[bs] static read failed', { target, err });
|
|
329
|
+
sendJson(res, 404, { detail: 'not found' });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return createServer((req, res) => {
|
|
333
|
+
void (async () => {
|
|
334
|
+
const url = new URL(req.url ?? '/', 'http://bs.local');
|
|
335
|
+
const pathname = url.pathname;
|
|
336
|
+
if (pathname === '/api/v2/events' && req.method === 'GET') {
|
|
337
|
+
bus.attach(res);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (pathname.startsWith('/api/v2/')) {
|
|
341
|
+
await handleApi(req, res, pathname);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (pathname.startsWith('/host/')) {
|
|
345
|
+
await handleHost(req, res, pathname);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
349
|
+
await serveStatic(req, res, pathname);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
sendJson(res, 404, { detail: 'not found' });
|
|
353
|
+
})().catch((err) => {
|
|
354
|
+
log.error('[bs] unhandled request error', err);
|
|
355
|
+
if (!res.headersSent)
|
|
356
|
+
sendJson(res, 500, { detail: 'internal error' });
|
|
357
|
+
else
|
|
358
|
+
res.end();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BS 模式入口:`node dist/server/index.js`。
|
|
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');
|
|
41
|
+
async function main() {
|
|
42
|
+
const brand = getBrand();
|
|
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();
|
|
91
|
+
let shuttingDown = false;
|
|
92
|
+
const shutdown = () => {
|
|
93
|
+
if (shuttingDown)
|
|
94
|
+
return;
|
|
95
|
+
shuttingDown = true;
|
|
96
|
+
console.log('[bs] shutting down…');
|
|
97
|
+
void runtime.shutdown().finally(() => process.exit(0));
|
|
98
|
+
// 兜底:sidecar 卡住也不拖住退出。
|
|
99
|
+
setTimeout(() => process.exit(0), 5_000).unref();
|
|
100
|
+
};
|
|
101
|
+
process.on('SIGINT', shutdown);
|
|
102
|
+
process.on('SIGTERM', shutdown);
|
|
103
|
+
}
|
|
104
|
+
main().catch((err) => {
|
|
105
|
+
console.error('[bs] failed to start:', err);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BS 模式的 SSE 事件总线:替代 Electron 的 `webContents.send(channel, payload)`
|
|
3
|
+
* 广播。所有宿主事件(terminal:data、approval:request、chat-title-updated、
|
|
4
|
+
* 场景包事件……)经这一条 `GET /api/v2/events` 长连接推给浏览器,
|
|
5
|
+
* channel 名与 IPC 通道保持一致,前端 http-bridge 按名分发。
|
|
6
|
+
*/
|
|
7
|
+
import type { ServerResponse } from 'node:http';
|
|
8
|
+
export declare class SseBus {
|
|
9
|
+
private readonly clients;
|
|
10
|
+
/** 已连接浏览器数;approval 桥用它判断"有没有人能应答"(对齐 hasWindow)。 */
|
|
11
|
+
get size(): number;
|
|
12
|
+
attach(res: ServerResponse): void;
|
|
13
|
+
broadcast(channel: string, payload: unknown): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class SseBus {
|
|
2
|
+
clients = new Set();
|
|
3
|
+
/** 已连接浏览器数;approval 桥用它判断"有没有人能应答"(对齐 hasWindow)。 */
|
|
4
|
+
get size() {
|
|
5
|
+
return this.clients.size;
|
|
6
|
+
}
|
|
7
|
+
attach(res) {
|
|
8
|
+
res.writeHead(200, {
|
|
9
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
10
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
11
|
+
Connection: 'keep-alive',
|
|
12
|
+
'X-Accel-Buffering': 'no',
|
|
13
|
+
});
|
|
14
|
+
res.write(': connected\n\n');
|
|
15
|
+
this.clients.add(res);
|
|
16
|
+
res.on('close', () => this.clients.delete(res));
|
|
17
|
+
}
|
|
18
|
+
broadcast(channel, payload) {
|
|
19
|
+
if (this.clients.size === 0)
|
|
20
|
+
return;
|
|
21
|
+
const frame = `event: ${channel}\ndata: ${JSON.stringify(payload ?? null)}\n\n`;
|
|
22
|
+
for (const res of this.clients) {
|
|
23
|
+
try {
|
|
24
|
+
res.write(frame);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
this.clients.delete(res);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-dialect command adaptation for Windows PowerShell 5.1.
|
|
3
|
+
*
|
|
4
|
+
* LLM 生成的命令经常混用 cmd / bash 语法(`&&`、`||`、`%VAR%`),而我们在
|
|
5
|
+
* Windows 上默认用 `powershell.exe`(= Windows PowerShell 5.1,**不支持**
|
|
6
|
+
* Bash 风格的 `&&` / `||` 链接符,也不认识 `%VAR%` 环境变量语法)。
|
|
7
|
+
* 与其指望模型每次都写对方言,不如在执行前做一次确定性转换:
|
|
8
|
+
*
|
|
9
|
+
* - `%VAR%` → `$env:VAR`
|
|
10
|
+
* - `A && B` → `A; if ($?) { B }`
|
|
11
|
+
* - `A || B` → `A; if (-not $?) { B }`
|
|
12
|
+
* - `A && B && C` → `A; if ($?) { B; if ($?) { C } }`(从右往左嵌套)
|
|
13
|
+
*
|
|
14
|
+
* 引号内的 `&&` / `||` / `%VAR%` 不会被改写(按单双引号做词法扫描)。
|
|
15
|
+
* 转换是幂等的:已经是纯 PowerShell 语法的命令原样返回。
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* 把混用 cmd / bash 语法的命令转换成 Windows PowerShell 5.1 可执行的形式。
|
|
19
|
+
* 对已经合法的 PowerShell 命令是 no-op。
|
|
20
|
+
*/
|
|
21
|
+
export declare function adaptCommandForPowerShell(command: string): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-dialect command adaptation for Windows PowerShell 5.1.
|
|
3
|
+
*
|
|
4
|
+
* LLM 生成的命令经常混用 cmd / bash 语法(`&&`、`||`、`%VAR%`),而我们在
|
|
5
|
+
* Windows 上默认用 `powershell.exe`(= Windows PowerShell 5.1,**不支持**
|
|
6
|
+
* Bash 风格的 `&&` / `||` 链接符,也不认识 `%VAR%` 环境变量语法)。
|
|
7
|
+
* 与其指望模型每次都写对方言,不如在执行前做一次确定性转换:
|
|
8
|
+
*
|
|
9
|
+
* - `%VAR%` → `$env:VAR`
|
|
10
|
+
* - `A && B` → `A; if ($?) { B }`
|
|
11
|
+
* - `A || B` → `A; if (-not $?) { B }`
|
|
12
|
+
* - `A && B && C` → `A; if ($?) { B; if ($?) { C } }`(从右往左嵌套)
|
|
13
|
+
*
|
|
14
|
+
* 引号内的 `&&` / `||` / `%VAR%` 不会被改写(按单双引号做词法扫描)。
|
|
15
|
+
* 转换是幂等的:已经是纯 PowerShell 语法的命令原样返回。
|
|
16
|
+
*/
|
|
17
|
+
/** 把 cmd 风格的 `%VAR%` 环境变量替换成 PowerShell 的 `$env:VAR`。 */
|
|
18
|
+
function replaceCmdEnvVars(command) {
|
|
19
|
+
let out = '';
|
|
20
|
+
let quote = null;
|
|
21
|
+
for (let i = 0; i < command.length; i++) {
|
|
22
|
+
const ch = command[i];
|
|
23
|
+
if (quote) {
|
|
24
|
+
out += ch;
|
|
25
|
+
if (ch === quote)
|
|
26
|
+
quote = null;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (ch === '"' || ch === "'") {
|
|
30
|
+
quote = ch;
|
|
31
|
+
out += ch;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (ch === '%') {
|
|
35
|
+
const m = command.slice(i).match(/^%([A-Za-z_][A-Za-z0-9_]*)%/);
|
|
36
|
+
if (m) {
|
|
37
|
+
out += `$env:${m[1]}`;
|
|
38
|
+
i += m[0].length - 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
out += ch;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/** 按引号感知拆分顶层 `&&` / `||` 链。没有链接符时返回 null。 */
|
|
47
|
+
function splitChain(command) {
|
|
48
|
+
const parts = [];
|
|
49
|
+
const ops = [];
|
|
50
|
+
let current = '';
|
|
51
|
+
let quote = null;
|
|
52
|
+
for (let i = 0; i < command.length; i++) {
|
|
53
|
+
const ch = command[i];
|
|
54
|
+
if (quote) {
|
|
55
|
+
current += ch;
|
|
56
|
+
if (ch === quote)
|
|
57
|
+
quote = null;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === '"' || ch === "'") {
|
|
61
|
+
quote = ch;
|
|
62
|
+
current += ch;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const two = command.slice(i, i + 2);
|
|
66
|
+
if (two === '&&' || two === '||') {
|
|
67
|
+
parts.push(current);
|
|
68
|
+
ops.push(two);
|
|
69
|
+
current = '';
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
current += ch;
|
|
74
|
+
}
|
|
75
|
+
parts.push(current);
|
|
76
|
+
if (ops.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
const trimmed = parts.map(p => p.trim());
|
|
79
|
+
// 任一段为空(如 "foo &&" 结尾)说明命令本身畸形,不做改写。
|
|
80
|
+
if (trimmed.some(p => p.length === 0))
|
|
81
|
+
return null;
|
|
82
|
+
return { parts: trimmed, ops };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 把混用 cmd / bash 语法的命令转换成 Windows PowerShell 5.1 可执行的形式。
|
|
86
|
+
* 对已经合法的 PowerShell 命令是 no-op。
|
|
87
|
+
*/
|
|
88
|
+
export function adaptCommandForPowerShell(command) {
|
|
89
|
+
if (!command)
|
|
90
|
+
return command;
|
|
91
|
+
let out = replaceCmdEnvVars(command);
|
|
92
|
+
const chain = splitChain(out);
|
|
93
|
+
if (chain) {
|
|
94
|
+
const { parts, ops } = chain;
|
|
95
|
+
// 从右往左嵌套构造,保持 && / || 的短路语义。
|
|
96
|
+
let acc = parts[parts.length - 1];
|
|
97
|
+
for (let i = ops.length - 1; i >= 0; i--) {
|
|
98
|
+
const cond = ops[i] === '&&' ? '$?' : '-not $?';
|
|
99
|
+
acc = `${parts[i]}; if (${cond}) { ${acc} }`;
|
|
100
|
+
}
|
|
101
|
+
out = acc;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|