@x-all-in-one/coding-helper 0.2.0 → 0.3.1
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/lib/model-service.d.ts +7 -8
- package/dist/lib/model-service.js +23 -51
- package/dist/lib/tools/base-tool.d.ts +1 -1
- package/dist/lib/tools/claude-code-tool.d.ts +1 -1
- package/dist/lib/tools/claude-code-tool.js +3 -26
- package/dist/lib/tools/codex-tool.d.ts +14 -33
- package/dist/lib/tools/codex-tool.js +127 -71
- package/dist/lib/tools/opencode-tool.d.ts +3 -12
- package/dist/lib/tools/opencode-tool.js +4 -44
- package/dist/lib/utils/fetch-models.d.ts +8 -0
- package/dist/lib/utils/fetch-models.js +30 -0
- package/dist/lib/wizard/menus/plugin-menu.js +3 -0
- package/dist/lib/wizard/menus/tool-menu.js +6 -2
- package/dist/locales/en_US.json +2 -1
- package/dist/locales/zh_CN.json +2 -1
- package/package.json +2 -1
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
context_length?: number;
|
|
5
|
-
max_output_tokens?: number;
|
|
6
|
-
}
|
|
1
|
+
import type { ModelInfo } from './utils/fetch-models.js';
|
|
2
|
+
export type { ModelInfo } from './utils/fetch-models.js';
|
|
3
|
+
export { CODE_PLAN_MODEL_LIST_API } from './utils/fetch-models.js';
|
|
7
4
|
/**
|
|
8
5
|
* 模型服务
|
|
9
6
|
* 负责模型列表的获取、缓存和同步
|
|
@@ -19,9 +16,9 @@ export declare class ModelService {
|
|
|
19
16
|
*/
|
|
20
17
|
fetchModels(): Promise<string[]>;
|
|
21
18
|
/**
|
|
22
|
-
*
|
|
19
|
+
* 获取带详细信息的模型列表(带缓存)
|
|
23
20
|
*/
|
|
24
|
-
fetchModelsWithInfo(
|
|
21
|
+
fetchModelsWithInfo(): Promise<ModelInfo[]>;
|
|
25
22
|
/**
|
|
26
23
|
* 清除缓存
|
|
27
24
|
*/
|
|
@@ -32,6 +29,7 @@ export declare class ModelService {
|
|
|
32
29
|
refreshModels(): Promise<{
|
|
33
30
|
success: boolean;
|
|
34
31
|
count: number;
|
|
32
|
+
error?: string;
|
|
35
33
|
}>;
|
|
36
34
|
/**
|
|
37
35
|
* 刷新并同步模型列表到 OpenCode 配置
|
|
@@ -40,6 +38,7 @@ export declare class ModelService {
|
|
|
40
38
|
refreshAndSyncToOpenCode(): Promise<{
|
|
41
39
|
success: boolean;
|
|
42
40
|
count: number;
|
|
41
|
+
error?: string;
|
|
43
42
|
}>;
|
|
44
43
|
/**
|
|
45
44
|
* 获取缓存的模型数量
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { configManager } from './config.js';
|
|
2
2
|
import { openCodeTool } from './tools/opencode-tool.js';
|
|
3
|
+
import { fetchModelsFromApi } from './utils/fetch-models.js';
|
|
4
|
+
export { CODE_PLAN_MODEL_LIST_API } from './utils/fetch-models.js';
|
|
3
5
|
/**
|
|
4
6
|
* 模型服务
|
|
5
7
|
* 负责模型列表的获取、缓存和同步
|
|
@@ -19,56 +21,23 @@ export class ModelService {
|
|
|
19
21
|
* 获取可用模型列表(带缓存)
|
|
20
22
|
*/
|
|
21
23
|
async fetchModels() {
|
|
22
|
-
const apiKey = configManager.getApiKey();
|
|
23
|
-
if (!apiKey) {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
24
|
if (this.cachedModels.length > 0) {
|
|
27
25
|
return this.cachedModels;
|
|
28
26
|
}
|
|
29
|
-
const modelsWithInfo = await this.fetchModelsWithInfo(
|
|
27
|
+
const modelsWithInfo = await this.fetchModelsWithInfo();
|
|
30
28
|
this.cachedModels = modelsWithInfo.map(m => m.id);
|
|
31
29
|
return this.cachedModels;
|
|
32
30
|
}
|
|
33
31
|
/**
|
|
34
|
-
*
|
|
32
|
+
* 获取带详细信息的模型列表(带缓存)
|
|
35
33
|
*/
|
|
36
|
-
async fetchModelsWithInfo(
|
|
34
|
+
async fetchModelsWithInfo() {
|
|
37
35
|
if (this.cachedModelsWithInfo.length > 0) {
|
|
38
36
|
return this.cachedModelsWithInfo;
|
|
39
37
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const response = await fetch('https://code-api.x-aio.com/v1/models', {
|
|
44
|
-
method: 'GET',
|
|
45
|
-
headers: {
|
|
46
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
47
|
-
'Content-Type': 'application/json',
|
|
48
|
-
},
|
|
49
|
-
signal: controller.signal,
|
|
50
|
-
});
|
|
51
|
-
clearTimeout(timeoutId);
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
54
|
-
}
|
|
55
|
-
const data = await response.json();
|
|
56
|
-
if (data && data.data && Array.isArray(data.data)) {
|
|
57
|
-
this.cachedModelsWithInfo = data.data.map((m) => ({
|
|
58
|
-
id: m.id,
|
|
59
|
-
name: m.name || m.id,
|
|
60
|
-
context_length: m.context_length,
|
|
61
|
-
max_output_tokens: m.max_output_tokens,
|
|
62
|
-
}));
|
|
63
|
-
this.cachedModels = this.cachedModelsWithInfo.map(m => m.id);
|
|
64
|
-
return this.cachedModelsWithInfo;
|
|
65
|
-
}
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
clearTimeout(timeoutId);
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
38
|
+
this.cachedModelsWithInfo = await fetchModelsFromApi();
|
|
39
|
+
this.cachedModels = this.cachedModelsWithInfo.map(m => m.id);
|
|
40
|
+
return this.cachedModelsWithInfo;
|
|
72
41
|
}
|
|
73
42
|
/**
|
|
74
43
|
* 清除缓存
|
|
@@ -83,15 +52,19 @@ export class ModelService {
|
|
|
83
52
|
async refreshModels() {
|
|
84
53
|
const apiKey = configManager.getApiKey();
|
|
85
54
|
if (!apiKey) {
|
|
86
|
-
return { success: false, count: 0 };
|
|
55
|
+
return { success: false, count: 0, error: 'No API key configured' };
|
|
87
56
|
}
|
|
88
57
|
this.clearCache();
|
|
89
58
|
try {
|
|
90
|
-
const models = await this.fetchModelsWithInfo(
|
|
91
|
-
|
|
59
|
+
const models = await this.fetchModelsWithInfo();
|
|
60
|
+
if (models.length === 0) {
|
|
61
|
+
return { success: false, count: 0, error: 'API returned empty model list' };
|
|
62
|
+
}
|
|
63
|
+
return { success: true, count: models.length };
|
|
92
64
|
}
|
|
93
|
-
catch {
|
|
94
|
-
|
|
65
|
+
catch (err) {
|
|
66
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
67
|
+
return { success: false, count: 0, error: errorMessage };
|
|
95
68
|
}
|
|
96
69
|
}
|
|
97
70
|
/**
|
|
@@ -101,21 +74,20 @@ export class ModelService {
|
|
|
101
74
|
async refreshAndSyncToOpenCode() {
|
|
102
75
|
const apiKey = configManager.getApiKey();
|
|
103
76
|
if (!apiKey) {
|
|
104
|
-
return { success: false, count: 0 };
|
|
77
|
+
return { success: false, count: 0, error: 'No API key configured' };
|
|
105
78
|
}
|
|
106
79
|
this.clearCache();
|
|
107
80
|
try {
|
|
108
|
-
|
|
109
|
-
const models = await this.fetchModelsWithInfo(apiKey);
|
|
81
|
+
const models = await this.fetchModelsWithInfo();
|
|
110
82
|
if (models.length === 0) {
|
|
111
|
-
return { success: false, count: 0 };
|
|
83
|
+
return { success: false, count: 0, error: 'API returned empty model list' };
|
|
112
84
|
}
|
|
113
|
-
// 同步到 OpenCode 配置
|
|
114
85
|
openCodeTool.syncModelsToConfig(models);
|
|
115
86
|
return { success: true, count: models.length };
|
|
116
87
|
}
|
|
117
|
-
catch {
|
|
118
|
-
|
|
88
|
+
catch (err) {
|
|
89
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
90
|
+
return { success: false, count: 0, error: errorMessage };
|
|
119
91
|
}
|
|
120
92
|
}
|
|
121
93
|
/**
|
|
@@ -69,5 +69,5 @@ export declare abstract class BaseTool implements ITool {
|
|
|
69
69
|
abstract getModelConfig(): unknown;
|
|
70
70
|
abstract saveModelConfig(config: unknown): Promise<void>;
|
|
71
71
|
abstract clearModelConfig(): void;
|
|
72
|
-
abstract fetchAvailableModels(
|
|
72
|
+
abstract fetchAvailableModels(): Promise<string[]>;
|
|
73
73
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { modelService } from '../model-service.js';
|
|
4
5
|
import { BaseTool } from './base-tool.js';
|
|
5
6
|
// 默认配置
|
|
6
7
|
export const DEFAULT_CONFIG = {
|
|
@@ -131,32 +132,8 @@ export class ClaudeCodeTool extends BaseTool {
|
|
|
131
132
|
/**
|
|
132
133
|
* 从 API 获取可用的模型列表
|
|
133
134
|
*/
|
|
134
|
-
async fetchAvailableModels(
|
|
135
|
-
|
|
136
|
-
const controller = new AbortController();
|
|
137
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
138
|
-
const response = await fetch('https://code-api.x-aio.com/v1/models', {
|
|
139
|
-
method: 'GET',
|
|
140
|
-
headers: {
|
|
141
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
142
|
-
'Content-Type': 'application/json',
|
|
143
|
-
},
|
|
144
|
-
signal: controller.signal,
|
|
145
|
-
});
|
|
146
|
-
clearTimeout(timeoutId);
|
|
147
|
-
if (!response.ok) {
|
|
148
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
149
|
-
}
|
|
150
|
-
const data = await response.json();
|
|
151
|
-
if (data && data.data && Array.isArray(data.data)) {
|
|
152
|
-
return data.data.map((model) => model.id);
|
|
153
|
-
}
|
|
154
|
-
return [];
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
console.warn('Failed to fetch available models:', error);
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
135
|
+
async fetchAvailableModels() {
|
|
136
|
+
return modelService.fetchModels();
|
|
160
137
|
}
|
|
161
138
|
/**
|
|
162
139
|
* 保存模型配置到 Claude Code
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { BaseTool } from './base-tool.js';
|
|
2
|
+
export interface ModelProviderInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
base_url?: string;
|
|
5
|
+
env_key?: string;
|
|
6
|
+
wire_api?: 'chat' | 'responses';
|
|
7
|
+
}
|
|
2
8
|
export interface CodexConfig {
|
|
3
9
|
model?: string;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
apiKey?: string;
|
|
8
|
-
};
|
|
9
|
-
[key: string]: any;
|
|
10
|
+
model_provider?: string;
|
|
11
|
+
model_providers?: Record<string, ModelProviderInfo>;
|
|
12
|
+
[key: string]: unknown;
|
|
10
13
|
}
|
|
11
14
|
export interface CodexModelConfig {
|
|
12
15
|
apiKey: string;
|
|
13
16
|
model: string;
|
|
14
17
|
baseUrl?: string;
|
|
15
18
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Codex 工具实现
|
|
18
|
-
* 实现 ITool 接口,管理 Codex 的配置和生命周期
|
|
19
|
-
*/
|
|
20
19
|
export declare class CodexTool extends BaseTool {
|
|
21
20
|
private static instance;
|
|
22
21
|
readonly name = "codex";
|
|
@@ -26,33 +25,15 @@ export declare class CodexTool extends BaseTool {
|
|
|
26
25
|
readonly configPath: string;
|
|
27
26
|
private constructor();
|
|
28
27
|
static getInstance(): CodexTool;
|
|
29
|
-
/**
|
|
30
|
-
* Ensure config directory exists
|
|
31
|
-
*/
|
|
32
28
|
private ensureDir;
|
|
33
|
-
/**
|
|
34
|
-
* Read codex config
|
|
35
|
-
*/
|
|
36
29
|
getConfig(): CodexConfig;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*/
|
|
40
|
-
saveConfig(config: unknown): void;
|
|
41
|
-
/**
|
|
42
|
-
* Fetch available models from API
|
|
43
|
-
*/
|
|
44
|
-
fetchAvailableModels(apiKey: string): Promise<string[]>;
|
|
45
|
-
/**
|
|
46
|
-
* Save model config to Codex
|
|
47
|
-
*/
|
|
30
|
+
saveConfig(config: CodexConfig): void;
|
|
31
|
+
fetchAvailableModels(): Promise<string[]>;
|
|
48
32
|
saveModelConfig(config: unknown): Promise<void>;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
private setApiKeyEnv;
|
|
34
|
+
private getShellRcPath;
|
|
35
|
+
private getEnvValue;
|
|
52
36
|
getModelConfig(): CodexModelConfig | null;
|
|
53
|
-
/**
|
|
54
|
-
* Clear model config
|
|
55
|
-
*/
|
|
56
37
|
clearModelConfig(): void;
|
|
57
38
|
}
|
|
58
39
|
export declare const codexTool: CodexTool;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { parse, stringify } from 'smol-toml';
|
|
6
|
+
import { modelService } from '../model-service.js';
|
|
4
7
|
import { BaseTool } from './base-tool.js';
|
|
5
|
-
// 使用相同的 X-AIO API 端点
|
|
6
8
|
const DEFAULT_BASE_URL = 'https://code-api.x-aio.com/v1';
|
|
7
|
-
|
|
8
|
-
* Codex 工具实现
|
|
9
|
-
* 实现 ITool 接口,管理 Codex 的配置和生命周期
|
|
10
|
-
*/
|
|
9
|
+
const X_AIO_CODE_API_KEY_ENV = 'X_AIO_CODE_API_KEY';
|
|
11
10
|
export class CodexTool extends BaseTool {
|
|
12
11
|
static instance;
|
|
13
12
|
name = 'codex';
|
|
@@ -17,10 +16,7 @@ export class CodexTool extends BaseTool {
|
|
|
17
16
|
configPath;
|
|
18
17
|
constructor() {
|
|
19
18
|
super();
|
|
20
|
-
|
|
21
|
-
// - macOS/Linux: ~/.codex/config.json
|
|
22
|
-
// - Windows: %USERPROFILE%\.codex\config.json
|
|
23
|
-
this.configPath = join(homedir(), '.codex', 'config.json');
|
|
19
|
+
this.configPath = join(homedir(), '.codex', 'config.toml');
|
|
24
20
|
}
|
|
25
21
|
static getInstance() {
|
|
26
22
|
if (!CodexTool.instance) {
|
|
@@ -28,24 +24,17 @@ export class CodexTool extends BaseTool {
|
|
|
28
24
|
}
|
|
29
25
|
return CodexTool.instance;
|
|
30
26
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Ensure config directory exists
|
|
33
|
-
*/
|
|
34
27
|
ensureDir(filePath) {
|
|
35
28
|
const dir = dirname(filePath);
|
|
36
29
|
if (!existsSync(dir)) {
|
|
37
30
|
mkdirSync(dir, { recursive: true });
|
|
38
31
|
}
|
|
39
32
|
}
|
|
40
|
-
// ==================== ITool 接口实现 ====================
|
|
41
|
-
/**
|
|
42
|
-
* Read codex config
|
|
43
|
-
*/
|
|
44
33
|
getConfig() {
|
|
45
34
|
try {
|
|
46
35
|
if (existsSync(this.configPath)) {
|
|
47
36
|
const content = readFileSync(this.configPath, 'utf-8');
|
|
48
|
-
return
|
|
37
|
+
return parse(content);
|
|
49
38
|
}
|
|
50
39
|
}
|
|
51
40
|
catch (error) {
|
|
@@ -53,90 +42,157 @@ export class CodexTool extends BaseTool {
|
|
|
53
42
|
}
|
|
54
43
|
return {};
|
|
55
44
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Save codex config
|
|
58
|
-
*/
|
|
59
45
|
saveConfig(config) {
|
|
60
46
|
try {
|
|
61
47
|
this.ensureDir(this.configPath);
|
|
62
|
-
writeFileSync(this.configPath,
|
|
48
|
+
writeFileSync(this.configPath, stringify(config), 'utf-8');
|
|
63
49
|
}
|
|
64
50
|
catch (error) {
|
|
65
51
|
throw new Error(`Failed to save Codex config: ${error}`);
|
|
66
52
|
}
|
|
67
53
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*/
|
|
71
|
-
async fetchAvailableModels(apiKey) {
|
|
72
|
-
try {
|
|
73
|
-
const controller = new AbortController();
|
|
74
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
75
|
-
const response = await fetch(`${DEFAULT_BASE_URL}/models`, {
|
|
76
|
-
method: 'GET',
|
|
77
|
-
headers: {
|
|
78
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
79
|
-
'Content-Type': 'application/json',
|
|
80
|
-
},
|
|
81
|
-
signal: controller.signal,
|
|
82
|
-
});
|
|
83
|
-
clearTimeout(timeoutId);
|
|
84
|
-
if (!response.ok) {
|
|
85
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
86
|
-
}
|
|
87
|
-
const data = await response.json();
|
|
88
|
-
if (data && data.data && Array.isArray(data.data)) {
|
|
89
|
-
return data.data.map((model) => model.id);
|
|
90
|
-
}
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
console.warn('Failed to fetch available models:', error);
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
54
|
+
async fetchAvailableModels() {
|
|
55
|
+
return modelService.fetchModels();
|
|
97
56
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Save model config to Codex
|
|
100
|
-
*/
|
|
101
57
|
async saveModelConfig(config) {
|
|
102
58
|
const modelConfig = config;
|
|
103
|
-
// Read current config (preserve user's other settings)
|
|
104
59
|
const currentConfig = this.getConfig();
|
|
105
|
-
|
|
60
|
+
const providerKey = 'x-aio';
|
|
106
61
|
const newConfig = {
|
|
107
62
|
...currentConfig,
|
|
108
63
|
model: modelConfig.model,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
64
|
+
model_provider: providerKey,
|
|
65
|
+
model_providers: {
|
|
66
|
+
...currentConfig.model_providers,
|
|
67
|
+
[providerKey]: {
|
|
68
|
+
name: 'X-AIO',
|
|
69
|
+
base_url: modelConfig.baseUrl || DEFAULT_BASE_URL,
|
|
70
|
+
env_key: X_AIO_CODE_API_KEY_ENV,
|
|
71
|
+
wire_api: 'responses',
|
|
72
|
+
},
|
|
113
73
|
},
|
|
114
74
|
};
|
|
115
75
|
this.saveConfig(newConfig);
|
|
76
|
+
this.setApiKeyEnv(modelConfig.apiKey);
|
|
77
|
+
}
|
|
78
|
+
setApiKeyEnv(apiKey) {
|
|
79
|
+
const isWindows = process.platform === 'win32';
|
|
80
|
+
if (isWindows) {
|
|
81
|
+
const escapedKey = apiKey.replace(/'/g, '\'\'');
|
|
82
|
+
const psCommand = `[Environment]::SetEnvironmentVariable('${X_AIO_CODE_API_KEY_ENV}', '${escapedKey}', 'User')`;
|
|
83
|
+
try {
|
|
84
|
+
execSync(`powershell -Command "${psCommand}"`, { stdio: 'ignore' });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
console.warn('Failed to set Windows environment variable');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const shellRcPath = this.getShellRcPath();
|
|
92
|
+
if (shellRcPath) {
|
|
93
|
+
let content = existsSync(shellRcPath) ? readFileSync(shellRcPath, 'utf-8') : '';
|
|
94
|
+
const exportLine = `export ${X_AIO_CODE_API_KEY_ENV}="${apiKey}"`;
|
|
95
|
+
const regex = new RegExp(`^export ${X_AIO_CODE_API_KEY_ENV}=.*$`, 'm');
|
|
96
|
+
if (regex.test(content)) {
|
|
97
|
+
content = content.replace(regex, exportLine);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
content = content ? `${content}\n${exportLine}\n` : `${exportLine}\n`;
|
|
101
|
+
}
|
|
102
|
+
writeFileSync(shellRcPath, content, 'utf-8');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
process.env[X_AIO_CODE_API_KEY_ENV] = apiKey;
|
|
106
|
+
}
|
|
107
|
+
getShellRcPath() {
|
|
108
|
+
const home = homedir();
|
|
109
|
+
const shell = process.env.SHELL || '';
|
|
110
|
+
if (shell.includes('zsh')) {
|
|
111
|
+
const zshrc = join(home, '.zshrc');
|
|
112
|
+
const zprofile = join(home, '.zprofile');
|
|
113
|
+
if (existsSync(zshrc))
|
|
114
|
+
return zshrc;
|
|
115
|
+
if (existsSync(zprofile))
|
|
116
|
+
return zprofile;
|
|
117
|
+
return zshrc;
|
|
118
|
+
}
|
|
119
|
+
if (shell.includes('bash')) {
|
|
120
|
+
const bashrc = join(home, '.bashrc');
|
|
121
|
+
const bashProfile = join(home, '.bash_profile');
|
|
122
|
+
if (existsSync(bashrc))
|
|
123
|
+
return bashrc;
|
|
124
|
+
if (existsSync(bashProfile))
|
|
125
|
+
return bashProfile;
|
|
126
|
+
return bashrc;
|
|
127
|
+
}
|
|
128
|
+
return join(home, '.profile');
|
|
129
|
+
}
|
|
130
|
+
getEnvValue(envKey) {
|
|
131
|
+
if (process.env[envKey]) {
|
|
132
|
+
return process.env[envKey];
|
|
133
|
+
}
|
|
134
|
+
if (process.platform === 'win32') {
|
|
135
|
+
try {
|
|
136
|
+
const result = execSync(`powershell -Command "[Environment]::GetEnvironmentVariable('${envKey}', 'User')"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
137
|
+
if (result && result !== '') {
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Ignore errors
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
try {
|
|
147
|
+
const shellRcPath = this.getShellRcPath();
|
|
148
|
+
if (existsSync(shellRcPath)) {
|
|
149
|
+
const content = readFileSync(shellRcPath, 'utf-8');
|
|
150
|
+
const regex = new RegExp(`^export ${envKey}=["']?([^"'\\n]+)["']?`, 'm');
|
|
151
|
+
const match = content.match(regex);
|
|
152
|
+
if (match && match[1]) {
|
|
153
|
+
return match[1];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Ignore errors
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
116
162
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Get current model config
|
|
119
|
-
*/
|
|
120
163
|
getModelConfig() {
|
|
121
164
|
const config = this.getConfig();
|
|
122
|
-
|
|
165
|
+
const providerKey = config.model_provider;
|
|
166
|
+
if (!providerKey || !config.model_providers?.[providerKey]) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const provider = config.model_providers[providerKey];
|
|
170
|
+
const envKey = provider.env_key;
|
|
171
|
+
const apiKey = envKey ? this.getEnvValue(envKey) : undefined;
|
|
172
|
+
if (!apiKey) {
|
|
123
173
|
return null;
|
|
124
174
|
}
|
|
125
175
|
return {
|
|
126
|
-
apiKey
|
|
176
|
+
apiKey,
|
|
127
177
|
model: config.model || '',
|
|
128
|
-
baseUrl:
|
|
178
|
+
baseUrl: provider.base_url || '',
|
|
129
179
|
};
|
|
130
180
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Clear model config
|
|
133
|
-
*/
|
|
134
181
|
clearModelConfig() {
|
|
135
182
|
const currentConfig = this.getConfig();
|
|
136
|
-
|
|
137
|
-
|
|
183
|
+
const providerKey = 'x-aio';
|
|
184
|
+
if (currentConfig.model_provider === providerKey) {
|
|
185
|
+
delete currentConfig.model;
|
|
186
|
+
delete currentConfig.model_provider;
|
|
187
|
+
}
|
|
188
|
+
if (currentConfig.model_providers?.[providerKey]) {
|
|
189
|
+
delete currentConfig.model_providers[providerKey];
|
|
190
|
+
if (Object.keys(currentConfig.model_providers).length === 0) {
|
|
191
|
+
delete currentConfig.model_providers;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
138
194
|
this.saveConfig(currentConfig);
|
|
195
|
+
delete process.env[X_AIO_CODE_API_KEY_ENV];
|
|
139
196
|
}
|
|
140
197
|
}
|
|
141
|
-
// 单例导出
|
|
142
198
|
export const codexTool = CodexTool.getInstance();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModelInfo } from '../utils/fetch-models.js';
|
|
1
2
|
import type { IPlugin } from './base-tool.js';
|
|
2
3
|
import { BaseTool } from './base-tool.js';
|
|
3
4
|
export interface OhMyOpenCodeConfig {
|
|
@@ -18,12 +19,7 @@ export declare const OPENCODE_DEFAULT_CONFIG: {
|
|
|
18
19
|
VISION_CONTEXT: number;
|
|
19
20
|
VISION_OUTPUT: number;
|
|
20
21
|
};
|
|
21
|
-
export
|
|
22
|
-
id: string;
|
|
23
|
-
name?: string;
|
|
24
|
-
context_length?: number;
|
|
25
|
-
max_output_tokens?: number;
|
|
26
|
-
}
|
|
22
|
+
export type { ModelInfo } from '../utils/fetch-models.js';
|
|
27
23
|
export interface OpenCodeModelEntry {
|
|
28
24
|
name?: string;
|
|
29
25
|
limit?: {
|
|
@@ -70,7 +66,6 @@ export declare class OpenCodeTool extends BaseTool {
|
|
|
70
66
|
readonly installCommand = "npm install -g opencode-ai";
|
|
71
67
|
readonly configPath: string;
|
|
72
68
|
private ohMyOpenCodeConfigPath;
|
|
73
|
-
private cachedModels;
|
|
74
69
|
private constructor();
|
|
75
70
|
static getInstance(): OpenCodeTool;
|
|
76
71
|
/**
|
|
@@ -88,11 +83,7 @@ export declare class OpenCodeTool extends BaseTool {
|
|
|
88
83
|
/**
|
|
89
84
|
* Fetch available models from API
|
|
90
85
|
*/
|
|
91
|
-
fetchAvailableModels(
|
|
92
|
-
/**
|
|
93
|
-
* Fetch available models with full info from API
|
|
94
|
-
*/
|
|
95
|
-
fetchAvailableModelsWithInfo(apiKey: string): Promise<ModelInfo[]>;
|
|
86
|
+
fetchAvailableModels(): Promise<string[]>;
|
|
96
87
|
/**
|
|
97
88
|
* Check if model is a vision model (contains "VL" or ends with "V")
|
|
98
89
|
*/
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { configManager } from '../config.js';
|
|
5
|
+
import { modelService } from '../model-service.js';
|
|
5
6
|
import { BaseTool } from './base-tool.js';
|
|
6
7
|
// X-AIO Provider ID
|
|
7
8
|
const XAIO_PROVIDER_ID = 'xaio';
|
|
@@ -40,7 +41,6 @@ export class OpenCodeTool extends BaseTool {
|
|
|
40
41
|
installCommand = 'npm install -g opencode-ai';
|
|
41
42
|
configPath;
|
|
42
43
|
ohMyOpenCodeConfigPath;
|
|
43
|
-
cachedModels = [];
|
|
44
44
|
constructor() {
|
|
45
45
|
super();
|
|
46
46
|
// OpenCode config file paths (cross-platform support)
|
|
@@ -96,45 +96,8 @@ export class OpenCodeTool extends BaseTool {
|
|
|
96
96
|
/**
|
|
97
97
|
* Fetch available models from API
|
|
98
98
|
*/
|
|
99
|
-
async fetchAvailableModels(
|
|
100
|
-
|
|
101
|
-
return models.map(m => m.id);
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Fetch available models with full info from API
|
|
105
|
-
*/
|
|
106
|
-
async fetchAvailableModelsWithInfo(apiKey) {
|
|
107
|
-
try {
|
|
108
|
-
const controller = new AbortController();
|
|
109
|
-
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
110
|
-
const response = await fetch(`${OPENCODE_DEFAULT_CONFIG.BASE_URL}/models`, {
|
|
111
|
-
method: 'GET',
|
|
112
|
-
headers: {
|
|
113
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
114
|
-
'Content-Type': 'application/json',
|
|
115
|
-
},
|
|
116
|
-
signal: controller.signal,
|
|
117
|
-
});
|
|
118
|
-
clearTimeout(timeoutId);
|
|
119
|
-
if (!response.ok) {
|
|
120
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
121
|
-
}
|
|
122
|
-
const data = await response.json();
|
|
123
|
-
if (data && data.data && Array.isArray(data.data)) {
|
|
124
|
-
this.cachedModels = data.data.map((model) => ({
|
|
125
|
-
id: model.id,
|
|
126
|
-
name: model.name || model.id,
|
|
127
|
-
context_length: model.context_length,
|
|
128
|
-
max_output_tokens: model.max_output_tokens,
|
|
129
|
-
}));
|
|
130
|
-
return this.cachedModels;
|
|
131
|
-
}
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
console.warn('Failed to fetch available models:', error);
|
|
136
|
-
return [];
|
|
137
|
-
}
|
|
99
|
+
async fetchAvailableModels() {
|
|
100
|
+
return modelService.fetchModels();
|
|
138
101
|
}
|
|
139
102
|
/**
|
|
140
103
|
* Check if model is a vision model (contains "VL" or ends with "V")
|
|
@@ -183,10 +146,7 @@ export class OpenCodeTool extends BaseTool {
|
|
|
183
146
|
async saveModelConfig(config) {
|
|
184
147
|
const modelConfig = config;
|
|
185
148
|
// Fetch all available models from API
|
|
186
|
-
|
|
187
|
-
if (modelInfos.length === 0) {
|
|
188
|
-
modelInfos = await this.fetchAvailableModelsWithInfo(modelConfig.apiKey);
|
|
189
|
-
}
|
|
149
|
+
const modelInfos = await modelService.fetchModelsWithInfo();
|
|
190
150
|
// Build models config from API response
|
|
191
151
|
const modelsConfig = this.buildModelsConfig(modelInfos);
|
|
192
152
|
// If no models fetched, at least include the selected ones
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const CODE_PLAN_MODEL_LIST_API = "https://dashboard.x-aio.com/api/index_view/code_plan_model_list";
|
|
2
|
+
export interface ModelInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
context_length?: number;
|
|
6
|
+
max_output_tokens?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function fetchModelsFromApi(): Promise<ModelInfo[]>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const CODE_PLAN_MODEL_LIST_API = 'https://dashboard.x-aio.com/api/index_view/code_plan_model_list';
|
|
2
|
+
export async function fetchModelsFromApi() {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetch(CODE_PLAN_MODEL_LIST_API, {
|
|
7
|
+
method: 'GET',
|
|
8
|
+
signal: controller.signal,
|
|
9
|
+
});
|
|
10
|
+
clearTimeout(timeoutId);
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
13
|
+
}
|
|
14
|
+
const responseData = await response.json();
|
|
15
|
+
const models = responseData?.data;
|
|
16
|
+
if (models && Array.isArray(models)) {
|
|
17
|
+
return models.map((m) => ({
|
|
18
|
+
id: m.real_model_name,
|
|
19
|
+
name: m.real_model_name,
|
|
20
|
+
context_length: m.context ? m.context * 1000 : undefined,
|
|
21
|
+
max_output_tokens: m.context ? m.context * 500 : undefined,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
clearTimeout(timeoutId);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -358,6 +358,9 @@ export class ToolMenu {
|
|
|
358
358
|
}
|
|
359
359
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
360
360
|
spinner.succeed(chalk.green(i18n.t('wizard.config_loaded', { tool: toolRegistry.getTool(toolName)?.displayName })));
|
|
361
|
+
if (toolName === 'codex' && process.platform === 'win32') {
|
|
362
|
+
console.log(chalk.yellow(` ${i18n.t('wizard.restart_terminal_hint')}`));
|
|
363
|
+
}
|
|
361
364
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
362
365
|
}
|
|
363
366
|
catch (error) {
|
|
@@ -497,11 +500,9 @@ export class ToolMenu {
|
|
|
497
500
|
}).start();
|
|
498
501
|
let result;
|
|
499
502
|
if (toolName === 'opencode') {
|
|
500
|
-
// OpenCode: 刷新并同步到配置文件
|
|
501
503
|
result = await modelService.refreshAndSyncToOpenCode();
|
|
502
504
|
}
|
|
503
505
|
else {
|
|
504
|
-
// 其他工具: 仅刷新缓存
|
|
505
506
|
result = await modelService.refreshModels();
|
|
506
507
|
}
|
|
507
508
|
if (result.success) {
|
|
@@ -509,6 +510,9 @@ export class ToolMenu {
|
|
|
509
510
|
}
|
|
510
511
|
else {
|
|
511
512
|
spinner.fail(i18n.t('wizard.refresh_models_failed'));
|
|
513
|
+
if (result.error) {
|
|
514
|
+
console.log(chalk.red(` Error: ${result.error}`));
|
|
515
|
+
}
|
|
512
516
|
}
|
|
513
517
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
514
518
|
}
|
package/dist/locales/en_US.json
CHANGED
|
@@ -201,7 +201,8 @@
|
|
|
201
201
|
"prompt_refresh_models": "Refresh model list now? (Update available models to config)",
|
|
202
202
|
"refreshing_models": "Refreshing model list...",
|
|
203
203
|
"models_refreshed": "Model list refreshed, {{count}} models synced",
|
|
204
|
-
"refresh_models_failed": "Failed to refresh model list"
|
|
204
|
+
"refresh_models_failed": "Failed to refresh model list",
|
|
205
|
+
"restart_terminal_hint": "Environment variable set. Please restart your terminal before using Codex"
|
|
205
206
|
},
|
|
206
207
|
"doctor": {
|
|
207
208
|
"checking": "Running health check...",
|
package/dist/locales/zh_CN.json
CHANGED
|
@@ -201,7 +201,8 @@
|
|
|
201
201
|
"prompt_refresh_models": "是否立即刷新模型列表?(更新可用模型到配置)",
|
|
202
202
|
"refreshing_models": "正在刷新模型列表...",
|
|
203
203
|
"models_refreshed": "模型列表已刷新,共同步 {{count}} 个模型",
|
|
204
|
-
"refresh_models_failed": "刷新模型列表失败"
|
|
204
|
+
"refresh_models_failed": "刷新模型列表失败",
|
|
205
|
+
"restart_terminal_hint": "环境变量已设置,请重新打开终端后再使用 Codex"
|
|
205
206
|
},
|
|
206
207
|
"doctor": {
|
|
207
208
|
"checking": "正在进行健康检查...",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-all-in-one/coding-helper",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "X All In One Coding Helper",
|
|
6
6
|
"author": "X.AIO",
|
|
7
7
|
"homepage": "https://docs.x-aio.com/zh/docs",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
38
|
"open": "^11.0.0",
|
|
39
39
|
"ora": "^8.0.1",
|
|
40
|
+
"smol-toml": "^1.6.0",
|
|
40
41
|
"terminal-link": "^5.0.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|