duojie-helper 0.2.21 → 0.2.23
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/README.md +160 -160
- package/bin/cli.js +328 -328
- package/package.json +53 -53
- package/src/index.js +324 -323
- package/src/tools/claude.js +198 -198
- package/src/tools/cline.js +103 -103
- package/src/tools/codex.js +154 -154
- package/src/tools/continue.js +158 -158
- package/src/tools/cursor.js +99 -99
- package/src/tools/droid.js +262 -261
- package/src/tools/openclaw.js +292 -292
- package/src/tools/opencode.js +216 -216
package/src/tools/openclaw.js
CHANGED
|
@@ -1,292 +1,292 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { getModels, getApiBaseForProtocol } from '../index.js';
|
|
5
|
-
|
|
6
|
-
const DUOJIE_PROVIDER_PREFIX = 'duojie-';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 获取 OpenClaw 配置路径
|
|
10
|
-
* 支持 macOS / Linux / Windows(os.homedir() 跨平台处理)
|
|
11
|
-
*/
|
|
12
|
-
function getOpenClawConfigPaths() {
|
|
13
|
-
const home = os.homedir();
|
|
14
|
-
return {
|
|
15
|
-
configDir: path.join(home, '.openclaw'),
|
|
16
|
-
configFile: path.join(home, '.openclaw', 'openclaw.json'),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 读取并解析 openclaw.json(支持 JSON5 注释/尾逗号)
|
|
22
|
-
*/
|
|
23
|
-
async function readConfig(filePath) {
|
|
24
|
-
try {
|
|
25
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
26
|
-
const clean = content
|
|
27
|
-
.replace(/\/\/.*$/gm, '')
|
|
28
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
29
|
-
.replace(/,(\s*[}\]])/g, '$1');
|
|
30
|
-
return JSON.parse(clean);
|
|
31
|
-
} catch {
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 将模型对象转换为 OpenClaw models.providers 格式
|
|
38
|
-
*/
|
|
39
|
-
function buildModelEntry(m, contextWindow, maxTokens) {
|
|
40
|
-
return {
|
|
41
|
-
id: m.id,
|
|
42
|
-
name: m.name || m.id,
|
|
43
|
-
reasoning: false,
|
|
44
|
-
input: ['text', 'image'],
|
|
45
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
46
|
-
contextWindow,
|
|
47
|
-
maxTokens,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
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
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 配置 OpenClaw
|
|
81
|
-
*
|
|
82
|
-
* 使用官方 models.providers 自定义 provider 格式:
|
|
83
|
-
* models.providers.<name>.baseUrl / apiKey / api / models[]
|
|
84
|
-
* agents.defaults.model.primary → "<provider>/<model-id>"
|
|
85
|
-
*
|
|
86
|
-
* 参考文档:https://docs.openclaw.ai/gateway/configuration-reference#custom-providers-and-base-urls
|
|
87
|
-
*/
|
|
88
|
-
export async function configureOpenClaw(apiKey) {
|
|
89
|
-
const paths = getOpenClawConfigPaths();
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
await fs.ensureDir(paths.configDir);
|
|
93
|
-
|
|
94
|
-
const existingConfig = await fs.pathExists(paths.configFile)
|
|
95
|
-
? await readConfig(paths.configFile)
|
|
96
|
-
: {};
|
|
97
|
-
|
|
98
|
-
// OpenClaw 配置会做 schema 校验:根级未知字段可能导致校验失败
|
|
99
|
-
// 旧版本写入过 `_duojie`,这里显式剔除
|
|
100
|
-
const { _duojie: _ignoredDuojie, ...baseConfig } = existingConfig || {};
|
|
101
|
-
void _ignoredDuojie;
|
|
102
|
-
|
|
103
|
-
const models = getModels();
|
|
104
|
-
|
|
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
|
-
}));
|
|
126
|
-
|
|
127
|
-
const allModels = [...claudeModels, ...gptModels, ...geminiModels, ...otherModels];
|
|
128
|
-
|
|
129
|
-
// 构建 providers(按 api 协议分组,避免在同一个 provider 混用不同协议)
|
|
130
|
-
const providers = {};
|
|
131
|
-
const modelIdToProvider = new Map();
|
|
132
|
-
|
|
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);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 默认模型:优先 gpt-5.2(如存在)
|
|
151
|
-
const gpt52 = gptModels.find(m => m.id === 'gpt-5.2')?.id || null;
|
|
152
|
-
let defaultProvider = 'duojie-claude';
|
|
153
|
-
let defaultModelId = claudeModels[0]?.id || allModels[0]?.id || '';
|
|
154
|
-
|
|
155
|
-
if (gpt52) {
|
|
156
|
-
defaultProvider = modelIdToProvider.get(gpt52) || providerNameForApi('openai-completions');
|
|
157
|
-
defaultModelId = gpt52;
|
|
158
|
-
} else if (claudeModels.length > 0) {
|
|
159
|
-
defaultModelId = (
|
|
160
|
-
claudeModels.find(m => m.id.includes('opus-4-6')) ||
|
|
161
|
-
claudeModels.find(m => m.id.includes('opus')) ||
|
|
162
|
-
claudeModels.find(m => m.id.includes('sonnet')) ||
|
|
163
|
-
claudeModels[0]
|
|
164
|
-
).id;
|
|
165
|
-
defaultProvider = modelIdToProvider.get(defaultModelId) || 'duojie-claude';
|
|
166
|
-
} else if (gptModels.length > 0) {
|
|
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);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// 构建模型 allowlist(用于 /model 切换)
|
|
181
|
-
const modelAllowList = {};
|
|
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
|
-
}
|
|
186
|
-
|
|
187
|
-
const totalModels = allModels.length;
|
|
188
|
-
|
|
189
|
-
// 合并配置,保留用户已有的非 Duojie 设置
|
|
190
|
-
const existingProviders = stripDuojieKeysFromObject(baseConfig.models?.providers);
|
|
191
|
-
const existingAllowList = stripDuojieKeysFromObject(baseConfig.agents?.defaults?.models);
|
|
192
|
-
|
|
193
|
-
const newConfig = {
|
|
194
|
-
...baseConfig,
|
|
195
|
-
models: {
|
|
196
|
-
...baseConfig.models,
|
|
197
|
-
mode: 'merge',
|
|
198
|
-
providers: {
|
|
199
|
-
...existingProviders,
|
|
200
|
-
...providers,
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
agents: {
|
|
204
|
-
...baseConfig.agents,
|
|
205
|
-
defaults: {
|
|
206
|
-
...baseConfig.agents?.defaults,
|
|
207
|
-
model: {
|
|
208
|
-
...baseConfig.agents?.defaults?.model,
|
|
209
|
-
primary: `${defaultProvider}/${defaultModelId}`,
|
|
210
|
-
},
|
|
211
|
-
models: {
|
|
212
|
-
...existingAllowList,
|
|
213
|
-
...modelAllowList,
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
await fs.writeJson(paths.configFile, newConfig, { spaces: 2 });
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
success: true,
|
|
223
|
-
message: `已配置 ${totalModels} 个模型 → ${paths.configFile}`,
|
|
224
|
-
configPath: paths.configFile,
|
|
225
|
-
hint: '运行 openclaw gateway restart 使配置生效',
|
|
226
|
-
};
|
|
227
|
-
} catch (error) {
|
|
228
|
-
return {
|
|
229
|
-
success: false,
|
|
230
|
-
message: `配置失败: ${error.message}`,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* 检查 OpenClaw 配置状态
|
|
237
|
-
*/
|
|
238
|
-
configureOpenClaw.checkStatus = async function () {
|
|
239
|
-
const paths = getOpenClawConfigPaths();
|
|
240
|
-
|
|
241
|
-
if (await fs.pathExists(paths.configFile)) {
|
|
242
|
-
const config = await readConfig(paths.configFile);
|
|
243
|
-
const providerKeys = Object.keys(config.models?.providers || {});
|
|
244
|
-
if (providerKeys.some(k => `${k}`.startsWith(DUOJIE_PROVIDER_PREFIX))) {
|
|
245
|
-
return { configured: true, message: '已配置 Duojie API' };
|
|
246
|
-
}
|
|
247
|
-
if (providerKeys.length > 0) {
|
|
248
|
-
return { configured: true, message: '已配置(非 Duojie)' };
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return { configured: false };
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* 撤销 OpenClaw 配置
|
|
257
|
-
*/
|
|
258
|
-
configureOpenClaw.revoke = async function () {
|
|
259
|
-
const paths = getOpenClawConfigPaths();
|
|
260
|
-
|
|
261
|
-
if (await fs.pathExists(paths.configFile)) {
|
|
262
|
-
const config = await readConfig(paths.configFile);
|
|
263
|
-
|
|
264
|
-
if (config.models?.providers) {
|
|
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;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
await fs.writeJson(paths.configFile, config, { spaces: 2 });
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
export default configureOpenClaw;
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { getModels, getApiBaseForProtocol } from '../index.js';
|
|
5
|
+
|
|
6
|
+
const DUOJIE_PROVIDER_PREFIX = 'duojie-';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 获取 OpenClaw 配置路径
|
|
10
|
+
* 支持 macOS / Linux / Windows(os.homedir() 跨平台处理)
|
|
11
|
+
*/
|
|
12
|
+
function getOpenClawConfigPaths() {
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
return {
|
|
15
|
+
configDir: path.join(home, '.openclaw'),
|
|
16
|
+
configFile: path.join(home, '.openclaw', 'openclaw.json'),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 读取并解析 openclaw.json(支持 JSON5 注释/尾逗号)
|
|
22
|
+
*/
|
|
23
|
+
async function readConfig(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
26
|
+
const clean = content
|
|
27
|
+
.replace(/\/\/.*$/gm, '')
|
|
28
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
29
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
30
|
+
return JSON.parse(clean);
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 将模型对象转换为 OpenClaw models.providers 格式
|
|
38
|
+
*/
|
|
39
|
+
function buildModelEntry(m, contextWindow, maxTokens) {
|
|
40
|
+
return {
|
|
41
|
+
id: m.id,
|
|
42
|
+
name: m.name || m.id,
|
|
43
|
+
reasoning: false,
|
|
44
|
+
input: ['text', 'image'],
|
|
45
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
46
|
+
contextWindow,
|
|
47
|
+
maxTokens,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
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
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 配置 OpenClaw
|
|
81
|
+
*
|
|
82
|
+
* 使用官方 models.providers 自定义 provider 格式:
|
|
83
|
+
* models.providers.<name>.baseUrl / apiKey / api / models[]
|
|
84
|
+
* agents.defaults.model.primary → "<provider>/<model-id>"
|
|
85
|
+
*
|
|
86
|
+
* 参考文档:https://docs.openclaw.ai/gateway/configuration-reference#custom-providers-and-base-urls
|
|
87
|
+
*/
|
|
88
|
+
export async function configureOpenClaw(apiKey) {
|
|
89
|
+
const paths = getOpenClawConfigPaths();
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await fs.ensureDir(paths.configDir);
|
|
93
|
+
|
|
94
|
+
const existingConfig = await fs.pathExists(paths.configFile)
|
|
95
|
+
? await readConfig(paths.configFile)
|
|
96
|
+
: {};
|
|
97
|
+
|
|
98
|
+
// OpenClaw 配置会做 schema 校验:根级未知字段可能导致校验失败
|
|
99
|
+
// 旧版本写入过 `_duojie`,这里显式剔除
|
|
100
|
+
const { _duojie: _ignoredDuojie, ...baseConfig } = existingConfig || {};
|
|
101
|
+
void _ignoredDuojie;
|
|
102
|
+
|
|
103
|
+
const models = getModels();
|
|
104
|
+
|
|
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
|
+
}));
|
|
126
|
+
|
|
127
|
+
const allModels = [...claudeModels, ...gptModels, ...geminiModels, ...otherModels];
|
|
128
|
+
|
|
129
|
+
// 构建 providers(按 api 协议分组,避免在同一个 provider 混用不同协议)
|
|
130
|
+
const providers = {};
|
|
131
|
+
const modelIdToProvider = new Map();
|
|
132
|
+
|
|
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);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 默认模型:优先 gpt-5.2(如存在)
|
|
151
|
+
const gpt52 = gptModels.find(m => m.id === 'gpt-5.2')?.id || null;
|
|
152
|
+
let defaultProvider = 'duojie-claude';
|
|
153
|
+
let defaultModelId = claudeModels[0]?.id || allModels[0]?.id || '';
|
|
154
|
+
|
|
155
|
+
if (gpt52) {
|
|
156
|
+
defaultProvider = modelIdToProvider.get(gpt52) || providerNameForApi('openai-completions');
|
|
157
|
+
defaultModelId = gpt52;
|
|
158
|
+
} else if (claudeModels.length > 0) {
|
|
159
|
+
defaultModelId = (
|
|
160
|
+
claudeModels.find(m => m.id.includes('opus-4-6')) ||
|
|
161
|
+
claudeModels.find(m => m.id.includes('opus')) ||
|
|
162
|
+
claudeModels.find(m => m.id.includes('sonnet')) ||
|
|
163
|
+
claudeModels[0]
|
|
164
|
+
).id;
|
|
165
|
+
defaultProvider = modelIdToProvider.get(defaultModelId) || 'duojie-claude';
|
|
166
|
+
} else if (gptModels.length > 0) {
|
|
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);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 构建模型 allowlist(用于 /model 切换)
|
|
181
|
+
const modelAllowList = {};
|
|
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
|
+
}
|
|
186
|
+
|
|
187
|
+
const totalModels = allModels.length;
|
|
188
|
+
|
|
189
|
+
// 合并配置,保留用户已有的非 Duojie 设置
|
|
190
|
+
const existingProviders = stripDuojieKeysFromObject(baseConfig.models?.providers);
|
|
191
|
+
const existingAllowList = stripDuojieKeysFromObject(baseConfig.agents?.defaults?.models);
|
|
192
|
+
|
|
193
|
+
const newConfig = {
|
|
194
|
+
...baseConfig,
|
|
195
|
+
models: {
|
|
196
|
+
...baseConfig.models,
|
|
197
|
+
mode: 'merge',
|
|
198
|
+
providers: {
|
|
199
|
+
...existingProviders,
|
|
200
|
+
...providers,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
agents: {
|
|
204
|
+
...baseConfig.agents,
|
|
205
|
+
defaults: {
|
|
206
|
+
...baseConfig.agents?.defaults,
|
|
207
|
+
model: {
|
|
208
|
+
...baseConfig.agents?.defaults?.model,
|
|
209
|
+
primary: `${defaultProvider}/${defaultModelId}`,
|
|
210
|
+
},
|
|
211
|
+
models: {
|
|
212
|
+
...existingAllowList,
|
|
213
|
+
...modelAllowList,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
await fs.writeJson(paths.configFile, newConfig, { spaces: 2 });
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
message: `已配置 ${totalModels} 个模型 → ${paths.configFile}`,
|
|
224
|
+
configPath: paths.configFile,
|
|
225
|
+
hint: '运行 openclaw gateway restart 使配置生效',
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
message: `配置失败: ${error.message}`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 检查 OpenClaw 配置状态
|
|
237
|
+
*/
|
|
238
|
+
configureOpenClaw.checkStatus = async function () {
|
|
239
|
+
const paths = getOpenClawConfigPaths();
|
|
240
|
+
|
|
241
|
+
if (await fs.pathExists(paths.configFile)) {
|
|
242
|
+
const config = await readConfig(paths.configFile);
|
|
243
|
+
const providerKeys = Object.keys(config.models?.providers || {});
|
|
244
|
+
if (providerKeys.some(k => `${k}`.startsWith(DUOJIE_PROVIDER_PREFIX))) {
|
|
245
|
+
return { configured: true, message: '已配置 Duojie API' };
|
|
246
|
+
}
|
|
247
|
+
if (providerKeys.length > 0) {
|
|
248
|
+
return { configured: true, message: '已配置(非 Duojie)' };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { configured: false };
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 撤销 OpenClaw 配置
|
|
257
|
+
*/
|
|
258
|
+
configureOpenClaw.revoke = async function () {
|
|
259
|
+
const paths = getOpenClawConfigPaths();
|
|
260
|
+
|
|
261
|
+
if (await fs.pathExists(paths.configFile)) {
|
|
262
|
+
const config = await readConfig(paths.configFile);
|
|
263
|
+
|
|
264
|
+
if (config.models?.providers) {
|
|
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;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await fs.writeJson(paths.configFile, config, { spaces: 2 });
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export default configureOpenClaw;
|