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.
Files changed (37) hide show
  1. package/README.md +9 -4
  2. package/dist/adapter/OpencodeSdkAdapter.d.ts +13 -1
  3. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
  4. package/dist/adapter/OpencodeSdkAdapter.js +56 -4
  5. package/dist/adapter/OpencodeSdkAdapter.test.js +57 -1
  6. package/dist/config.d.ts +14 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +103 -4
  9. package/dist/config.test.d.ts +2 -0
  10. package/dist/config.test.d.ts.map +1 -0
  11. package/dist/config.test.js +95 -0
  12. package/dist/routes/file-browser.d.ts +10 -0
  13. package/dist/routes/file-browser.d.ts.map +1 -1
  14. package/dist/routes/file-browser.js +206 -4
  15. package/dist/routes/file-browser.test.js +22 -0
  16. package/dist/routes/instance.d.ts +15 -0
  17. package/dist/routes/instance.d.ts.map +1 -1
  18. package/dist/routes/instance.js +25 -2
  19. package/dist/routes/instance.test.js +19 -0
  20. package/dist/services/aws-client-agent-mcp.test.js +1 -0
  21. package/dist/services/runtime-binding.d.ts.map +1 -1
  22. package/dist/services/runtime-binding.js +10 -3
  23. package/dist/services/runtime-binding.test.js +13 -1
  24. package/dist/services/session-output.d.ts +12 -0
  25. package/dist/services/session-output.d.ts.map +1 -1
  26. package/dist/services/session-output.js +15 -0
  27. package/dist/services/workspace-files.d.ts +72 -0
  28. package/dist/services/workspace-files.d.ts.map +1 -1
  29. package/dist/services/workspace-files.js +519 -21
  30. package/dist/services/workspace-files.test.js +387 -11
  31. package/dist/services/workspace-watch.d.ts +21 -0
  32. package/dist/services/workspace-watch.d.ts.map +1 -0
  33. package/dist/services/workspace-watch.js +123 -0
  34. package/dist/services/workspace-watch.test.d.ts +2 -0
  35. package/dist/services/workspace-watch.test.d.ts.map +1 -0
  36. package/dist/services/workspace-watch.test.js +38 -0
  37. 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、Docker 后台启动)不会阻塞等待输入,也会自动生成随机 `connectionKey` 并跳过引导。
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 地址 | `http://localhost:8080` |
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
- 生产环境中,`AWS_RUNTIME_SCHEDULER_BASE_URL` 必须填写机器实例可访问的 `aws-mcp-server` 地址,不能使用容器内的 `localhost`。
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 { BaseProviderAdapter, AdapterSessionConfig, ConversationMessage, SessionStatus } from './types.js';
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;AASH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EAGpB,mBAAmB,EACnB,aAAa,EACd,MAAM,YAAY,CAAC;AAsGpB,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;IAgI5E,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;IAOnG,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,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"}
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
- console.debug(`[OpencodeSdkAdapter] serve stdout: ${d.toString().slice(0, 200)}`);
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
- console.debug(`[OpencodeSdkAdapter] serve stderr: ${d.toString().slice(0, 200)}`);
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 this.waitForServer(client, config.workingDirectory, 10_000, port);
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
- await this.sendMessage(sessionId, config.initialPrompt);
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
- /** 调度器基础 URL */
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;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,WAAW;AACX,eAAO,MAAM,IAAI,EAAE,MAElB,CAAC;AAEF,gBAAgB;AAChB,eAAO,MAAM,gBAAgB,EAAE,MACwC,CAAC;AAExE,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"}
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 os from "node:os";
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
- /** 调度器基础 URL */
11
- export const schedulerBaseUrl = process.env.AWS_RUNTIME_SCHEDULER_BASE_URL || "http://localhost:8080";
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() || os.homedir();
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.test.d.ts.map
@@ -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;AAmBH,eAAO,MAAM,iBAAiB,4CAAW,CAAC"}
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"}