duojie-helper 0.2.6 → 0.2.7
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/package.json +1 -1
- package/src/tools/openclaw.js +92 -204
package/package.json
CHANGED
package/src/tools/openclaw.js
CHANGED
|
@@ -5,205 +5,112 @@ import { API_CONFIG, getModels } from '../index.js';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 获取 OpenClaw 配置路径
|
|
8
|
+
* 支持 macOS / Linux / Windows
|
|
8
9
|
*/
|
|
9
10
|
function getOpenClawConfigPaths() {
|
|
10
11
|
const home = os.homedir();
|
|
11
12
|
return {
|
|
12
13
|
configDir: path.join(home, '.openclaw'),
|
|
13
14
|
configFile: path.join(home, '.openclaw', 'openclaw.json'),
|
|
14
|
-
agentsDir: path.join(home, '.openclaw', 'agents'),
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* 读取并解析 openclaw.json(支持 JSON5 注释/尾逗号)
|
|
20
|
+
*/
|
|
21
|
+
async function readConfig(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
24
|
+
const clean = content
|
|
25
|
+
.replace(/\/\/.*$/gm, '')
|
|
26
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
27
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
28
|
+
return JSON.parse(clean);
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 从模型列表中选出最优默认模型 ID
|
|
36
|
+
* 优先级:opus-4-6-kiro > opus-4-6 > opus+kiro > opus > sonnet > 第一个
|
|
37
|
+
*/
|
|
38
|
+
function pickDefaultModel(models) {
|
|
39
|
+
const all = [
|
|
40
|
+
...(models.claude || []),
|
|
41
|
+
...(models.gpt || []),
|
|
42
|
+
...(models.gemini || []),
|
|
43
|
+
...(models.other || []),
|
|
44
|
+
];
|
|
45
|
+
if (all.length === 0) return 'claude-opus-4-6-kiro';
|
|
46
|
+
|
|
47
|
+
const claudeList = models.claude || [];
|
|
48
|
+
if (claudeList.length > 0) {
|
|
49
|
+
return (
|
|
50
|
+
claudeList.find(m => m.id.includes('opus-4-6') && m.id.includes('kiro'))?.id ||
|
|
51
|
+
claudeList.find(m => m.id.includes('opus-4-6'))?.id ||
|
|
52
|
+
claudeList.find(m => m.id.includes('opus') && m.id.includes('kiro'))?.id ||
|
|
53
|
+
claudeList.find(m => m.id.includes('opus'))?.id ||
|
|
54
|
+
claudeList.find(m => m.id.includes('sonnet'))?.id ||
|
|
55
|
+
claudeList[0].id
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return all[0].id;
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
/**
|
|
19
62
|
* 配置 OpenClaw
|
|
20
|
-
*
|
|
63
|
+
*
|
|
64
|
+
* 使用 OpenClaw 标准的 env + agents.defaults.model 格式:
|
|
65
|
+
* env.DUOJIE_API_KEY → API 密钥
|
|
66
|
+
* env.DUOJIE_BASE_URL → 自定义 base URL
|
|
67
|
+
* agents.defaults.model.primary → "duojie/<model-id>"
|
|
21
68
|
*/
|
|
22
69
|
export async function configureOpenClaw(apiKey) {
|
|
23
70
|
const paths = getOpenClawConfigPaths();
|
|
24
|
-
|
|
71
|
+
|
|
25
72
|
try {
|
|
26
|
-
// 确保目录存在
|
|
27
73
|
await fs.ensureDir(paths.configDir);
|
|
28
74
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const content = await fs.readFile(paths.configFile, 'utf-8');
|
|
34
|
-
// 简单的 JSON5 兼容处理(去除注释和尾逗号)
|
|
35
|
-
const cleanContent = content
|
|
36
|
-
.replace(/\/\/.*$/gm, '') // 移除单行注释
|
|
37
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除多行注释
|
|
38
|
-
.replace(/,(\s*[}\]])/g, '$1'); // 移除尾逗号
|
|
39
|
-
existingConfig = JSON.parse(cleanContent);
|
|
40
|
-
} catch {
|
|
41
|
-
existingConfig = {};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
75
|
+
const existingConfig = await fs.pathExists(paths.configFile)
|
|
76
|
+
? await readConfig(paths.configFile)
|
|
77
|
+
: {};
|
|
44
78
|
|
|
45
|
-
// 获取模型列表
|
|
46
79
|
const models = getModels();
|
|
80
|
+
const defaultModelId = pickDefaultModel(models);
|
|
81
|
+
const totalModels = Object.values(models).reduce((s, arr) => s + arr.length, 0);
|
|
47
82
|
|
|
48
|
-
//
|
|
49
|
-
const claudeModels = (models.claude || []).map(m => ({
|
|
50
|
-
id: m.id,
|
|
51
|
-
name: m.name,
|
|
52
|
-
reasoning: false,
|
|
53
|
-
input: ['text', 'image'],
|
|
54
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
55
|
-
contextWindow: 200000,
|
|
56
|
-
maxTokens: 64000,
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
// GPT 模型 (openai-responses 或 openai-completions)
|
|
60
|
-
const gptModels = (models.gpt || []).map(m => ({
|
|
61
|
-
id: m.id,
|
|
62
|
-
name: m.name,
|
|
63
|
-
reasoning: false,
|
|
64
|
-
input: ['text', 'image'],
|
|
65
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
66
|
-
contextWindow: 128000,
|
|
67
|
-
maxTokens: 16384,
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
// Gemini 模型
|
|
71
|
-
const geminiModels = (models.gemini || []).map(m => ({
|
|
72
|
-
id: m.id,
|
|
73
|
-
name: m.name,
|
|
74
|
-
reasoning: false,
|
|
75
|
-
input: ['text', 'image'],
|
|
76
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
77
|
-
contextWindow: 1000000,
|
|
78
|
-
maxTokens: 65536,
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
|
-
// 其他模型
|
|
82
|
-
const otherModels = (models.other || []).map(m => ({
|
|
83
|
-
id: m.id,
|
|
84
|
-
name: m.name,
|
|
85
|
-
reasoning: false,
|
|
86
|
-
input: ['text', 'image'],
|
|
87
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
88
|
-
contextWindow: 128000,
|
|
89
|
-
maxTokens: 16384,
|
|
90
|
-
}));
|
|
91
|
-
|
|
92
|
-
// 构建 providers
|
|
93
|
-
const providers = {};
|
|
94
|
-
|
|
95
|
-
if (claudeModels.length > 0) {
|
|
96
|
-
providers['duojie-claude'] = {
|
|
97
|
-
baseUrl: API_CONFIG.baseUrl,
|
|
98
|
-
apiKey: apiKey,
|
|
99
|
-
api: 'anthropic-messages',
|
|
100
|
-
models: claudeModels,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (gptModels.length > 0) {
|
|
105
|
-
// GPT 使用 openai-responses (如果支持) 或 openai-completions
|
|
106
|
-
const gptApi = models.gpt[0]?.api || 'openai-responses';
|
|
107
|
-
providers['duojie-gpt'] = {
|
|
108
|
-
baseUrl: API_CONFIG.baseUrl,
|
|
109
|
-
apiKey: apiKey,
|
|
110
|
-
api: gptApi,
|
|
111
|
-
models: gptModels,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (geminiModels.length > 0) {
|
|
116
|
-
providers['duojie-gemini'] = {
|
|
117
|
-
baseUrl: API_CONFIG.baseUrl,
|
|
118
|
-
apiKey: apiKey,
|
|
119
|
-
api: 'anthropic-messages',
|
|
120
|
-
models: geminiModels,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (otherModels.length > 0) {
|
|
125
|
-
providers['duojie-other'] = {
|
|
126
|
-
baseUrl: API_CONFIG.baseUrl,
|
|
127
|
-
apiKey: apiKey,
|
|
128
|
-
api: 'openai-completions',
|
|
129
|
-
models: otherModels,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 确定默认模型 - 优先使用 opus-4-6-kiro
|
|
134
|
-
let defaultModelProvider = 'duojie-claude';
|
|
135
|
-
let defaultModel = 'claude-opus-4-6-kiro';
|
|
136
|
-
|
|
137
|
-
if (claudeModels.length > 0) {
|
|
138
|
-
defaultModelProvider = 'duojie-claude';
|
|
139
|
-
// 优先级:opus-4-6-kiro > opus-4-6 > opus+kiro > opus > sonnet > 第一个
|
|
140
|
-
const opusKiro46 = claudeModels.find(m => m.id.includes('opus-4-6') && m.id.includes('kiro'));
|
|
141
|
-
const opus46 = claudeModels.find(m => m.id.includes('opus-4-6'));
|
|
142
|
-
const opusKiro = claudeModels.find(m => m.id.includes('opus') && m.id.includes('kiro'));
|
|
143
|
-
const opus = claudeModels.find(m => m.id.includes('opus'));
|
|
144
|
-
const sonnet = claudeModels.find(m => m.id.includes('sonnet'));
|
|
145
|
-
defaultModel = opusKiro46?.id || opus46?.id || opusKiro?.id || opus?.id || sonnet?.id || claudeModels[0].id;
|
|
146
|
-
} else if (gptModels.length > 0) {
|
|
147
|
-
defaultModelProvider = 'duojie-gpt';
|
|
148
|
-
defaultModel = gptModels[0].id;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// 构建模型允许列表
|
|
152
|
-
const modelAllowList = {};
|
|
153
|
-
for (const m of claudeModels) {
|
|
154
|
-
modelAllowList[`duojie-claude/${m.id}`] = { alias: m.name };
|
|
155
|
-
}
|
|
156
|
-
for (const m of gptModels) {
|
|
157
|
-
modelAllowList[`duojie-gpt/${m.id}`] = { alias: m.name };
|
|
158
|
-
}
|
|
159
|
-
for (const m of geminiModels) {
|
|
160
|
-
modelAllowList[`duojie-gemini/${m.id}`] = { alias: m.name };
|
|
161
|
-
}
|
|
162
|
-
for (const m of otherModels) {
|
|
163
|
-
modelAllowList[`duojie-other/${m.id}`] = { alias: m.name };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 统计配置的模型数量
|
|
167
|
-
const totalModels = claudeModels.length + gptModels.length + geminiModels.length + otherModels.length;
|
|
168
|
-
|
|
169
|
-
// 合并配置(不覆盖用户其他配置)
|
|
83
|
+
// 合并配置,保留用户已有的非 Duojie 设置
|
|
170
84
|
const newConfig = {
|
|
171
85
|
...existingConfig,
|
|
172
|
-
|
|
173
|
-
...existingConfig.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
...existingConfig.models?.providers,
|
|
177
|
-
...providers,
|
|
178
|
-
},
|
|
86
|
+
env: {
|
|
87
|
+
...existingConfig.env,
|
|
88
|
+
DUOJIE_API_KEY: apiKey,
|
|
89
|
+
DUOJIE_BASE_URL: API_CONFIG.baseUrl,
|
|
179
90
|
},
|
|
180
91
|
agents: {
|
|
181
92
|
...existingConfig.agents,
|
|
182
93
|
defaults: {
|
|
183
94
|
...existingConfig.agents?.defaults,
|
|
184
95
|
model: {
|
|
185
|
-
primary: `${defaultModelProvider}/${defaultModel}`,
|
|
186
96
|
...existingConfig.agents?.defaults?.model,
|
|
187
|
-
|
|
188
|
-
models: {
|
|
189
|
-
...existingConfig.agents?.defaults?.models,
|
|
190
|
-
...modelAllowList,
|
|
97
|
+
primary: `duojie/${defaultModelId}`,
|
|
191
98
|
},
|
|
192
99
|
},
|
|
193
100
|
},
|
|
194
|
-
// 标记配置来源
|
|
195
101
|
_duojie: {
|
|
196
102
|
configuredAt: new Date().toISOString(),
|
|
197
|
-
version: '0.2.
|
|
103
|
+
version: '0.2.7',
|
|
104
|
+
defaultModel: defaultModelId,
|
|
105
|
+
totalModels,
|
|
198
106
|
},
|
|
199
107
|
};
|
|
200
108
|
|
|
201
|
-
// 写入配置
|
|
202
109
|
await fs.writeJson(paths.configFile, newConfig, { spaces: 2 });
|
|
203
110
|
|
|
204
111
|
return {
|
|
205
112
|
success: true,
|
|
206
|
-
message: `已配置 ${totalModels}
|
|
113
|
+
message: `已配置 ${totalModels} 个模型,默认: duojie/${defaultModelId}`,
|
|
207
114
|
configPath: paths.configFile,
|
|
208
115
|
hint: '运行 openclaw gateway restart 使配置生效',
|
|
209
116
|
};
|
|
@@ -218,65 +125,46 @@ export async function configureOpenClaw(apiKey) {
|
|
|
218
125
|
/**
|
|
219
126
|
* 检查 OpenClaw 配置状态
|
|
220
127
|
*/
|
|
221
|
-
configureOpenClaw.checkStatus = async function() {
|
|
128
|
+
configureOpenClaw.checkStatus = async function () {
|
|
222
129
|
const paths = getOpenClawConfigPaths();
|
|
223
|
-
|
|
130
|
+
|
|
224
131
|
if (await fs.pathExists(paths.configFile)) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const config = JSON.parse(cleanContent);
|
|
232
|
-
|
|
233
|
-
if (config._duojie || config.models?.providers?.['duojie-claude']) {
|
|
234
|
-
return {
|
|
235
|
-
configured: true,
|
|
236
|
-
message: '已配置 Duojie API',
|
|
237
|
-
};
|
|
238
|
-
} else if (config.models?.providers) {
|
|
239
|
-
return {
|
|
240
|
-
configured: true,
|
|
241
|
-
message: '已配置(非 Duojie)',
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
} catch {
|
|
245
|
-
// ignore
|
|
132
|
+
const config = await readConfig(paths.configFile);
|
|
133
|
+
if (config._duojie || config.env?.DUOJIE_API_KEY) {
|
|
134
|
+
return { configured: true, message: '已配置 Duojie API' };
|
|
135
|
+
}
|
|
136
|
+
if (config.env || config.agents) {
|
|
137
|
+
return { configured: true, message: '已配置(非 Duojie)' };
|
|
246
138
|
}
|
|
247
139
|
}
|
|
248
|
-
|
|
140
|
+
|
|
249
141
|
return { configured: false };
|
|
250
142
|
};
|
|
251
143
|
|
|
252
144
|
/**
|
|
253
145
|
* 撤销 OpenClaw 配置
|
|
254
146
|
*/
|
|
255
|
-
configureOpenClaw.revoke = async function() {
|
|
147
|
+
configureOpenClaw.revoke = async function () {
|
|
256
148
|
const paths = getOpenClawConfigPaths();
|
|
257
|
-
|
|
149
|
+
|
|
258
150
|
if (await fs.pathExists(paths.configFile)) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
delete config.models.providers['duojie-gemini'];
|
|
272
|
-
delete config.models.providers['duojie-other'];
|
|
273
|
-
}
|
|
274
|
-
delete config._duojie;
|
|
275
|
-
|
|
276
|
-
await fs.writeJson(paths.configFile, config, { spaces: 2 });
|
|
277
|
-
} catch {
|
|
278
|
-
// ignore
|
|
151
|
+
const config = await readConfig(paths.configFile);
|
|
152
|
+
|
|
153
|
+
// 移除 Duojie 注入的 env 变量
|
|
154
|
+
if (config.env) {
|
|
155
|
+
delete config.env.DUOJIE_API_KEY;
|
|
156
|
+
delete config.env.DUOJIE_BASE_URL;
|
|
157
|
+
if (Object.keys(config.env).length === 0) delete config.env;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 若 primary 是 duojie/ 开头则清除
|
|
161
|
+
if (config.agents?.defaults?.model?.primary?.startsWith('duojie/')) {
|
|
162
|
+
delete config.agents.defaults.model.primary;
|
|
279
163
|
}
|
|
164
|
+
|
|
165
|
+
delete config._duojie;
|
|
166
|
+
|
|
167
|
+
await fs.writeJson(paths.configFile, config, { spaces: 2 });
|
|
280
168
|
}
|
|
281
169
|
};
|
|
282
170
|
|