diff --git a/__tests__/claude-code-router-config.test.js b/__tests__/claude-code-router-config.test.js index d517a2f..5c55cca 100644 --- a/__tests__/claude-code-router-config.test.js +++ b/__tests__/claude-code-router-config.test.js @@ -120,6 +120,32 @@ describe("ClaudeCodeRouterConfig", () => { }); }); + describe("error handling", () => { + beforeEach(() => { + config = new ClaudeCodeRouterConfig(); + }); + + it("should handle transformer file write errors", async () => { + const error = new Error("Write failed"); + fs.writeFile.mockRejectedValueOnce(error); + fs.writeJson.mockResolvedValue(); // Make sure other fs operations succeed + + await expect(config.createTransformerFile()).rejects.toThrow( + "Write failed" + ); + }); + + it("should handle config file write errors", async () => { + const error = new Error("JSON write failed"); + fs.writeJson.mockRejectedValueOnce(error); + fs.writeFile.mockResolvedValue(); // Make sure other fs operations succeed + + await expect(config.createConfigFile("test-key", "cn")).rejects.toThrow( + "JSON write failed" + ); + }); + }); + describe("createConfigFile", () => { beforeEach(() => { config = new ClaudeCodeRouterConfig(); @@ -127,8 +153,9 @@ describe("ClaudeCodeRouterConfig", () => { it("should create config file with API Key from environment variable", async () => { const testApiKey = "test-api-key-from-env"; + const testRegion = "cn"; - await config.createConfigFile(testApiKey); + await config.createConfigFile(testApiKey, testRegion); expect(fs.writeJson).toHaveBeenCalledWith( config.configFile, @@ -136,6 +163,7 @@ describe("ClaudeCodeRouterConfig", () => { Providers: expect.arrayContaining([ expect.objectContaining({ api_key: testApiKey, + api_base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", }), ]), }), @@ -145,8 +173,9 @@ describe("ClaudeCodeRouterConfig", () => { it("should create config file with provided API Key", async () => { const testApiKey = "test-api-key-provided"; + const testRegion = "intl"; - await config.createConfigFile(testApiKey); + await config.createConfigFile(testApiKey, testRegion); expect(fs.writeJson).toHaveBeenCalledWith( config.configFile, @@ -154,6 +183,7 @@ describe("ClaudeCodeRouterConfig", () => { Providers: expect.arrayContaining([ expect.objectContaining({ api_key: testApiKey, + api_base_url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions", }), ]), }), @@ -162,7 +192,7 @@ describe("ClaudeCodeRouterConfig", () => { }); it("should use undefined API Key when no API Key is provided", async () => { - await config.createConfigFile(); + await config.createConfigFile(undefined, "cn"); expect(fs.writeJson).toHaveBeenCalledWith( config.configFile, @@ -178,7 +208,7 @@ describe("ClaudeCodeRouterConfig", () => { }); it("should contain correct configuration structure", async () => { - await config.createConfigFile(); + await config.createConfigFile("test-key", "cn"); const writeJsonCall = fs.writeJson.mock.calls[0]; const configContent = writeJsonCall[1]; @@ -232,6 +262,7 @@ describe("ClaudeCodeRouterConfig", () => { jest.spyOn(config, "createConfigFile").mockResolvedValue(); jest.spyOn(config, "createTransformerFile").mockResolvedValue(); jest.spyOn(config, "promptForApiKey").mockResolvedValue("user-input-key"); + jest.spyOn(config, "promptForRegion").mockResolvedValue("cn"); }); afterEach(() => { @@ -244,8 +275,9 @@ describe("ClaudeCodeRouterConfig", () => { await config.setup(); + expect(config.promptForRegion).toHaveBeenCalled(); expect(config.createDirectories).toHaveBeenCalled(); - expect(config.createConfigFile).toHaveBeenCalledWith("env-test-key"); + expect(config.createConfigFile).toHaveBeenCalledWith("env-test-key", "cn"); expect(config.createTransformerFile).toHaveBeenCalled(); expect(config.promptForApiKey).not.toHaveBeenCalled(); }); @@ -260,7 +292,7 @@ describe("ClaudeCodeRouterConfig", () => { "DASHSCOPE_API_KEY environment variable detected" ) ); - expect(config.createConfigFile).toHaveBeenCalledWith("test-key-from-env"); + expect(config.createConfigFile).toHaveBeenCalledWith("test-key-from-env", "cn"); }); it("should prompt for API Key when environment variable is not present", async () => { @@ -274,7 +306,7 @@ describe("ClaudeCodeRouterConfig", () => { ) ); expect(config.promptForApiKey).toHaveBeenCalled(); - expect(config.createConfigFile).toHaveBeenCalledWith("user-input-key"); + expect(config.createConfigFile).toHaveBeenCalledWith("user-input-key", "cn"); }); it("should handle errors during setup process", async () => { @@ -287,6 +319,111 @@ describe("ClaudeCodeRouterConfig", () => { }); }); + describe("promptForRegion", () => { + beforeEach(() => { + config = new ClaudeCodeRouterConfig(); + + // Mock console.log + jest.spyOn(console, "log").mockImplementation(); + }); + + afterEach(() => { + console.log.mockRestore(); + }); + + it("should prompt for region and return 'cn' when user selects 1", async () => { + const mockReadline = { + question: jest.fn(), + close: jest.fn() + }; + + const readline = require("readline"); + jest.spyOn(readline, "createInterface").mockReturnValue(mockReadline); + + mockReadline.question.mockImplementation((prompt, callback) => { + callback("1"); + }); + + const result = await config.promptForRegion(); + + expect(readline.createInterface).toHaveBeenCalledWith({ + input: process.stdin, + output: process.stdout + }); + expect(mockReadline.question).toHaveBeenCalled(); + expect(mockReadline.close).toHaveBeenCalled(); + expect(result).toBe("cn"); + }); + + it("should prompt for region and return 'intl' when user selects 2", async () => { + const mockReadline = { + question: jest.fn(), + close: jest.fn() + }; + + const readline = require("readline"); + jest.spyOn(readline, "createInterface").mockReturnValue(mockReadline); + + mockReadline.question.mockImplementation((prompt, callback) => { + callback("2"); + }); + + const result = await config.promptForRegion(); + + expect(result).toBe("intl"); + expect(mockReadline.close).toHaveBeenCalled(); + }); + + it("should re-prompt when invalid input is provided", async () => { + const mockReadline = { + question: jest.fn(), + close: jest.fn() + }; + + const readline = require("readline"); + jest.spyOn(readline, "createInterface").mockReturnValue(mockReadline); + + let callCount = 0; + mockReadline.question.mockImplementation((prompt, callback) => { + callCount++; + if (callCount === 1) { + callback("invalid"); + } else { + callback("1"); + } + }); + + const result = await config.promptForRegion(); + + expect(mockReadline.question).toHaveBeenCalledTimes(2); + expect(result).toBe("cn"); + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining("Please enter 1 or 2") + ); + }); + }); + + describe("getApiBaseUrl", () => { + beforeEach(() => { + config = new ClaudeCodeRouterConfig(); + }); + + it("should return China URL for 'cn' region", () => { + const result = config.getApiBaseUrl("cn"); + expect(result).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"); + }); + + it("should return International URL for 'intl' region", () => { + const result = config.getApiBaseUrl("intl"); + expect(result).toBe("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions"); + }); + + it("should default to China URL for unknown region", () => { + const result = config.getApiBaseUrl("unknown"); + expect(result).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"); + }); + }); + describe("promptForApiKey", () => { beforeEach(() => { config = new ClaudeCodeRouterConfig(); diff --git a/bin/claude-code-router-config.js b/bin/claude-code-router-config.js index cd88fbd..672417b 100755 --- a/bin/claude-code-router-config.js +++ b/bin/claude-code-router-config.js @@ -25,6 +25,42 @@ class ClaudeCodeRouterConfig { return locale.toLowerCase().includes('zh') ? 'zh' : 'en'; } + async promptForRegion() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + const askForRegion = () => { + const prompt = `${this.messages.regionPrompt}\n${this.messages.regionOption1}\n${this.messages.regionOption2}\n${this.messages.regionInput}`; + + rl.question(prompt, (answer) => { + const choice = answer.trim(); + if (choice === '1') { + console.log(this.messages.regionSelected1); + rl.close(); + resolve('cn'); + } else if (choice === '2') { + console.log(this.messages.regionSelected2); + rl.close(); + resolve('intl'); + } else { + console.log(this.messages.regionInvalid); + askForRegion(); + } + }); + }; + askForRegion(); + }); + } + + getApiBaseUrl(region) { + return region === 'intl' + ? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions" + : "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; + } + getMessages() { const messages = { zh: { @@ -38,6 +74,13 @@ class ClaudeCodeRouterConfig { step2: "2. 请确保已安装 @musistudio/claude-code-router", step3Warning: "3. ⚠️ 请手动配置环境变量DASHSCOPE_API_KEY:", step3Success: "3. ✅ API Key 已从环境变量自动配置", + regionPrompt: "请选择服务区域 (Please select service region):", + regionOption1: "1. 阿里云 (Alibaba Cloud China)", + regionOption2: "2. 阿里云国际站 (Alibaba Cloud International)", + regionInput: "请输入 1 或 2 (Enter 1 or 2): ", + regionSelected1: "✅ 已选择阿里云中国站", + regionSelected2: "✅ 已选择阿里云国际站", + regionInvalid: "❌ 请输入 1 或 2", promptApiKey: "请输入您的 DashScope API Key:", apiKeyPrompt: "DashScope API Key", apiKeyConfigured: "✅ API Key 已配置完成", @@ -65,6 +108,13 @@ class ClaudeCodeRouterConfig { step2: "2. Please ensure @musistudio/claude-code-router is installed", step3Warning: "3. ⚠️ Please manually set your DASHSCOPE_API_KEY environment variable:", step3Success: "3. ✅ API Key automatically configured from environment variable", + regionPrompt: "Please select service region:", + regionOption1: "1. Alibaba Cloud China", + regionOption2: "2. Alibaba Cloud International", + regionInput: "Enter 1 or 2: ", + regionSelected1: "✅ Selected Alibaba Cloud China", + regionSelected2: "✅ Selected Alibaba Cloud International", + regionInvalid: "❌ Please enter 1 or 2", promptApiKey: "Please enter your DashScope API Key:", apiKeyPrompt: "DashScope API Key", apiKeyConfigured: "✅ API Key configured successfully", @@ -113,6 +163,9 @@ class ClaudeCodeRouterConfig { try { console.log(this.messages.configuring); + // 询问用户选择服务区域 + const region = await this.promptForRegion(); + // 检查环境变量 let apiKey = process.env.DASHSCOPE_API_KEY; const hasEnvApiKey = !!apiKey; @@ -129,7 +182,7 @@ class ClaudeCodeRouterConfig { await this.createDirectories(); // 创建配置文件 - await this.createConfigFile(apiKey); + await this.createConfigFile(apiKey, region); // 创建插件文件 await this.createTransformerFile(); @@ -158,9 +211,10 @@ class ClaudeCodeRouterConfig { console.log(this.messages.createDir, this.configDir); } - async createConfigFile(apiKey) { + async createConfigFile(apiKey, region) { // 使用传入的 API Key(可能来自环境变量或用户输入) const dashscopeApiKey = apiKey; + const apiBaseUrl = this.getApiBaseUrl(region); const configContent = { LOG: true, @@ -183,8 +237,7 @@ class ClaudeCodeRouterConfig { Providers: [ { name: "dashscope", - api_base_url: - "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", + api_base_url: apiBaseUrl, api_key: dashscopeApiKey, models: ["qwen3-235b-a22b"], transformer: { diff --git a/package-lock.json b/package-lock.json index 7fb6a16..79302e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dashscope-js/claude-code-config", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dashscope-js/claude-code-config", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { "fs-extra": "^11.3.0"