aws-runtime-bridge 1.3.9 → 1.5.0
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 +9 -4
- package/dist/adapter/OpencodeSdkAdapter.d.ts +13 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +56 -4
- package/dist/adapter/OpencodeSdkAdapter.test.js +57 -1
- package/dist/config.d.ts +14 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -4
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +95 -0
- package/dist/routes/file-browser.d.ts +10 -0
- package/dist/routes/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +206 -4
- package/dist/routes/file-browser.test.js +22 -0
- package/dist/routes/instance.d.ts +15 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +25 -2
- package/dist/routes/instance.test.js +19 -0
- package/dist/services/aws-client-agent-mcp.test.js +1 -0
- package/dist/services/runtime-binding.d.ts.map +1 -1
- package/dist/services/runtime-binding.js +10 -3
- package/dist/services/runtime-binding.test.js +13 -1
- package/dist/services/session-output.d.ts +12 -0
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +15 -0
- package/dist/services/workspace-files.d.ts +72 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +519 -21
- package/dist/services/workspace-files.test.js +387 -11
- package/dist/services/workspace-watch.d.ts +21 -0
- package/dist/services/workspace-watch.d.ts.map +1 -0
- package/dist/services/workspace-watch.js +123 -0
- package/dist/services/workspace-watch.test.d.ts +2 -0
- package/dist/services/workspace-watch.test.d.ts.map +1 -0
- package/dist/services/workspace-watch.test.js +38 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
AgentsWorkStudio 机器实例运行时桥接服务,用于在实例上管理 Agent 运行时、终端、配置与回调通信。
|
|
4
4
|
|
|
5
|
+
> `aws-runtime-bridge` 面向机器实例宿主机运行,需要访问本机工作区、终端、运行时配置和文件系统;不要将它作为 Docker/Compose 服务部署。Docker Compose 仅用于仓库根目录的 `aws-dashboard` 与 `aws-mcp-server` Web 服务。
|
|
6
|
+
|
|
5
7
|
## 全局安装
|
|
6
8
|
|
|
7
9
|
从 npm 包安装时:
|
|
@@ -23,7 +25,7 @@ npm install -g .
|
|
|
23
25
|
|
|
24
26
|
## 启动
|
|
25
27
|
|
|
26
|
-
首次运行 `awsb` / `aws-bridge` 时,如果不存在 `~/.aws-bridge/config.json`,CLI 会进入交互式配置引导;也可以在提示中选择跳过。跳过时仍会创建配置文件并自动生成随机 `connectionKey`,终端会输出该密钥,请保存后在 server/面板连接此 Bridge 时使用。非交互环境(如 systemd、CI
|
|
28
|
+
首次运行 `awsb` / `aws-bridge` 时,如果不存在 `~/.aws-bridge/config.json`,CLI 会进入交互式配置引导;也可以在提示中选择跳过。跳过时仍会创建配置文件并自动生成随机 `connectionKey`,终端会输出该密钥,请保存后在 server/面板连接此 Bridge 时使用。非交互环境(如 systemd、CI 后台启动)不会阻塞等待输入,也会自动生成随机 `connectionKey` 并跳过引导。
|
|
27
29
|
|
|
28
30
|
引导会生成类似下面的配置:
|
|
29
31
|
|
|
@@ -45,11 +47,14 @@ npm install -g .
|
|
|
45
47
|
|
|
46
48
|
```bash
|
|
47
49
|
AWS_RUNTIME_BRIDGE_PORT=18081 \
|
|
48
|
-
AWS_RUNTIME_SCHEDULER_BASE_URL=http://your-server-host:7380 \
|
|
49
50
|
AWS_RUNTIME_HOME_DIR=/opt/agentswork/runtime-home \
|
|
50
51
|
aws-bridge
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
当 `~/.aws-bridge/config.json` 只配置了一个 `autoRegisterTargets[].serverUrl` 时,bridge 会自动将该地址作为 `/runtime/ping` 回连调度中心的地址,无需重复配置 `AWS_RUNTIME_SCHEDULER_BASE_URL`。如需显式覆盖,或同一个 bridge 配置了多个自动注册目标,请设置 `AWS_RUNTIME_SCHEDULER_BASE_URL` 指定当前实例测试连接时应回连的调度中心。
|
|
55
|
+
|
|
56
|
+
If an existing runtime binding still stores an old scheduler URL such as `http://127.0.0.1:8080`, re-run auto-register, refresh the runtime token, or re-pair after changing `serverUrl` / `AWS_RUNTIME_SCHEDULER_BASE_URL`; bridge will not reuse a token issued for the old scheduler URL against the new scheduler URL.
|
|
57
|
+
|
|
53
58
|
`aws-runtime-bridge` 命令仍作为兼容别名保留。安装 `aws-runtime-bridge` 后,包内会随附
|
|
54
59
|
`aws-client-agent-mcp` 的编译产物;bridge 启动时只负责准备该 MCP 产物,不再在 Agent 启动时默认动态注入 `aws-mcp`。
|
|
55
60
|
|
|
@@ -84,8 +89,8 @@ sudo awsb service uninstall
|
|
|
84
89
|
| 变量名 | 说明 | 默认值 |
|
|
85
90
|
| --- | --- | --- |
|
|
86
91
|
| `AWS_RUNTIME_BRIDGE_PORT` | Bridge HTTP 端口 | `18081` |
|
|
87
|
-
| `AWS_RUNTIME_SCHEDULER_BASE_URL` | aws-mcp-server
|
|
92
|
+
| `AWS_RUNTIME_SCHEDULER_BASE_URL` | aws-mcp-server 地址;显式配置优先级最高,未配置且只有一个 `autoRegisterTargets[].serverUrl` 时自动使用该地址 | 单目标自动注册地址;否则 `http://localhost:8080` |
|
|
88
93
|
| `AWS_RUNTIME_HOME_DIR` | Bridge 管理配置与状态的主目录 | 当前用户 Home |
|
|
89
94
|
| `AWS_RUNTIME_CORS_ORIGINS` | 允许访问 bridge 的来源,逗号分隔 | 本地开发地址 |
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
生产环境中,bridge 最终解析出的调度中心地址必须是机器实例可访问的 `aws-mcp-server` 地址,不能使用不可达的容器内 `localhost`。如果配置了多个 `autoRegisterTargets`,bridge 不会静默选择第一个目标,需通过 `AWS_RUNTIME_SCHEDULER_BASE_URL` 或后续多调度中心身份路由明确目标。
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 参考:spectrai-community/src/main/adapter/OpenCodeSdkAdapter.ts
|
|
12
12
|
*/
|
|
13
13
|
import { EventEmitter } from 'node:events';
|
|
14
|
-
import type {
|
|
14
|
+
import type { AdapterSessionConfig, BaseProviderAdapter, ConversationMessage, SessionStatus } from './types.js';
|
|
15
15
|
export declare class OpencodeSdkAdapter extends EventEmitter implements BaseProviderAdapter {
|
|
16
16
|
readonly providerId = "opencode";
|
|
17
17
|
readonly displayName = "OpenCode";
|
|
@@ -39,6 +39,18 @@ export declare class OpencodeSdkAdapter extends EventEmitter implements BaseProv
|
|
|
39
39
|
*/
|
|
40
40
|
private loadSdk;
|
|
41
41
|
private waitForServer;
|
|
42
|
+
/**
|
|
43
|
+
* 记录 OpenCode serve 启动阶段输出,用于进程提前退出时返回可诊断错误。
|
|
44
|
+
*/
|
|
45
|
+
private recordStartupOutput;
|
|
46
|
+
/**
|
|
47
|
+
* 将 OpenCode serve 提前退出信息转成前端可直接展示的错误文本。
|
|
48
|
+
*/
|
|
49
|
+
private formatStartupExitMessage;
|
|
50
|
+
/**
|
|
51
|
+
* 异步投递启动提示,避免首轮 OpenCode 处理耗时导致 /runtime/start 被前端误判为启动失败。
|
|
52
|
+
*/
|
|
53
|
+
private dispatchInitialPrompt;
|
|
42
54
|
private startSseLoop;
|
|
43
55
|
private runSseLoop;
|
|
44
56
|
private handleOpenCodeEvent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpencodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/OpencodeSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"OpencodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/OpencodeSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAc3C,OAAO,KAAK,EACV,oBAAoB,EAEpB,mBAAmB,EACnB,mBAAmB,EAEnB,aAAa,EACd,MAAM,YAAY,CAAC;AA4GpB,qBAAa,kBAAmB,SAAQ,YAAa,YAAW,mBAAmB;IACjF,QAAQ,CAAC,UAAU,cAAc;IACjC,QAAQ,CAAC,WAAW,cAAc;IAElC,OAAO,CAAC,QAAQ,CAA2C;IAC3D,OAAO,CAAC,SAAS,CAAC,CAAoB;IAEhC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IA+I5E,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwC9D,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBnE,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7F,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE;QAAE,gBAAgB,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAUnG,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAclD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BxD,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,EAAE;IAIzD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAItC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI3D,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI9D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAOpD,OAAO,IAAI,IAAI;IAaf;;;OAGG;YACW,OAAO;YA6BP,aAAa;IAmB3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAahC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,YAAY;YAYN,UAAU;IAuBxB,OAAO,CAAC,mBAAmB;IA6H3B,OAAO,CAAC,gBAAgB;IA6GxB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,2BAA2B;CAOpC"}
|
|
@@ -132,11 +132,16 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
132
132
|
shell: useShell,
|
|
133
133
|
});
|
|
134
134
|
session.serverProcess = proc;
|
|
135
|
+
session.startupOutput = [];
|
|
135
136
|
proc.stdout?.on('data', (d) => {
|
|
136
|
-
|
|
137
|
+
const text = d.toString();
|
|
138
|
+
this.recordStartupOutput(session, text);
|
|
139
|
+
console.debug(`[OpencodeSdkAdapter] serve stdout: ${text.slice(0, 200)}`);
|
|
137
140
|
});
|
|
138
141
|
proc.stderr?.on('data', (d) => {
|
|
139
|
-
|
|
142
|
+
const text = d.toString();
|
|
143
|
+
this.recordStartupOutput(session, text);
|
|
144
|
+
console.debug(`[OpencodeSdkAdapter] serve stderr: ${text.slice(0, 200)}`);
|
|
140
145
|
});
|
|
141
146
|
proc.on('error', (err) => {
|
|
142
147
|
console.error(`[OpencodeSdkAdapter] Server process error:`, err);
|
|
@@ -155,6 +160,9 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
155
160
|
console.log(`[OpencodeSdkAdapter] Server exited with code ${code}`);
|
|
156
161
|
session.sseActive = false;
|
|
157
162
|
});
|
|
163
|
+
const exitDuringStartup = new Promise((resolve) => {
|
|
164
|
+
proc.once('exit', (code, signal) => resolve({ code, signal }));
|
|
165
|
+
});
|
|
158
166
|
// 步骤 3:创建 SDK 客户端
|
|
159
167
|
const sdk = await this.loadSdk();
|
|
160
168
|
const client = sdk.createOpencodeClient({
|
|
@@ -162,7 +170,12 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
162
170
|
});
|
|
163
171
|
session.client = client;
|
|
164
172
|
// 步骤 4:等待服务器就绪
|
|
165
|
-
await
|
|
173
|
+
await Promise.race([
|
|
174
|
+
this.waitForServer(client, config.workingDirectory, 10_000, port),
|
|
175
|
+
exitDuringStartup.then((exitInfo) => {
|
|
176
|
+
throw new Error(this.formatStartupExitMessage(exitInfo, session.startupOutput));
|
|
177
|
+
}),
|
|
178
|
+
]);
|
|
166
179
|
console.log(`[OpencodeSdkAdapter] ★★★ OpenCode server ready ★★★ port=${port}`);
|
|
167
180
|
// 步骤 5:创建 OpenCode 会话
|
|
168
181
|
const createResult = await client.session.create({
|
|
@@ -182,7 +195,7 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
182
195
|
this.emit('status-change', sessionId, 'waiting_input');
|
|
183
196
|
// 步骤 7:发送初始 Prompt
|
|
184
197
|
if (config.initialPrompt) {
|
|
185
|
-
|
|
198
|
+
this.dispatchInitialPrompt(sessionId, config.initialPrompt);
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
201
|
catch (err) {
|
|
@@ -264,6 +277,9 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
264
277
|
const session = this.sessions.get(sessionId);
|
|
265
278
|
if (session) {
|
|
266
279
|
session.idleCommands = commands;
|
|
280
|
+
if (session.adapterSession.status === 'waiting_input' && (commands.idleInputCommand || commands.nonInputCommand)) {
|
|
281
|
+
this.startIdleDetection(sessionId);
|
|
282
|
+
}
|
|
267
283
|
}
|
|
268
284
|
}
|
|
269
285
|
async abortCurrentTurn(sessionId) {
|
|
@@ -382,6 +398,42 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
382
398
|
}
|
|
383
399
|
throw new Error(`OpenCode server (port ${port}) did not start within ${timeoutMs}ms`);
|
|
384
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* 记录 OpenCode serve 启动阶段输出,用于进程提前退出时返回可诊断错误。
|
|
403
|
+
*/
|
|
404
|
+
recordStartupOutput(session, text) {
|
|
405
|
+
const normalized = text.trim();
|
|
406
|
+
if (!normalized)
|
|
407
|
+
return;
|
|
408
|
+
session.startupOutput = [...(session.startupOutput ?? []), normalized].slice(-8);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 将 OpenCode serve 提前退出信息转成前端可直接展示的错误文本。
|
|
412
|
+
*/
|
|
413
|
+
formatStartupExitMessage(exitInfo, startupOutput) {
|
|
414
|
+
const exitReason = exitInfo.signal
|
|
415
|
+
? `signal ${exitInfo.signal}`
|
|
416
|
+
: `code ${exitInfo.code ?? 'unknown'}`;
|
|
417
|
+
const output = (startupOutput ?? []).join('\n').trim();
|
|
418
|
+
if (!output) {
|
|
419
|
+
return `OpenCode server exited during startup (${exitReason}). Please check whether opencode serve can run in this workspace.`;
|
|
420
|
+
}
|
|
421
|
+
return `OpenCode server exited during startup (${exitReason}): ${output}`;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* 异步投递启动提示,避免首轮 OpenCode 处理耗时导致 /runtime/start 被前端误判为启动失败。
|
|
425
|
+
*/
|
|
426
|
+
dispatchInitialPrompt(sessionId, initialPrompt) {
|
|
427
|
+
void this.sendMessage(sessionId, initialPrompt).catch((err) => {
|
|
428
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
429
|
+
this.emitEvent({
|
|
430
|
+
type: 'error',
|
|
431
|
+
sessionId,
|
|
432
|
+
timestamp: new Date().toISOString(),
|
|
433
|
+
data: { text: `OpenCode initial prompt failed: ${msg}` },
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
385
437
|
startSseLoop(sessionId, session) {
|
|
386
438
|
session.sseActive = true;
|
|
387
439
|
const ac = new AbortController();
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { OpencodeSdkAdapter } from './OpencodeSdkAdapter.js';
|
|
3
|
+
afterEach(() => {
|
|
4
|
+
vi.useRealTimers();
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
3
7
|
describe('OpencodeSdkAdapter', () => {
|
|
4
8
|
it('should expose opencode provider identity', () => {
|
|
5
9
|
const adapter = new OpencodeSdkAdapter();
|
|
@@ -16,4 +20,56 @@ describe('OpencodeSdkAdapter', () => {
|
|
|
16
20
|
const prompt = Reflect.get(adapter, 'toIdlePrompt').call(adapter, 'system: 醒来了吗?使用get_profile获取自己的信息,继续未完成的任务或使用poll_message阻塞获取消息');
|
|
17
21
|
expect(prompt).toBe('请调用 poll_message 工具阻塞等待新消息;收到消息后再处理消息内容。');
|
|
18
22
|
});
|
|
23
|
+
it('starts idle detection when commands are set after OpenCode is already waiting for input', async () => {
|
|
24
|
+
vi.useFakeTimers();
|
|
25
|
+
const adapter = new OpencodeSdkAdapter();
|
|
26
|
+
const sentMessages = [];
|
|
27
|
+
Reflect.set(adapter, 'sessions', new Map([
|
|
28
|
+
[
|
|
29
|
+
'session-1',
|
|
30
|
+
{
|
|
31
|
+
adapterSession: {
|
|
32
|
+
sessionId: 'session-1',
|
|
33
|
+
status: 'waiting_input',
|
|
34
|
+
messages: [],
|
|
35
|
+
createdAt: new Date().toISOString(),
|
|
36
|
+
totalUsage: { inputTokens: 0, outputTokens: 0 },
|
|
37
|
+
},
|
|
38
|
+
config: {
|
|
39
|
+
command: 'opencode',
|
|
40
|
+
workingDirectory: process.cwd(),
|
|
41
|
+
autoAccept: true,
|
|
42
|
+
},
|
|
43
|
+
sseActive: false,
|
|
44
|
+
pendingPermissions: new Map(),
|
|
45
|
+
emittedToolStarts: new Set(),
|
|
46
|
+
workingDirectory: process.cwd(),
|
|
47
|
+
userMessageIds: new Set(),
|
|
48
|
+
currentAssistantText: '',
|
|
49
|
+
seenTextPartIds: new Set(),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
]));
|
|
53
|
+
vi.spyOn(adapter, 'sendMessage').mockImplementation(async (_sessionId, message) => {
|
|
54
|
+
sentMessages.push(message);
|
|
55
|
+
});
|
|
56
|
+
adapter.setIdleCommands('session-1', {
|
|
57
|
+
idleInputCommand: 'system: 使用poll_message阻塞获取消息',
|
|
58
|
+
nonInputCommand: '',
|
|
59
|
+
});
|
|
60
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
61
|
+
expect(sentMessages).toEqual(['请调用 poll_message 工具阻塞等待新消息;收到消息后再处理消息内容。']);
|
|
62
|
+
});
|
|
63
|
+
it('includes serve startup output when the OpenCode process exits early', () => {
|
|
64
|
+
const adapter = new OpencodeSdkAdapter();
|
|
65
|
+
const message = Reflect.get(adapter, 'formatStartupExitMessage').call(adapter, { code: 1, signal: null }, ['missing auth configuration']);
|
|
66
|
+
expect(message).toContain('OpenCode server exited during startup (code 1)');
|
|
67
|
+
expect(message).toContain('missing auth configuration');
|
|
68
|
+
});
|
|
69
|
+
it('dispatches the initial prompt asynchronously so launch can return after session creation', () => {
|
|
70
|
+
const adapter = new OpencodeSdkAdapter();
|
|
71
|
+
const sendMessage = vi.spyOn(adapter, 'sendMessage').mockResolvedValue(undefined);
|
|
72
|
+
Reflect.get(adapter, 'dispatchInitialPrompt').call(adapter, 'session-1', 'hello');
|
|
73
|
+
expect(sendMessage).toHaveBeenCalledWith('session-1', 'hello');
|
|
74
|
+
});
|
|
19
75
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -5,7 +5,20 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** 服务端口 */
|
|
7
7
|
export declare const port: number;
|
|
8
|
-
/**
|
|
8
|
+
/** 默认调度器基础 URL */
|
|
9
|
+
export declare const DEFAULT_SCHEDULER_BASE_URL = "http://localhost:8080";
|
|
10
|
+
export type SchedulerBaseUrlSource = "env" | "single-auto-register-target" | "default" | "ambiguous-auto-register-targets";
|
|
11
|
+
export interface SchedulerBaseUrlResolution {
|
|
12
|
+
url: string;
|
|
13
|
+
source: SchedulerBaseUrlSource;
|
|
14
|
+
ambiguousTargetUrls: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 解析调度中心基础 URL。
|
|
18
|
+
* 主流程:显式环境变量优先;未配置时,单个自动注册目标可作为宿主机部署的零额外配置回退;多目标不静默取第一个。
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveSchedulerBaseUrl(): SchedulerBaseUrlResolution;
|
|
21
|
+
/** 调度器基础 URL(兼容旧导入;请求处理应优先调用 resolveSchedulerBaseUrl 获取最新值) */
|
|
9
22
|
export declare const schedulerBaseUrl: string;
|
|
10
23
|
/** Node 环境 */
|
|
11
24
|
export declare const nodeEnv: string;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,WAAW;AACX,eAAO,MAAM,IAAI,EAAE,MAElB,CAAC;AAEF,kBAAkB;AAClB,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AAElE,MAAM,MAAM,sBAAsB,GAC9B,KAAK,GACL,6BAA6B,GAC7B,SAAS,GACT,iCAAiC,CAAC;AAEtC,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,sBAAsB,CAAC;IAC/B,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B;AA+ED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,0BAA0B,CAiCpE;AAED,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,EAAE,MAEH,CAAC;AAE7B,cAAc;AACd,eAAO,MAAM,OAAO,EAAE,MAA8C,CAAC;AAErE,oCAAoC;AACpC,eAAO,MAAM,oBAAoB,EAAE,OACiB,CAAC;AAErD,8BAA8B;AAC9B,eAAO,MAAM,kBAAkB,EAAE,MAAM,EAMC,CAAC;AAEzC,gEAAgE;AAChE,wBAAgB,uBAAuB,IAAI,IAAI,CAAG;AAElD,eAAe;AACf,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,8CAA8C;AAC9C,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,2BAA2B;AAC3B,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKlE;AAED,0BAA0B;AAC1B,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,yBAAyB;AACzB,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED,sBAAsB;AACtB,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,yBAAyB;AACzB,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,4BAA4B;AAC5B,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;GAEG;AAEH,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,sBAAsB,EACrB,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1C,eAAe;AACf,eAAO,MAAM,mBAAmB,SACiB,CAAC;AAElD,iCAAiC;AACjC,eAAO,MAAM,2BAA2B,QAEvC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,oBAAoB,QAEhC,CAAC;AAEF,qCAAqC;AACrC,eAAO,MAAM,uBAAuB,QAEnC,CAAC;AAEF;;GAEG;AAEH,eAAe;AACf,eAAO,MAAM,qBAAqB,SAA2C,CAAC;AAE9E,YAAY;AACZ,eAAO,MAAM,uBAAuB,QAAkC,CAAC;AAEvE,iBAAiB;AACjB,eAAO,MAAM,sBAAsB,QAAiC,CAAC;AAErE,oBAAoB;AACpB,eAAO,MAAM,2BAA2B,QAAsC,CAAC;AAE/E,WAAW;AACX,eAAO,MAAM,0BAA0B,QAAqC,CAAC;AAE7E,WAAW;AACX,eAAO,MAAM,uBAAuB,QAAkC,CAAC;AAEvE,aAAa;AACb,eAAO,MAAM,4BAA4B,QACH,CAAC;AAEvC,kCAAkC;AAClC,eAAO,MAAM,wBAAwB,QAC4B,CAAC;AAElE,aAAa;AACb,eAAO,MAAM,yBAAyB,QAErC,CAAC;AAEF,iBAAiB;AACjB,eAAO,MAAM,4BAA4B,QAExC,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -3,12 +3,111 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 集中管理所有环境变量和配置常量
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
|
+
import fs from "node:fs";
|
|
8
9
|
/** 服务端口 */
|
|
9
10
|
export const port = Number(process.env.AWS_RUNTIME_BRIDGE_PORT || 18081);
|
|
10
|
-
/**
|
|
11
|
-
export const
|
|
11
|
+
/** 默认调度器基础 URL */
|
|
12
|
+
export const DEFAULT_SCHEDULER_BASE_URL = "http://localhost:8080";
|
|
13
|
+
function normalizeOptionalString(value) {
|
|
14
|
+
if (value == null) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const normalized = String(value).trim();
|
|
18
|
+
return normalized || undefined;
|
|
19
|
+
}
|
|
20
|
+
function normalizeSchedulerHttpBaseUrl(value) {
|
|
21
|
+
const raw = normalizeOptionalString(value);
|
|
22
|
+
if (!raw) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const repaired = raw.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i, "$1");
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(repaired);
|
|
28
|
+
if (url.protocol === "ws:") {
|
|
29
|
+
url.protocol = "http:";
|
|
30
|
+
}
|
|
31
|
+
else if (url.protocol === "wss:") {
|
|
32
|
+
url.protocol = "https:";
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
url.protocol = url.protocol.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
url.hostname = url.hostname.toLowerCase();
|
|
38
|
+
return url.origin;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return repaired.replace(/\/+$/, "");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getAutoRegisterConfigFilePath() {
|
|
45
|
+
const testHomeDir = String(process.env.AWS_TEST_HOME || "").trim();
|
|
46
|
+
return path.join(testHomeDir || homedir(), ".aws-bridge", "config.json");
|
|
47
|
+
}
|
|
48
|
+
function readAutoRegisterTargetUrls() {
|
|
49
|
+
const configPath = getAutoRegisterConfigFilePath();
|
|
50
|
+
try {
|
|
51
|
+
if (!fs.existsSync(configPath)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
55
|
+
const rawTargets = Array.isArray(parsed.autoRegisterTargets)
|
|
56
|
+
? parsed.autoRegisterTargets
|
|
57
|
+
: [];
|
|
58
|
+
const targetUrls = rawTargets
|
|
59
|
+
.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item)))
|
|
60
|
+
.map((target) => normalizeSchedulerHttpBaseUrl(target.serverUrl) ||
|
|
61
|
+
normalizeSchedulerHttpBaseUrl(target.schedulerBaseUrl))
|
|
62
|
+
.filter((url) => Boolean(url));
|
|
63
|
+
if (targetUrls.length > 0) {
|
|
64
|
+
return Array.from(new Set(targetUrls));
|
|
65
|
+
}
|
|
66
|
+
const topLevelUrl = normalizeSchedulerHttpBaseUrl(parsed.serverUrl) ||
|
|
67
|
+
normalizeSchedulerHttpBaseUrl(parsed.schedulerBaseUrl);
|
|
68
|
+
return topLevelUrl ? [topLevelUrl] : [];
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 解析调度中心基础 URL。
|
|
76
|
+
* 主流程:显式环境变量优先;未配置时,单个自动注册目标可作为宿主机部署的零额外配置回退;多目标不静默取第一个。
|
|
77
|
+
*/
|
|
78
|
+
export function resolveSchedulerBaseUrl() {
|
|
79
|
+
const envSchedulerBaseUrl = normalizeSchedulerHttpBaseUrl(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL);
|
|
80
|
+
if (envSchedulerBaseUrl) {
|
|
81
|
+
return {
|
|
82
|
+
url: envSchedulerBaseUrl,
|
|
83
|
+
source: "env",
|
|
84
|
+
ambiguousTargetUrls: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const autoRegisterTargetUrls = readAutoRegisterTargetUrls();
|
|
88
|
+
if (autoRegisterTargetUrls.length === 1) {
|
|
89
|
+
return {
|
|
90
|
+
url: autoRegisterTargetUrls[0] || DEFAULT_SCHEDULER_BASE_URL,
|
|
91
|
+
source: "single-auto-register-target",
|
|
92
|
+
ambiguousTargetUrls: [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (autoRegisterTargetUrls.length > 1) {
|
|
96
|
+
return {
|
|
97
|
+
url: DEFAULT_SCHEDULER_BASE_URL,
|
|
98
|
+
source: "ambiguous-auto-register-targets",
|
|
99
|
+
ambiguousTargetUrls: autoRegisterTargetUrls,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
url: DEFAULT_SCHEDULER_BASE_URL,
|
|
104
|
+
source: "default",
|
|
105
|
+
ambiguousTargetUrls: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** 调度器基础 URL(兼容旧导入;请求处理应优先调用 resolveSchedulerBaseUrl 获取最新值) */
|
|
109
|
+
export const schedulerBaseUrl = normalizeSchedulerHttpBaseUrl(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL) ||
|
|
110
|
+
DEFAULT_SCHEDULER_BASE_URL;
|
|
12
111
|
/** Node 环境 */
|
|
13
112
|
export const nodeEnv = process.env.NODE_ENV || "development";
|
|
14
113
|
/** 是否允许浏览宿主机任意目录(默认关闭,仅建议本地排障启用) */
|
|
@@ -23,7 +122,7 @@ export const allowedCorsOrigins = String(process.env.AWS_RUNTIME_CORS_ORIGINS ||
|
|
|
23
122
|
export function validateProductionToken() { }
|
|
24
123
|
/** 获取运行时主目录 */
|
|
25
124
|
export function getRuntimeHomeDir() {
|
|
26
|
-
return String(process.env.AWS_RUNTIME_HOME_DIR || "").trim() ||
|
|
125
|
+
return String(process.env.AWS_RUNTIME_HOME_DIR || "").trim() || homedir();
|
|
27
126
|
}
|
|
28
127
|
/** 获取 Claude 配置文件路径(AI 配置写入 settings.json) */
|
|
29
128
|
export function getClaudeConfigFile(runtimeHome) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../src/config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
vi.mock("node:os", async () => {
|
|
6
|
+
const actual = await vi.importActual("node:os");
|
|
7
|
+
const mocked = {
|
|
8
|
+
...actual,
|
|
9
|
+
homedir: () => process.env.AWS_TEST_HOME || actual.homedir(),
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
...mocked,
|
|
13
|
+
default: mocked,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
const originalEnv = { ...process.env };
|
|
17
|
+
const tempRoots = [];
|
|
18
|
+
function createRuntimeHome() {
|
|
19
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "aws-config-"));
|
|
20
|
+
tempRoots.push(root);
|
|
21
|
+
return root;
|
|
22
|
+
}
|
|
23
|
+
function useRuntimeHome(runtimeHome) {
|
|
24
|
+
process.env.AWS_TEST_HOME = runtimeHome;
|
|
25
|
+
process.env.HOME = runtimeHome;
|
|
26
|
+
process.env.USERPROFILE = runtimeHome;
|
|
27
|
+
process.env.AWS_RUNTIME_HOME_DIR = runtimeHome;
|
|
28
|
+
delete process.env.AWS_RUNTIME_SCHEDULER_BASE_URL;
|
|
29
|
+
}
|
|
30
|
+
function writeBridgeConfig(runtimeHome, autoRegisterTargets) {
|
|
31
|
+
const configPath = path.join(runtimeHome, ".aws-bridge", "config.json");
|
|
32
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
33
|
+
writeFileSync(configPath, `${JSON.stringify({ connectionKey: "bridge-key", autoRegisterTargets }, null, 2)}\n`, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
process.env = { ...originalEnv };
|
|
37
|
+
vi.resetModules();
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
for (const root of tempRoots.splice(0)) {
|
|
41
|
+
rmSync(root, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
describe("scheduler base URL resolution", () => {
|
|
45
|
+
it("keeps explicit AWS_RUNTIME_SCHEDULER_BASE_URL as the highest priority", async () => {
|
|
46
|
+
const runtimeHome = createRuntimeHome();
|
|
47
|
+
useRuntimeHome(runtimeHome);
|
|
48
|
+
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = "http://explicit.local:7380/path";
|
|
49
|
+
writeBridgeConfig(runtimeHome, [{ serverUrl: "http://target.local:7380" }]);
|
|
50
|
+
const { resolveSchedulerBaseUrl } = await import("./config.js");
|
|
51
|
+
expect(resolveSchedulerBaseUrl()).toEqual({
|
|
52
|
+
url: "http://explicit.local:7380",
|
|
53
|
+
source: "env",
|
|
54
|
+
ambiguousTargetUrls: [],
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it("uses a single auto-register target serverUrl when scheduler env is absent", async () => {
|
|
58
|
+
const runtimeHome = createRuntimeHome();
|
|
59
|
+
useRuntimeHome(runtimeHome);
|
|
60
|
+
writeBridgeConfig(runtimeHome, [{ serverUrl: "http://127.0.0.1:7380" }]);
|
|
61
|
+
const { resolveSchedulerBaseUrl } = await import("./config.js");
|
|
62
|
+
expect(resolveSchedulerBaseUrl()).toEqual({
|
|
63
|
+
url: "http://127.0.0.1:7380",
|
|
64
|
+
source: "single-auto-register-target",
|
|
65
|
+
ambiguousTargetUrls: [],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
it("does not silently select the first auto-register target when multiple URLs exist", async () => {
|
|
69
|
+
const runtimeHome = createRuntimeHome();
|
|
70
|
+
useRuntimeHome(runtimeHome);
|
|
71
|
+
writeBridgeConfig(runtimeHome, [
|
|
72
|
+
{ serverUrl: "http://scheduler-a.local:7380" },
|
|
73
|
+
{ serverUrl: "http://scheduler-b.local:7380" },
|
|
74
|
+
]);
|
|
75
|
+
const { DEFAULT_SCHEDULER_BASE_URL, resolveSchedulerBaseUrl } = await import("./config.js");
|
|
76
|
+
expect(resolveSchedulerBaseUrl()).toEqual({
|
|
77
|
+
url: DEFAULT_SCHEDULER_BASE_URL,
|
|
78
|
+
source: "ambiguous-auto-register-targets",
|
|
79
|
+
ambiguousTargetUrls: [
|
|
80
|
+
"http://scheduler-a.local:7380",
|
|
81
|
+
"http://scheduler-b.local:7380",
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it("falls back to the legacy localhost default when no scheduler source exists", async () => {
|
|
86
|
+
const runtimeHome = createRuntimeHome();
|
|
87
|
+
useRuntimeHome(runtimeHome);
|
|
88
|
+
const { DEFAULT_SCHEDULER_BASE_URL, resolveSchedulerBaseUrl } = await import("./config.js");
|
|
89
|
+
expect(resolveSchedulerBaseUrl()).toEqual({
|
|
90
|
+
url: DEFAULT_SCHEDULER_BASE_URL,
|
|
91
|
+
source: "default",
|
|
92
|
+
ambiguousTargetUrls: [],
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -3,5 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 提供文件系统浏览功能
|
|
5
5
|
*/
|
|
6
|
+
import multer from 'multer';
|
|
6
7
|
export declare const fileBrowserRouter: import("express-serve-static-core").Router;
|
|
8
|
+
export declare const WORKSPACE_UPLOAD_FILE_LIMIT = 2000;
|
|
9
|
+
export declare function createWorkspaceUploadLimitResponse(error: multer.MulterError): {
|
|
10
|
+
status: number;
|
|
11
|
+
body: {
|
|
12
|
+
error: string;
|
|
13
|
+
code: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export declare function parseWorkspaceUploadRelativePaths(body: Record<string, unknown>): string[];
|
|
7
17
|
//# sourceMappingURL=file-browser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-browser.d.ts","sourceRoot":"","sources":["../../src/routes/file-browser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"file-browser.d.ts","sourceRoot":"","sources":["../../src/routes/file-browser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAsB5B,eAAO,MAAM,iBAAiB,4CAAW,CAAC;AAE1C,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAqChD,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAUvI;AAED,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAgBzF"}
|