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 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('0.2.3');
18
+ .version(pkg.version);
15
19
 
16
20
  // 默认命令 - 交互式配置
17
21
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duojie-helper",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Duojie API 一键配置助手 - 支持 Claude Code, Droid, OpenClaw, Cursor, Cline 等主流 AI 编程工具",
5
5
  "type": "module",
6
6
  "bin": {
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 fetch(`${API_CONFIG.baseUrl}/v1/models`, {
61
- headers: {
62
- 'Authorization': `Bearer ${apiKey}`,
63
- 'Content-Type': 'application/json',
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
  // 判断最佳协议
@@ -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-opus-4-6-kiro
51
+ 例如: gpt-5.2 或 claude-sonnet-4-6
52
52
 
53
53
  ${chalk.cyan('步骤 5:')} 可选配置:
54
54
  • ${chalk.green('Max Tokens:')} 8192 (推荐)
@@ -5,7 +5,7 @@ import TOML from '@iarna/toml';
5
5
  import { getApiBaseForProtocol } from '../index.js';
6
6
 
7
7
  const DUOJIE_PROVIDER_ID = 'duojie';
8
- const DEFAULT_MODEL = 'gpt-5.2';
8
+ const DEFAULT_MODEL = 'gpt-5.4';
9
9
 
10
10
  /**
11
11
  * 获取 Codex CLI 配置路径
@@ -48,7 +48,7 @@ ${chalk.cyan('步骤 3:')} 填入以下配置:
48
48
  ${chalk.green('Override OpenAI Base URL:')} ${baseUrl}
49
49
  ${chalk.green('Model Name:')} 输入模型名称(注意大小写)
50
50
  例如: gpt-5.2
51
- 或: claude-opus-4-6-kiro
51
+ 或: claude-sonnet-4-6
52
52
 
53
53
  ${chalk.cyan('步骤 4:')} 点击保存,然后在模型列表中选择刚添加的模型
54
54
 
@@ -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}`,
@@ -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 => buildModelEntry(m, 200000, 64000));
72
- const gptModels = (models.gpt || []).map(m => buildModelEntry(m, 128000, 16384));
73
- const geminiModels = (models.gemini || []).map(m => buildModelEntry(m, 1000000, 65536));
74
- const otherModels = (models.other || []).map(m => buildModelEntry(m, 128000, 16384));
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
- // 构建 providers(按模型类型分组,api 协议对应文档规范)
127
+ const allModels = [...claudeModels, ...gptModels, ...geminiModels, ...otherModels];
128
+
129
+ // 构建 providers(按 api 协议分组,避免在同一个 provider 混用不同协议)
77
130
  const providers = {};
131
+ const modelIdToProvider = new Map();
78
132
 
79
- if (claudeModels.length > 0) {
80
- providers['duojie-claude'] = {
81
- baseUrl: getApiBaseForProtocol('anthropic-messages'),
82
- apiKey,
83
- api: 'anthropic-messages',
84
- models: claudeModels,
85
- };
86
- }
87
- if (gptModels.length > 0) {
88
- providers['duojie-gpt'] = {
89
- baseUrl: getApiBaseForProtocol('anthropic-messages'),
90
- apiKey,
91
- api: 'anthropic-messages',
92
- models: gptModels,
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 = (models.gpt || []).find(m => m.id === 'gpt-5.2')?.id || null;
151
+ const gpt52 = gptModels.find(m => m.id === 'gpt-5.2')?.id || null;
114
152
  let defaultProvider = 'duojie-claude';
115
- let defaultModelId = 'claude-opus-4-6-kiro';
153
+ let defaultModelId = claudeModels[0]?.id || allModels[0]?.id || '';
116
154
 
117
155
  if (gpt52) {
118
- defaultProvider = 'duojie-gpt';
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 claudeModels) modelAllowList[`duojie-claude/${m.id}`] = { alias: m.name };
138
- for (const m of gptModels) modelAllowList[`duojie-gpt/${m.id}`] = { alias: m.name };
139
- for (const m of geminiModels) modelAllowList[`duojie-gemini/${m.id}`] = { alias: m.name };
140
- for (const m of otherModels) modelAllowList[`duojie-other/${m.id}`] = { alias: m.name };
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 = claudeModels.length + gptModels.length + geminiModels.length + otherModels.length;
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
- ...existingConfig,
194
+ ...baseConfig,
147
195
  models: {
148
- ...existingConfig.models,
196
+ ...baseConfig.models,
149
197
  mode: 'merge',
150
198
  providers: {
151
- ...existingConfig.models?.providers,
199
+ ...existingProviders,
152
200
  ...providers,
153
201
  },
154
202
  },
155
203
  agents: {
156
- ...existingConfig.agents,
204
+ ...baseConfig.agents,
157
205
  defaults: {
158
- ...existingConfig.agents?.defaults,
206
+ ...baseConfig.agents?.defaults,
159
207
  model: {
160
- ...existingConfig.agents?.defaults?.model,
208
+ ...baseConfig.agents?.defaults?.model,
161
209
  primary: `${defaultProvider}/${defaultModelId}`,
162
210
  },
163
211
  models: {
164
- ...existingConfig.agents?.defaults?.models,
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
- if (config._duojie || config.models?.providers?.['duojie-claude']) {
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 (config.models?.providers) {
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
- delete config.models.providers['duojie-claude'];
221
- delete config.models.providers['duojie-gpt'];
222
- delete config.models.providers['duojie-gemini'];
223
- delete config.models.providers['duojie-other'];
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
  }