aws-runtime-bridge 1.3.7 → 1.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/ClaudeSdkAdapter.d.ts +12 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +114 -41
- package/dist/adapter/ClaudeSdkAdapter.test.js +146 -0
- package/dist/routes/instance.d.ts +13 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +22 -5
- package/dist/routes/instance.test.js +21 -0
- package/package.json +1 -1
|
@@ -92,6 +92,18 @@ export declare class ClaudeSdkAdapter extends EventEmitter implements BaseProvid
|
|
|
92
92
|
* 支持 Windows / macOS / Linux
|
|
93
93
|
*/
|
|
94
94
|
private findClaudeExecutable;
|
|
95
|
+
/**
|
|
96
|
+
* 从面板配置的命令行中提取实际可执行文件,避免把参数一起传给 PATH 查找。
|
|
97
|
+
*/
|
|
98
|
+
private extractExecutableCommand;
|
|
99
|
+
/**
|
|
100
|
+
* 按当前 Bridge 进程的 PATH/PATHEXT 直接查找可执行文件,避免依赖 which/where 和 shell 解析。
|
|
101
|
+
*/
|
|
102
|
+
private resolveExecutableFromPath;
|
|
103
|
+
private getWindowsPathExtensions;
|
|
104
|
+
private shouldUseDefaultClaudeFallbacks;
|
|
105
|
+
private isWindowsWrapperPath;
|
|
106
|
+
private resolveWindowsWrapper;
|
|
95
107
|
/**
|
|
96
108
|
* 从 wrapper 脚本解析 cli.js 路径
|
|
97
109
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClaudeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/ClaudeSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"ClaudeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/ClaudeSdkAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* - 支持 AskUserQuestion 拦截并转发到聊天面板
|
|
10
10
|
* - 支持 turn_complete 事件精确判断 AI 就绪状态
|
|
11
11
|
*/
|
|
12
|
+
import { execSync } from 'child_process';
|
|
12
13
|
import { EventEmitter } from 'events';
|
|
13
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import {
|
|
16
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
17
17
|
import { getToolActionInfo } from './types.js';
|
|
18
18
|
// ============ AsyncIterableQueue ============
|
|
19
19
|
/**
|
|
@@ -574,13 +574,25 @@ export class ClaudeSdkAdapter extends EventEmitter {
|
|
|
574
574
|
* 支持 Windows / macOS / Linux
|
|
575
575
|
*/
|
|
576
576
|
findClaudeExecutable(command) {
|
|
577
|
-
const normalizedCommand = (command
|
|
577
|
+
const normalizedCommand = this.extractExecutableCommand(command);
|
|
578
578
|
// 1. 如果是绝对路径,直接返回
|
|
579
579
|
if (path.isAbsolute(normalizedCommand) && fs.existsSync(normalizedCommand)) {
|
|
580
|
+
const resolvedWrapper = this.resolveWindowsWrapper(normalizedCommand);
|
|
581
|
+
if (resolvedWrapper) {
|
|
582
|
+
return resolvedWrapper;
|
|
583
|
+
}
|
|
584
|
+
if (this.isWindowsWrapperPath(normalizedCommand)) {
|
|
585
|
+
console.warn(`[ClaudeSdkAdapter] Skipping wrapper script for Claude Code executable: ${normalizedCommand}`);
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
580
588
|
return normalizedCommand;
|
|
581
589
|
}
|
|
582
|
-
|
|
583
|
-
if (
|
|
590
|
+
const pathExecutable = this.resolveExecutableFromPath(normalizedCommand);
|
|
591
|
+
if (pathExecutable) {
|
|
592
|
+
return pathExecutable;
|
|
593
|
+
}
|
|
594
|
+
// 2. Windows 特定路径(作为 claude 默认命令的兜底)
|
|
595
|
+
if (process.platform === 'win32' && this.shouldUseDefaultClaudeFallbacks(normalizedCommand)) {
|
|
584
596
|
const userProfile = process.env.USERPROFILE || '';
|
|
585
597
|
const localAppData = process.env.LOCALAPPDATA || '';
|
|
586
598
|
const appData = process.env.APPDATA || '';
|
|
@@ -607,55 +619,111 @@ export class ClaudeSdkAdapter extends EventEmitter {
|
|
|
607
619
|
}
|
|
608
620
|
}
|
|
609
621
|
}
|
|
610
|
-
// 3.
|
|
611
|
-
try {
|
|
612
|
-
const findCmd = process.platform === 'win32' ? `where ${normalizedCommand}` : `which ${normalizedCommand}`;
|
|
613
|
-
const result = execSync(findCmd, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
614
|
-
if (result) {
|
|
615
|
-
const foundPath = result.split(/\r?\n/)[0].trim();
|
|
616
|
-
if (fs.existsSync(foundPath)) {
|
|
617
|
-
// 如果是 wrapper 脚本,尝试找到实际的 cli.js
|
|
618
|
-
if (foundPath.endsWith('.cmd') || foundPath.endsWith('.ps1')) {
|
|
619
|
-
const cliJs = this.resolveCliJsFromWrapper(foundPath);
|
|
620
|
-
if (cliJs)
|
|
621
|
-
return cliJs;
|
|
622
|
-
}
|
|
623
|
-
return foundPath;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
// 忽略查找失败
|
|
629
|
-
}
|
|
630
|
-
// 4. macOS/Linux 特定路径
|
|
622
|
+
// 3. macOS/Linux 特定路径(作为 claude 默认命令的兜底)
|
|
631
623
|
const home = process.env.HOME || '';
|
|
632
624
|
const candidates = [
|
|
625
|
+
path.join(home, '.local', 'bin', 'claude'),
|
|
626
|
+
path.join(home, '.npm-global', 'bin', 'claude'),
|
|
627
|
+
path.join(home, '.local', 'share', 'mise', 'shims', 'claude'),
|
|
628
|
+
path.join(home, '.asdf', 'shims', 'claude'),
|
|
629
|
+
`/opt/homebrew/bin/claude`,
|
|
630
|
+
`/usr/local/bin/claude`,
|
|
631
|
+
`/usr/bin/claude`,
|
|
633
632
|
`/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js`,
|
|
634
633
|
`/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js`,
|
|
635
634
|
path.join(home, '.local', 'share', 'volta', 'tools', 'shared', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
636
635
|
path.join(home, '.volta', 'tools', 'shared', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
637
636
|
];
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
637
|
+
if (this.shouldUseDefaultClaudeFallbacks(normalizedCommand)) {
|
|
638
|
+
for (const p of candidates) {
|
|
639
|
+
if (p && fs.existsSync(p)) {
|
|
640
|
+
console.log(`[ClaudeSdkAdapter] Found Claude Code CLI: ${p}`);
|
|
641
|
+
return p;
|
|
642
|
+
}
|
|
642
643
|
}
|
|
643
644
|
}
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
645
|
+
// 4. 尝试 npm root -g
|
|
646
|
+
if (this.shouldUseDefaultClaudeFallbacks(normalizedCommand)) {
|
|
647
|
+
try {
|
|
648
|
+
const npmRoot = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
649
|
+
const cliJs = path.join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js');
|
|
650
|
+
if (fs.existsSync(cliJs)) {
|
|
651
|
+
console.log(`[ClaudeSdkAdapter] Found Claude Code CLI via npm root: ${cliJs}`);
|
|
652
|
+
return cliJs;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// 忽略
|
|
651
657
|
}
|
|
652
|
-
}
|
|
653
|
-
catch {
|
|
654
|
-
// 忽略
|
|
655
658
|
}
|
|
656
659
|
console.warn(`[ClaudeSdkAdapter] Claude Code executable not found for command: ${normalizedCommand}`);
|
|
657
660
|
return undefined;
|
|
658
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* 从面板配置的命令行中提取实际可执行文件,避免把参数一起传给 PATH 查找。
|
|
664
|
+
*/
|
|
665
|
+
extractExecutableCommand(command) {
|
|
666
|
+
const input = (command || 'claude').trim() || 'claude';
|
|
667
|
+
const firstToken = input.match(/^\s*(?:"([^"]+)"|'([^']+)'|(\S+))/);
|
|
668
|
+
return firstToken?.[1] || firstToken?.[2] || firstToken?.[3] || 'claude';
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* 按当前 Bridge 进程的 PATH/PATHEXT 直接查找可执行文件,避免依赖 which/where 和 shell 解析。
|
|
672
|
+
*/
|
|
673
|
+
resolveExecutableFromPath(command) {
|
|
674
|
+
const pathValue = process.env.PATH || '';
|
|
675
|
+
if (!pathValue) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
const commandHasExtension = path.extname(command).length > 0;
|
|
679
|
+
const extensions = process.platform === 'win32'
|
|
680
|
+
? this.getWindowsPathExtensions(commandHasExtension)
|
|
681
|
+
: [''];
|
|
682
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
683
|
+
if (!directory) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
for (const extension of extensions) {
|
|
687
|
+
const candidate = path.join(directory, `${command}${extension}`);
|
|
688
|
+
if (!fs.existsSync(candidate)) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (this.isWindowsWrapperPath(candidate)) {
|
|
692
|
+
const resolvedWrapper = this.resolveWindowsWrapper(candidate);
|
|
693
|
+
if (resolvedWrapper) {
|
|
694
|
+
return resolvedWrapper;
|
|
695
|
+
}
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
console.log(`[ClaudeSdkAdapter] Found Claude Code CLI via PATH: ${candidate}`);
|
|
699
|
+
return candidate;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
getWindowsPathExtensions(commandHasExtension) {
|
|
705
|
+
if (commandHasExtension) {
|
|
706
|
+
return [''];
|
|
707
|
+
}
|
|
708
|
+
const pathExt = process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM;.PS1';
|
|
709
|
+
return pathExt
|
|
710
|
+
.split(';')
|
|
711
|
+
.map((extension) => extension.trim())
|
|
712
|
+
.filter((extension) => extension.length > 0);
|
|
713
|
+
}
|
|
714
|
+
shouldUseDefaultClaudeFallbacks(command) {
|
|
715
|
+
return ['claude', 'claude-code', 'claudecode'].includes(command.toLowerCase());
|
|
716
|
+
}
|
|
717
|
+
isWindowsWrapperPath(filePath) {
|
|
718
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
719
|
+
return ['.cmd', '.bat', '.ps1'].includes(extension);
|
|
720
|
+
}
|
|
721
|
+
resolveWindowsWrapper(wrapperPath) {
|
|
722
|
+
if (!this.isWindowsWrapperPath(wrapperPath)) {
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
return this.resolveCliJsFromWrapper(wrapperPath);
|
|
726
|
+
}
|
|
659
727
|
/**
|
|
660
728
|
* 从 wrapper 脚本解析 cli.js 路径
|
|
661
729
|
*/
|
|
@@ -669,6 +737,11 @@ export class ClaudeSdkAdapter extends EventEmitter {
|
|
|
669
737
|
return voltaShared;
|
|
670
738
|
}
|
|
671
739
|
}
|
|
740
|
+
const wrapperDirectory = path.dirname(wrapperPath);
|
|
741
|
+
const npmCliJs = path.join(wrapperDirectory, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
|
|
742
|
+
if (fs.existsSync(npmCliJs)) {
|
|
743
|
+
return npmCliJs;
|
|
744
|
+
}
|
|
672
745
|
}
|
|
673
746
|
catch {
|
|
674
747
|
// 忽略
|
|
@@ -1063,7 +1136,7 @@ export class ClaudeSdkAdapter extends EventEmitter {
|
|
|
1063
1136
|
toolInput = JSON.parse(jsonStr);
|
|
1064
1137
|
console.log(`[ClaudeSdkAdapter] Parsed tool input:`, JSON.stringify(toolInput).slice(0, 200));
|
|
1065
1138
|
}
|
|
1066
|
-
catch
|
|
1139
|
+
catch {
|
|
1067
1140
|
console.warn(`[ClaudeSdkAdapter] Failed to parse tool input JSON:`, jsonStr.slice(0, 100));
|
|
1068
1141
|
}
|
|
1069
1142
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
1
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
5
|
import { ClaudeSdkAdapter } from './ClaudeSdkAdapter.js';
|
|
3
6
|
describe('ClaudeSdkAdapter', () => {
|
|
@@ -5,6 +8,149 @@ describe('ClaudeSdkAdapter', () => {
|
|
|
5
8
|
vi.useRealTimers();
|
|
6
9
|
vi.restoreAllMocks();
|
|
7
10
|
});
|
|
11
|
+
it('resolves claude from PATH without shelling out to which or where', () => {
|
|
12
|
+
const adapter = new ClaudeSdkAdapter();
|
|
13
|
+
const originalPath = process.env.PATH;
|
|
14
|
+
const originalPathExt = process.env.PATHEXT;
|
|
15
|
+
const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-path-'));
|
|
16
|
+
const commandPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.EXE' : 'claude');
|
|
17
|
+
fs.writeFileSync(commandPath, '#!/usr/bin/env node\n');
|
|
18
|
+
if (process.platform !== 'win32') {
|
|
19
|
+
fs.chmodSync(commandPath, 0o755);
|
|
20
|
+
}
|
|
21
|
+
process.env.PATH = commandDirectory;
|
|
22
|
+
process.env.PATHEXT = '.EXE;.CMD';
|
|
23
|
+
try {
|
|
24
|
+
const result = adapter.findClaudeExecutable('claude');
|
|
25
|
+
expect(result).toBe(commandPath);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
process.env.PATH = originalPath;
|
|
29
|
+
process.env.PATHEXT = originalPathExt;
|
|
30
|
+
fs.rmSync(commandDirectory, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
it('extracts the executable before resolving commands that include flags', () => {
|
|
34
|
+
const adapter = new ClaudeSdkAdapter();
|
|
35
|
+
const originalPath = process.env.PATH;
|
|
36
|
+
const originalPathExt = process.env.PATHEXT;
|
|
37
|
+
const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-flags-'));
|
|
38
|
+
const commandPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.EXE' : 'claude');
|
|
39
|
+
const unexpectedCommandWithFlags = path.join(commandDirectory, process.platform === 'win32' ? 'claude --model sonnet.EXE' : 'claude --model sonnet');
|
|
40
|
+
fs.writeFileSync(commandPath, '#!/usr/bin/env node\n');
|
|
41
|
+
if (process.platform !== 'win32') {
|
|
42
|
+
fs.chmodSync(commandPath, 0o755);
|
|
43
|
+
}
|
|
44
|
+
process.env.PATH = commandDirectory;
|
|
45
|
+
process.env.PATHEXT = '.EXE;.CMD';
|
|
46
|
+
try {
|
|
47
|
+
const result = adapter.findClaudeExecutable('claude --model sonnet');
|
|
48
|
+
expect(result).toBe(commandPath);
|
|
49
|
+
expect(fs.existsSync(unexpectedCommandWithFlags)).toBe(false);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
process.env.PATH = originalPath;
|
|
53
|
+
process.env.PATHEXT = originalPathExt;
|
|
54
|
+
fs.rmSync(commandDirectory, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
it.skipIf(process.platform !== 'win32')('resolves uppercase Windows wrapper extensions before returning PATH candidate', () => {
|
|
58
|
+
const adapter = new ClaudeSdkAdapter();
|
|
59
|
+
const originalPath = process.env.PATH;
|
|
60
|
+
const originalPathExt = process.env.PATHEXT;
|
|
61
|
+
const originalLocalAppData = process.env.LOCALAPPDATA;
|
|
62
|
+
const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-wrapper-'));
|
|
63
|
+
const localAppDataDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-localappdata-'));
|
|
64
|
+
const wrapperPath = path.join(commandDirectory, 'claude.CMD');
|
|
65
|
+
const cliPath = path.join(localAppDataDirectory, 'Volta', 'tools', 'shared', '@anthropic-ai', 'claude-code', 'cli.js');
|
|
66
|
+
fs.mkdirSync(path.dirname(cliPath), { recursive: true });
|
|
67
|
+
fs.writeFileSync(wrapperPath, 'volta run %~n0 %*\n');
|
|
68
|
+
fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
|
|
69
|
+
process.env.PATH = commandDirectory;
|
|
70
|
+
process.env.PATHEXT = '.EXE;.CMD;.PS1';
|
|
71
|
+
process.env.LOCALAPPDATA = localAppDataDirectory;
|
|
72
|
+
try {
|
|
73
|
+
const result = adapter.findClaudeExecutable('claude');
|
|
74
|
+
expect(result).toBe(cliPath);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
process.env.PATH = originalPath;
|
|
78
|
+
process.env.PATHEXT = originalPathExt;
|
|
79
|
+
process.env.LOCALAPPDATA = originalLocalAppData;
|
|
80
|
+
fs.rmSync(commandDirectory, { recursive: true, force: true });
|
|
81
|
+
fs.rmSync(localAppDataDirectory, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
it.skipIf(process.platform !== 'win32')('resolves npm command wrappers to the Claude Code cli.js file', () => {
|
|
85
|
+
const adapter = new ClaudeSdkAdapter();
|
|
86
|
+
const originalPath = process.env.PATH;
|
|
87
|
+
const originalPathExt = process.env.PATHEXT;
|
|
88
|
+
const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-npm-wrapper-'));
|
|
89
|
+
const wrapperPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.CMD' : 'claude.ps1');
|
|
90
|
+
const cliPath = path.join(commandDirectory, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
|
|
91
|
+
fs.mkdirSync(path.dirname(cliPath), { recursive: true });
|
|
92
|
+
fs.writeFileSync(wrapperPath, '@ECHO off\nnode "%~dp0\\node_modules\\@anthropic-ai\\claude-code\\cli.js" %*\n');
|
|
93
|
+
fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
|
|
94
|
+
process.env.PATH = commandDirectory;
|
|
95
|
+
process.env.PATHEXT = '.EXE;.CMD;.PS1';
|
|
96
|
+
try {
|
|
97
|
+
const result = adapter.findClaudeExecutable('claude');
|
|
98
|
+
expect(result).toBe(cliPath);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
process.env.PATH = originalPath;
|
|
102
|
+
process.env.PATHEXT = originalPathExt;
|
|
103
|
+
fs.rmSync(commandDirectory, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
it('skips unresolved command wrappers instead of returning them to the SDK', () => {
|
|
107
|
+
const adapter = new ClaudeSdkAdapter();
|
|
108
|
+
const originalPath = process.env.PATH;
|
|
109
|
+
const originalPathExt = process.env.PATHEXT;
|
|
110
|
+
const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-unresolved-wrapper-'));
|
|
111
|
+
const wrapperPath = path.join(commandDirectory, 'claude.CMD');
|
|
112
|
+
fs.writeFileSync(wrapperPath, '@ECHO off\nnode missing-cli.js %*\n');
|
|
113
|
+
process.env.PATH = commandDirectory;
|
|
114
|
+
process.env.PATHEXT = '.EXE;.CMD;.PS1';
|
|
115
|
+
try {
|
|
116
|
+
const result = adapter.findClaudeExecutable('claude');
|
|
117
|
+
expect(result).not.toBe(wrapperPath);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
process.env.PATH = originalPath;
|
|
121
|
+
process.env.PATHEXT = originalPathExt;
|
|
122
|
+
fs.rmSync(commandDirectory, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
it('resolves absolute npm command wrappers before passing them to the SDK', () => {
|
|
126
|
+
const adapter = new ClaudeSdkAdapter();
|
|
127
|
+
const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-absolute-wrapper-'));
|
|
128
|
+
const wrapperPath = path.join(commandDirectory, 'claude.cmd');
|
|
129
|
+
const cliPath = path.join(commandDirectory, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
|
|
130
|
+
fs.mkdirSync(path.dirname(cliPath), { recursive: true });
|
|
131
|
+
fs.writeFileSync(wrapperPath, '@ECHO off\nnode "%~dp0\\node_modules\\@anthropic-ai\\claude-code\\cli.js" %*\n');
|
|
132
|
+
fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
|
|
133
|
+
try {
|
|
134
|
+
const result = adapter.findClaudeExecutable(wrapperPath);
|
|
135
|
+
expect(result).toBe(cliPath);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
fs.rmSync(commandDirectory, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
it('does not pass unresolved absolute wrapper scripts to the SDK', () => {
|
|
142
|
+
const adapter = new ClaudeSdkAdapter();
|
|
143
|
+
const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-absolute-unresolved-wrapper-'));
|
|
144
|
+
const wrapperPath = path.join(commandDirectory, 'claude.bat');
|
|
145
|
+
fs.writeFileSync(wrapperPath, '@ECHO off\nnode missing-cli.js %*\n');
|
|
146
|
+
try {
|
|
147
|
+
const result = adapter.findClaudeExecutable(wrapperPath);
|
|
148
|
+
expect(result).toBeUndefined();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
fs.rmSync(commandDirectory, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
8
154
|
it('does not auto-wake Claude for my_task-style idle commands', async () => {
|
|
9
155
|
vi.useFakeTimers();
|
|
10
156
|
const adapter = new ClaudeSdkAdapter();
|
|
@@ -10,4 +10,17 @@ export declare function buildConnectionCheckResponse(connectionKeyMd5: string):
|
|
|
10
10
|
error?: string;
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
|
+
export interface SchedulerPingFailureResponse {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
failureStage: "scheduler_ping";
|
|
17
|
+
runtimeBridge: "healthy";
|
|
18
|
+
schedulerBaseUrl: string;
|
|
19
|
+
hint: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 构建调度中心 ping 失败响应。
|
|
23
|
+
* 主流程:保留底层错误与 scheduler 地址,同时明确 bridge 本身已连通、失败点在 bridge 回连调度中心。
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildSchedulerPingFailureResponse(error: Error, configuredSchedulerBaseUrl: string): SchedulerPingFailureResponse;
|
|
13
26
|
//# sourceMappingURL=instance.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AA6BA,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAyBvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA"}
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AA6BA,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAyBvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,KAAK,EACZ,0BAA0B,EAAE,MAAM,GACjC,4BAA4B,CAiB9B"}
|
package/dist/routes/instance.js
CHANGED
|
@@ -72,6 +72,25 @@ export function buildConnectionCheckResponse(connectionKeyMd5) {
|
|
|
72
72
|
},
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 构建调度中心 ping 失败响应。
|
|
77
|
+
* 主流程:保留底层错误与 scheduler 地址,同时明确 bridge 本身已连通、失败点在 bridge 回连调度中心。
|
|
78
|
+
*/
|
|
79
|
+
export function buildSchedulerPingFailureResponse(error, configuredSchedulerBaseUrl) {
|
|
80
|
+
const trimmedSchedulerBaseUrl = configuredSchedulerBaseUrl.trim();
|
|
81
|
+
const schedulerUrl = trimmedSchedulerBaseUrl || "http://localhost:8080";
|
|
82
|
+
const isLocalhostScheduler = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?(?:\/|$)/i.test(schedulerUrl);
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: error.message || "scheduler ping failed",
|
|
86
|
+
failureStage: "scheduler_ping",
|
|
87
|
+
runtimeBridge: "healthy",
|
|
88
|
+
schedulerBaseUrl: schedulerUrl,
|
|
89
|
+
hint: isLocalhostScheduler
|
|
90
|
+
? "aws-runtime-bridge 已连通,但它回连调度中心失败。请在 bridge 启动环境中将 AWS_RUNTIME_SCHEDULER_BASE_URL 配置为该实例可访问的调度中心地址,例如 http://<server-host>:7380。"
|
|
91
|
+
: "aws-runtime-bridge 已连通,但它回连调度中心失败。请确认 AWS_RUNTIME_SCHEDULER_BASE_URL 指向的调度中心地址可从 bridge 机器访问,并且运行时访问令牌仍有效。",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
75
94
|
instanceRouter.get("/healthz", (_req, res) => {
|
|
76
95
|
res.json({
|
|
77
96
|
ok: true,
|
|
@@ -100,11 +119,9 @@ instanceRouter.get("/ping", validateToken, async (_req, res) => {
|
|
|
100
119
|
}
|
|
101
120
|
catch (error) {
|
|
102
121
|
const err = error;
|
|
103
|
-
res
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
schedulerBaseUrl,
|
|
107
|
-
});
|
|
122
|
+
res
|
|
123
|
+
.status(502)
|
|
124
|
+
.json(buildSchedulerPingFailureResponse(err, schedulerBaseUrl));
|
|
108
125
|
}
|
|
109
126
|
});
|
|
110
127
|
instanceRouter.post("/init-instance", validateToken, async (req, res) => {
|
|
@@ -98,4 +98,25 @@ describe('instance route validation', () => {
|
|
|
98
98
|
},
|
|
99
99
|
});
|
|
100
100
|
});
|
|
101
|
+
it('explains localhost scheduler ping failures with environment variable guidance', async () => {
|
|
102
|
+
const { buildSchedulerPingFailureResponse } = await import('./instance.js');
|
|
103
|
+
const response = buildSchedulerPingFailureResponse(new Error('connect ECONNREFUSED 127.0.0.1:8080'), 'http://localhost:8080');
|
|
104
|
+
expect(response).toEqual({
|
|
105
|
+
ok: false,
|
|
106
|
+
error: 'connect ECONNREFUSED 127.0.0.1:8080',
|
|
107
|
+
failureStage: 'scheduler_ping',
|
|
108
|
+
runtimeBridge: 'healthy',
|
|
109
|
+
schedulerBaseUrl: 'http://localhost:8080',
|
|
110
|
+
hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
|
|
111
|
+
});
|
|
112
|
+
expect(response.hint).toContain('http://<server-host>:7380');
|
|
113
|
+
});
|
|
114
|
+
it('explains non-localhost scheduler ping failures as reachability or token issues', async () => {
|
|
115
|
+
const { buildSchedulerPingFailureResponse } = await import('./instance.js');
|
|
116
|
+
const response = buildSchedulerPingFailureResponse(new Error('Request failed with status code 401'), 'http://10.0.0.8:7380');
|
|
117
|
+
expect(response.schedulerBaseUrl).toBe('http://10.0.0.8:7380');
|
|
118
|
+
expect(response.failureStage).toBe('scheduler_ping');
|
|
119
|
+
expect(response.runtimeBridge).toBe('healthy');
|
|
120
|
+
expect(response.hint).toContain('运行时访问令牌仍有效');
|
|
121
|
+
});
|
|
101
122
|
});
|