duojie-helper 0.2.16 → 0.2.18
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/bin/cli.js +5 -1
- package/package.json +1 -1
- package/src/index.js +32 -6
- package/src/tools/cline.js +1 -1
- package/src/tools/codex.js +1 -1
- package/src/tools/cursor.js +1 -1
- package/src/tools/droid.js +16 -1
- package/src/tools/openclaw.js +128 -67
package/bin/cli.js
CHANGED
|
@@ -4,14 +4,18 @@ import { Command } from 'commander';
|
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import ora from 'ora';
|
|
7
|
+
import { createRequire } from 'module';
|
|
7
8
|
import { configureAll, configureTool, listTools, showStatus, revokeKey, fetchModels, getModels } from '../src/index.js';
|
|
8
9
|
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
|
|
9
13
|
const program = new Command();
|
|
10
14
|
|
|
11
15
|
program
|
|
12
16
|
.name('duojie')
|
|
13
17
|
.description('🎮 Duojie API 一键配置助手 - 支持主流 AI 编程工具')
|
|
14
|
-
.version(
|
|
18
|
+
.version(pkg.version);
|
|
15
19
|
|
|
16
20
|
// 默认命令 - 交互式配置
|
|
17
21
|
program
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -52,17 +52,41 @@ export function getApiBaseForProtocol(protocol = '') {
|
|
|
52
52
|
return withoutV1(API_CONFIG.baseUrl);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 从 pricing 页面获取公开模型 ID 集合
|
|
57
|
+
* 返回 null 表示获取失败,调用方应跳过过滤
|
|
58
|
+
*/
|
|
59
|
+
async function fetchPublicModelIds(apiKey) {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`${API_CONFIG.baseUrl}/pricing`, {
|
|
62
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) return null;
|
|
65
|
+
const html = await response.text();
|
|
66
|
+
const ids = new Set();
|
|
67
|
+
for (const m of (html.match(/###\s+([^\n\r]+)/g) || [])) {
|
|
68
|
+
ids.add(m.replace(/^###\s+/, '').trim());
|
|
69
|
+
}
|
|
70
|
+
return ids.size > 0 ? ids : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
/**
|
|
56
77
|
* 从 API 获取模型列表
|
|
57
78
|
*/
|
|
58
79
|
export async function fetchModels(apiKey) {
|
|
59
80
|
try {
|
|
60
|
-
const response = await
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
const [response, publicIds] = await Promise.all([
|
|
82
|
+
fetch(`${API_CONFIG.baseUrl}/v1/models`, {
|
|
83
|
+
headers: {
|
|
84
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
fetchPublicModelIds(apiKey),
|
|
89
|
+
]);
|
|
66
90
|
|
|
67
91
|
if (!response.ok) {
|
|
68
92
|
throw new Error(`HTTP ${response.status}`);
|
|
@@ -79,6 +103,8 @@ export async function fetchModels(apiKey) {
|
|
|
79
103
|
if (data.data && Array.isArray(data.data)) {
|
|
80
104
|
for (const model of data.data) {
|
|
81
105
|
const id = model.id;
|
|
106
|
+
// 跳过未在 pricing 页面公开的隐藏模型
|
|
107
|
+
if (publicIds && !publicIds.has(id)) continue;
|
|
82
108
|
const endpoints = model.supported_endpoint_types || [];
|
|
83
109
|
|
|
84
110
|
// 判断最佳协议
|
package/src/tools/cline.js
CHANGED
|
@@ -48,7 +48,7 @@ ${chalk.cyan('步骤 4:')} 填入以下配置:
|
|
|
48
48
|
${chalk.green('Base URL:')} ${baseUrl}
|
|
49
49
|
${chalk.green('API Key:')} ${apiKey}
|
|
50
50
|
${chalk.green('Model:')} 选择 "Enter model ID" 并输入模型名称
|
|
51
|
-
例如: gpt-5.2 或 claude-
|
|
51
|
+
例如: gpt-5.2 或 claude-sonnet-4-6
|
|
52
52
|
|
|
53
53
|
${chalk.cyan('步骤 5:')} 可选配置:
|
|
54
54
|
• ${chalk.green('Max Tokens:')} 8192 (推荐)
|
package/src/tools/codex.js
CHANGED
package/src/tools/cursor.js
CHANGED
package/src/tools/droid.js
CHANGED
|
@@ -15,6 +15,7 @@ function getDroidConfigPaths() {
|
|
|
15
15
|
return {
|
|
16
16
|
configDir: path.join(home, '.factory'),
|
|
17
17
|
configFile: path.join(home, '.factory', 'config.json'),
|
|
18
|
+
settingsFile: path.join(home, '.factory', 'settings.json'),
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -69,7 +70,8 @@ function generateDisplayName(modelId) {
|
|
|
69
70
|
* @returns {string} provider 名称
|
|
70
71
|
*/
|
|
71
72
|
function determineProvider(modelId, endpoints = []) {
|
|
72
|
-
return 'anthropic';
|
|
73
|
+
if (modelId.startsWith('claude')) return 'anthropic';
|
|
74
|
+
return 'openai';
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/**
|
|
@@ -166,6 +168,19 @@ export async function configureDroid(apiKey) {
|
|
|
166
168
|
// 6. 写入配置文件
|
|
167
169
|
await fs.writeJson(paths.configFile, newConfig, { spaces: 2 });
|
|
168
170
|
|
|
171
|
+
// 7. 清空 settings.json 中的 customModels(如文件和字段存在)
|
|
172
|
+
if (await fs.pathExists(paths.settingsFile)) {
|
|
173
|
+
try {
|
|
174
|
+
const settings = await fs.readJson(paths.settingsFile);
|
|
175
|
+
if (Array.isArray(settings.customModels) && settings.customModels.length > 0) {
|
|
176
|
+
settings.customModels = [];
|
|
177
|
+
await fs.writeJson(paths.settingsFile, settings, { spaces: 2 });
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// 忽略,不影响主流程
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
return {
|
|
170
185
|
success: true,
|
|
171
186
|
message: `已配置 ${duojieModels.length} 个模型 → ${paths.configFile}`,
|
package/src/tools/openclaw.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { getModels, getApiBaseForProtocol } from '../index.js';
|
|
5
5
|
|
|
6
|
+
const DUOJIE_PROVIDER_PREFIX = 'duojie-';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* 获取 OpenClaw 配置路径
|
|
8
10
|
* 支持 macOS / Linux / Windows(os.homedir() 跨平台处理)
|
|
@@ -46,6 +48,34 @@ function buildModelEntry(m, contextWindow, maxTokens) {
|
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
function canonicalizeApi(api) {
|
|
52
|
+
const normalized = `${api || ''}`.toLowerCase().trim();
|
|
53
|
+
if (!normalized) return 'openai-completions';
|
|
54
|
+
if (normalized === 'openai-response') return 'openai-responses';
|
|
55
|
+
if (normalized === 'openai-completion') return 'openai-completions';
|
|
56
|
+
if (normalized === 'openai') return 'openai-completions';
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function providerNameForApi(api) {
|
|
61
|
+
const normalized = canonicalizeApi(api);
|
|
62
|
+
if (normalized === 'anthropic-messages') return 'duojie-claude';
|
|
63
|
+
if (normalized === 'openai-completions') return 'duojie-openai';
|
|
64
|
+
if (normalized === 'openai-responses') return 'duojie-openai-responses';
|
|
65
|
+
return `duojie-${normalized.replace(/[^a-z0-9]+/g, '-')}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stripDuojieKeysFromObject(obj) {
|
|
69
|
+
if (!obj || typeof obj !== 'object') return {};
|
|
70
|
+
const next = { ...obj };
|
|
71
|
+
for (const key of Object.keys(next)) {
|
|
72
|
+
if (`${key}`.startsWith(DUOJIE_PROVIDER_PREFIX)) {
|
|
73
|
+
delete next[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
/**
|
|
50
80
|
* 配置 OpenClaw
|
|
51
81
|
*
|
|
@@ -65,111 +95,125 @@ export async function configureOpenClaw(apiKey) {
|
|
|
65
95
|
? await readConfig(paths.configFile)
|
|
66
96
|
: {};
|
|
67
97
|
|
|
98
|
+
// OpenClaw 配置会做 schema 校验:根级未知字段可能导致校验失败
|
|
99
|
+
// 旧版本写入过 `_duojie`,这里显式剔除
|
|
100
|
+
const { _duojie: _ignoredDuojie, ...baseConfig } = existingConfig || {};
|
|
101
|
+
void _ignoredDuojie;
|
|
102
|
+
|
|
68
103
|
const models = getModels();
|
|
69
104
|
|
|
70
|
-
//
|
|
71
|
-
const claudeModels = (models.claude || []).map(m =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
105
|
+
// 构建各类模型列表(保留 api 协议,后续按 api 拆分 providers)
|
|
106
|
+
const claudeModels = (models.claude || []).map(m => ({
|
|
107
|
+
...m,
|
|
108
|
+
api: canonicalizeApi(m.api || 'anthropic-messages'),
|
|
109
|
+
_entry: buildModelEntry(m, 200000, 64000),
|
|
110
|
+
}));
|
|
111
|
+
const gptModels = (models.gpt || []).map(m => ({
|
|
112
|
+
...m,
|
|
113
|
+
api: canonicalizeApi(m.api || 'openai-completions'),
|
|
114
|
+
_entry: buildModelEntry(m, 128000, 16384),
|
|
115
|
+
}));
|
|
116
|
+
const geminiModels = (models.gemini || []).map(m => ({
|
|
117
|
+
...m,
|
|
118
|
+
api: canonicalizeApi(m.api || 'openai-completions'),
|
|
119
|
+
_entry: buildModelEntry(m, 1000000, 65536),
|
|
120
|
+
}));
|
|
121
|
+
const otherModels = (models.other || []).map(m => ({
|
|
122
|
+
...m,
|
|
123
|
+
api: canonicalizeApi(m.api || 'openai-completions'),
|
|
124
|
+
_entry: buildModelEntry(m, 128000, 16384),
|
|
125
|
+
}));
|
|
75
126
|
|
|
76
|
-
|
|
127
|
+
const allModels = [...claudeModels, ...gptModels, ...geminiModels, ...otherModels];
|
|
128
|
+
|
|
129
|
+
// 构建 providers(按 api 协议分组,避免在同一个 provider 混用不同协议)
|
|
77
130
|
const providers = {};
|
|
131
|
+
const modelIdToProvider = new Map();
|
|
78
132
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
if (geminiModels.length > 0) {
|
|
96
|
-
providers['duojie-gemini'] = {
|
|
97
|
-
baseUrl: getApiBaseForProtocol('anthropic-messages'),
|
|
98
|
-
apiKey,
|
|
99
|
-
api: 'anthropic-messages',
|
|
100
|
-
models: geminiModels,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
if (otherModels.length > 0) {
|
|
104
|
-
providers['duojie-other'] = {
|
|
105
|
-
baseUrl: getApiBaseForProtocol('anthropic-messages'),
|
|
106
|
-
apiKey,
|
|
107
|
-
api: 'anthropic-messages',
|
|
108
|
-
models: otherModels,
|
|
109
|
-
};
|
|
133
|
+
for (const m of allModels) {
|
|
134
|
+
const api = canonicalizeApi(m.api);
|
|
135
|
+
const providerName = providerNameForApi(api);
|
|
136
|
+
|
|
137
|
+
if (!providers[providerName]) {
|
|
138
|
+
providers[providerName] = {
|
|
139
|
+
baseUrl: getApiBaseForProtocol(api),
|
|
140
|
+
apiKey,
|
|
141
|
+
api,
|
|
142
|
+
models: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
providers[providerName].models.push(m._entry);
|
|
147
|
+
modelIdToProvider.set(m.id, providerName);
|
|
110
148
|
}
|
|
111
149
|
|
|
112
150
|
// 默认模型:优先 gpt-5.2(如存在)
|
|
113
|
-
const gpt52 =
|
|
151
|
+
const gpt52 = gptModels.find(m => m.id === 'gpt-5.2')?.id || null;
|
|
114
152
|
let defaultProvider = 'duojie-claude';
|
|
115
|
-
let defaultModelId = '
|
|
153
|
+
let defaultModelId = claudeModels[0]?.id || allModels[0]?.id || '';
|
|
116
154
|
|
|
117
155
|
if (gpt52) {
|
|
118
|
-
defaultProvider = '
|
|
156
|
+
defaultProvider = modelIdToProvider.get(gpt52) || providerNameForApi('openai-completions');
|
|
119
157
|
defaultModelId = gpt52;
|
|
120
158
|
} else if (claudeModels.length > 0) {
|
|
121
|
-
defaultProvider = 'duojie-claude';
|
|
122
159
|
defaultModelId = (
|
|
123
|
-
claudeModels.find(m => m.id.includes('opus-4-6') && m.id.includes('kiro')) ||
|
|
124
160
|
claudeModels.find(m => m.id.includes('opus-4-6')) ||
|
|
125
|
-
claudeModels.find(m => m.id.includes('opus') && m.id.includes('kiro')) ||
|
|
126
161
|
claudeModels.find(m => m.id.includes('opus')) ||
|
|
127
162
|
claudeModels.find(m => m.id.includes('sonnet')) ||
|
|
128
163
|
claudeModels[0]
|
|
129
164
|
).id;
|
|
165
|
+
defaultProvider = modelIdToProvider.get(defaultModelId) || 'duojie-claude';
|
|
130
166
|
} else if (gptModels.length > 0) {
|
|
131
|
-
defaultProvider = 'duojie-gpt';
|
|
132
167
|
defaultModelId = gptModels[0].id;
|
|
168
|
+
defaultProvider = modelIdToProvider.get(defaultModelId) || providerNameForApi(gptModels[0].api);
|
|
169
|
+
} else if (geminiModels.length > 0) {
|
|
170
|
+
defaultModelId = geminiModels[0].id;
|
|
171
|
+
defaultProvider = modelIdToProvider.get(defaultModelId) || providerNameForApi(geminiModels[0].api);
|
|
172
|
+
} else if (otherModels.length > 0) {
|
|
173
|
+
defaultModelId = otherModels[0].id;
|
|
174
|
+
defaultProvider = modelIdToProvider.get(defaultModelId) || providerNameForApi(otherModels[0].api);
|
|
175
|
+
} else if (allModels.length > 0) {
|
|
176
|
+
defaultModelId = allModels[0].id;
|
|
177
|
+
defaultProvider = modelIdToProvider.get(defaultModelId) || providerNameForApi(allModels[0].api);
|
|
133
178
|
}
|
|
134
179
|
|
|
135
180
|
// 构建模型 allowlist(用于 /model 切换)
|
|
136
181
|
const modelAllowList = {};
|
|
137
|
-
for (const m of
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
182
|
+
for (const m of allModels) {
|
|
183
|
+
const providerName = modelIdToProvider.get(m.id) || providerNameForApi(m.api);
|
|
184
|
+
modelAllowList[`${providerName}/${m.id}`] = { alias: m.name || m.id };
|
|
185
|
+
}
|
|
141
186
|
|
|
142
|
-
const totalModels =
|
|
187
|
+
const totalModels = allModels.length;
|
|
143
188
|
|
|
144
189
|
// 合并配置,保留用户已有的非 Duojie 设置
|
|
190
|
+
const existingProviders = stripDuojieKeysFromObject(baseConfig.models?.providers);
|
|
191
|
+
const existingAllowList = stripDuojieKeysFromObject(baseConfig.agents?.defaults?.models);
|
|
192
|
+
|
|
145
193
|
const newConfig = {
|
|
146
|
-
...
|
|
194
|
+
...baseConfig,
|
|
147
195
|
models: {
|
|
148
|
-
...
|
|
196
|
+
...baseConfig.models,
|
|
149
197
|
mode: 'merge',
|
|
150
198
|
providers: {
|
|
151
|
-
...
|
|
199
|
+
...existingProviders,
|
|
152
200
|
...providers,
|
|
153
201
|
},
|
|
154
202
|
},
|
|
155
203
|
agents: {
|
|
156
|
-
...
|
|
204
|
+
...baseConfig.agents,
|
|
157
205
|
defaults: {
|
|
158
|
-
...
|
|
206
|
+
...baseConfig.agents?.defaults,
|
|
159
207
|
model: {
|
|
160
|
-
...
|
|
208
|
+
...baseConfig.agents?.defaults?.model,
|
|
161
209
|
primary: `${defaultProvider}/${defaultModelId}`,
|
|
162
210
|
},
|
|
163
211
|
models: {
|
|
164
|
-
...
|
|
212
|
+
...existingAllowList,
|
|
165
213
|
...modelAllowList,
|
|
166
214
|
},
|
|
167
215
|
},
|
|
168
216
|
},
|
|
169
|
-
_duojie: {
|
|
170
|
-
configuredAt: new Date().toISOString(),
|
|
171
|
-
version: '0.2.8',
|
|
172
|
-
},
|
|
173
217
|
};
|
|
174
218
|
|
|
175
219
|
await fs.writeJson(paths.configFile, newConfig, { spaces: 2 });
|
|
@@ -196,10 +240,11 @@ configureOpenClaw.checkStatus = async function () {
|
|
|
196
240
|
|
|
197
241
|
if (await fs.pathExists(paths.configFile)) {
|
|
198
242
|
const config = await readConfig(paths.configFile);
|
|
199
|
-
|
|
243
|
+
const providerKeys = Object.keys(config.models?.providers || {});
|
|
244
|
+
if (providerKeys.some(k => `${k}`.startsWith(DUOJIE_PROVIDER_PREFIX))) {
|
|
200
245
|
return { configured: true, message: '已配置 Duojie API' };
|
|
201
246
|
}
|
|
202
|
-
if (
|
|
247
|
+
if (providerKeys.length > 0) {
|
|
203
248
|
return { configured: true, message: '已配置(非 Duojie)' };
|
|
204
249
|
}
|
|
205
250
|
}
|
|
@@ -217,12 +262,28 @@ configureOpenClaw.revoke = async function () {
|
|
|
217
262
|
const config = await readConfig(paths.configFile);
|
|
218
263
|
|
|
219
264
|
if (config.models?.providers) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
265
|
+
for (const key of Object.keys(config.models.providers)) {
|
|
266
|
+
if (`${key}`.startsWith(DUOJIE_PROVIDER_PREFIX)) {
|
|
267
|
+
delete config.models.providers[key];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (config.agents?.defaults?.models) {
|
|
273
|
+
for (const key of Object.keys(config.agents.defaults.models)) {
|
|
274
|
+
if (`${key}`.startsWith(DUOJIE_PROVIDER_PREFIX)) {
|
|
275
|
+
delete config.agents.defaults.models[key];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (config.agents?.defaults?.model?.primary?.startsWith?.(DUOJIE_PROVIDER_PREFIX)) {
|
|
281
|
+
delete config.agents.defaults.model.primary;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (config._duojie) {
|
|
285
|
+
delete config._duojie;
|
|
224
286
|
}
|
|
225
|
-
delete config._duojie;
|
|
226
287
|
|
|
227
288
|
await fs.writeJson(paths.configFile, config, { spaces: 2 });
|
|
228
289
|
}
|