@kikkimo/claude-launcher 3.0.0 → 3.1.0
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/CHANGELOG.md +28 -0
- package/README.md +7 -4
- package/claude-launcher +634 -38
- package/docs/README-zh.md +7 -4
- package/lib/api-manager.js +501 -67
- package/lib/i18n/locales/de.js +144 -6
- package/lib/i18n/locales/en.js +150 -6
- package/lib/i18n/locales/es.js +144 -6
- package/lib/i18n/locales/fr.js +144 -6
- package/lib/i18n/locales/it.js +144 -6
- package/lib/i18n/locales/ja.js +144 -6
- package/lib/i18n/locales/ko.js +144 -6
- package/lib/i18n/locales/pt.js +144 -6
- package/lib/i18n/locales/ru.js +144 -6
- package/lib/i18n/locales/zh-TW.js +144 -6
- package/lib/i18n/locales/zh.js +150 -6
- package/lib/launcher.js +46 -17
- package/lib/presets/providers.js +143 -39
- package/lib/ui/api-editor.js +668 -210
- package/lib/ui/i18n-labels.js +16 -0
- package/lib/ui/menu.js +19 -13
- package/lib/ui/screen.js +125 -125
- package/lib/utils/version-checker.js +6 -5
- package/lib/validators.js +102 -1
- package/package.json +2 -2
package/lib/api-manager.js
CHANGED
|
@@ -9,31 +9,63 @@ const { encrypt, decrypt } = require('./crypto');
|
|
|
9
9
|
const { validateBaseUrl, validateAuthToken, validateModel, validateApiName } = require('./validators');
|
|
10
10
|
const screen = require('./ui/screen');
|
|
11
11
|
|
|
12
|
+
class DuplicateApiError extends Error {
|
|
13
|
+
constructor(existingApi) {
|
|
14
|
+
super(`Duplicate API: ${existingApi.name}`);
|
|
15
|
+
this.name = 'DuplicateApiError';
|
|
16
|
+
this.code = 'DUPLICATE_API';
|
|
17
|
+
this.existingApiId = existingApi.id;
|
|
18
|
+
this.existingApiName = existingApi.name;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MODEL_CONFIG_LABELS = {
|
|
23
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: 'Regular Model (Sonnet)',
|
|
24
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: 'Heavy Model (Opus)',
|
|
25
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'Fast Model (Haiku)',
|
|
26
|
+
CLAUDE_CODE_SUBAGENT_MODEL: 'Subagent Model',
|
|
27
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION: 'Custom Model',
|
|
28
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'Custom Model Name',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const RUNTIME_CONFIG_LABELS = {
|
|
32
|
+
API_TIMEOUT_MS: 'Request Timeout',
|
|
33
|
+
CLAUDE_CODE_ATTRIBUTION_HEADER: 'Output Attribution',
|
|
34
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 'Reduce Non-Essential Traffic',
|
|
35
|
+
CLAUDE_CODE_EFFORT_LEVEL: 'Reasoning Effort',
|
|
36
|
+
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: 'Experimental Features',
|
|
37
|
+
CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: 'Disable Non-Streaming Fallback',
|
|
38
|
+
};
|
|
39
|
+
|
|
12
40
|
class ApiManager {
|
|
13
41
|
constructor() {
|
|
14
42
|
this.configFile = path.join(os.homedir(), '.claude-launcher-apis.json');
|
|
15
|
-
|
|
43
|
+
const { config, migrated } = this.loadConfig();
|
|
44
|
+
this.config = config;
|
|
45
|
+
if (migrated) {
|
|
46
|
+
this.saveConfig();
|
|
47
|
+
}
|
|
16
48
|
}
|
|
17
49
|
|
|
18
50
|
/**
|
|
19
51
|
* Load configuration from encrypted file
|
|
20
52
|
*/
|
|
21
53
|
loadConfig() {
|
|
54
|
+
let migrated = false;
|
|
22
55
|
try {
|
|
23
56
|
if (fs.existsSync(this.configFile)) {
|
|
24
57
|
const encryptedData = fs.readFileSync(this.configFile, 'utf8');
|
|
25
58
|
const decrypted = decrypt(encryptedData);
|
|
26
59
|
if (decrypted.success) {
|
|
27
60
|
const config = JSON.parse(decrypted.value);
|
|
28
|
-
|
|
29
|
-
if (!config.hasOwnProperty('
|
|
30
|
-
config.exportPassword = null;
|
|
31
|
-
}
|
|
32
|
-
if (!config.hasOwnProperty('passwordSkipped')) {
|
|
33
|
-
config.passwordSkipped = false;
|
|
34
|
-
}
|
|
61
|
+
if (!config.hasOwnProperty('exportPassword')) config.exportPassword = null;
|
|
62
|
+
if (!config.hasOwnProperty('passwordSkipped')) config.passwordSkipped = false;
|
|
35
63
|
|
|
36
|
-
|
|
64
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
65
|
+
for (const api of config.apis || []) {
|
|
66
|
+
migrated = this._migrateApiEntry(api, PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS) || migrated;
|
|
67
|
+
}
|
|
68
|
+
return { config, migrated };
|
|
37
69
|
}
|
|
38
70
|
}
|
|
39
71
|
} catch (error) {
|
|
@@ -41,15 +73,143 @@ class ApiManager {
|
|
|
41
73
|
}
|
|
42
74
|
|
|
43
75
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
config: {
|
|
77
|
+
apis: [],
|
|
78
|
+
activeIndex: -1,
|
|
79
|
+
version: '2.0.0',
|
|
80
|
+
createdAt: new Date().toISOString(),
|
|
81
|
+
exportPassword: null,
|
|
82
|
+
passwordSkipped: false,
|
|
83
|
+
},
|
|
84
|
+
migrated: false,
|
|
50
85
|
};
|
|
51
86
|
}
|
|
52
87
|
|
|
88
|
+
_migrateApiEntry(api, MODEL_KEYS, RUNTIME_KEYS) {
|
|
89
|
+
const before = JSON.stringify({
|
|
90
|
+
modelEnvVars: api.modelEnvVars,
|
|
91
|
+
_autoModelEnvVars: api._autoModelEnvVars,
|
|
92
|
+
runtimeEnvVars: api.runtimeEnvVars,
|
|
93
|
+
_runtimeEnvSources: api._runtimeEnvSources,
|
|
94
|
+
customEnvVars: api.customEnvVars,
|
|
95
|
+
smallFastModel: api.smallFastModel,
|
|
96
|
+
_autoFilledModel: api._autoFilledModel,
|
|
97
|
+
});
|
|
98
|
+
this._normalizeApiFields(api);
|
|
99
|
+
const after = JSON.stringify({
|
|
100
|
+
modelEnvVars: api.modelEnvVars,
|
|
101
|
+
_autoModelEnvVars: api._autoModelEnvVars,
|
|
102
|
+
runtimeEnvVars: api.runtimeEnvVars,
|
|
103
|
+
_runtimeEnvSources: api._runtimeEnvSources,
|
|
104
|
+
customEnvVars: api.customEnvVars,
|
|
105
|
+
smallFastModel: api.smallFastModel,
|
|
106
|
+
_autoFilledModel: api._autoFilledModel,
|
|
107
|
+
});
|
|
108
|
+
return before !== after;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_normalizeApiFields(api) {
|
|
112
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
113
|
+
const { getProvider } = require('./presets/providers');
|
|
114
|
+
|
|
115
|
+
const effectiveModel = api._autoFilledModel || api.model;
|
|
116
|
+
const hadAutoModelEnvVars = !!api._autoModelEnvVars;
|
|
117
|
+
|
|
118
|
+
const providerConfig = getProvider(api.provider);
|
|
119
|
+
let template;
|
|
120
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
121
|
+
template = providerConfig.modelEnvTemplate.getValues(effectiveModel);
|
|
122
|
+
} else {
|
|
123
|
+
template = {};
|
|
124
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) template[k] = effectiveModel;
|
|
125
|
+
template.smallFastModel = effectiveModel;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let smallFastWasFixed = false;
|
|
129
|
+
|
|
130
|
+
// _autoModelEnvVars — full rebuild via template
|
|
131
|
+
if (!api._autoModelEnvVars) {
|
|
132
|
+
api._autoModelEnvVars = { ...template };
|
|
133
|
+
} else {
|
|
134
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
135
|
+
if (!(k in api._autoModelEnvVars) || typeof api._autoModelEnvVars[k] !== 'string') {
|
|
136
|
+
api._autoModelEnvVars[k] = template[k] || '';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const k of Object.keys(api._autoModelEnvVars)) {
|
|
140
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(k) && k !== 'smallFastModel') {
|
|
141
|
+
delete api._autoModelEnvVars[k];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!('smallFastModel' in api._autoModelEnvVars) || typeof api._autoModelEnvVars.smallFastModel !== 'string') {
|
|
145
|
+
api._autoModelEnvVars.smallFastModel = template.smallFastModel;
|
|
146
|
+
smallFastWasFixed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// modelEnvVars — fill with template actual values (NOT "")
|
|
151
|
+
if (!api.modelEnvVars) {
|
|
152
|
+
api.modelEnvVars = {};
|
|
153
|
+
}
|
|
154
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
155
|
+
if (!(k in api.modelEnvVars) || typeof api.modelEnvVars[k] !== 'string') {
|
|
156
|
+
api.modelEnvVars[k] = template[k] || '';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const k of Object.keys(api.modelEnvVars)) {
|
|
160
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(k)) {
|
|
161
|
+
delete api.modelEnvVars[k];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// smallFastModel — sync with template
|
|
166
|
+
if (!api.smallFastModel || typeof api.smallFastModel !== 'string'
|
|
167
|
+
|| !hadAutoModelEnvVars || smallFastWasFixed) {
|
|
168
|
+
api.smallFastModel = template.smallFastModel;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// runtimeEnvVars — fill "" not provider values
|
|
172
|
+
if (!api.runtimeEnvVars) {
|
|
173
|
+
api.runtimeEnvVars = {};
|
|
174
|
+
}
|
|
175
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
176
|
+
if (!(k in api.runtimeEnvVars) || typeof api.runtimeEnvVars[k] !== 'string') {
|
|
177
|
+
api.runtimeEnvVars[k] = '';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// _runtimeEnvSources — missing → "auto"
|
|
182
|
+
if (!api._runtimeEnvSources) {
|
|
183
|
+
api._runtimeEnvSources = {};
|
|
184
|
+
}
|
|
185
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
186
|
+
if (!(k in api._runtimeEnvSources)) {
|
|
187
|
+
api._runtimeEnvSources[k] = 'auto';
|
|
188
|
+
}
|
|
189
|
+
if (api._runtimeEnvSources[k] !== 'auto' && api._runtimeEnvSources[k] !== 'manual') {
|
|
190
|
+
api._runtimeEnvSources[k] = 'auto';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// runtime/source conflict resolution
|
|
195
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
196
|
+
if (api.runtimeEnvVars[k] !== '' && api._runtimeEnvSources[k] === 'auto') {
|
|
197
|
+
api.runtimeEnvVars[k] = '';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// customEnvVars
|
|
202
|
+
if (!api.customEnvVars) {
|
|
203
|
+
api.customEnvVars = {};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (api._autoFilledModel) {
|
|
207
|
+
delete api._autoFilledModel;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return api;
|
|
211
|
+
}
|
|
212
|
+
|
|
53
213
|
/**
|
|
54
214
|
* Save configuration to encrypted file
|
|
55
215
|
*/
|
|
@@ -120,7 +280,7 @@ class ApiManager {
|
|
|
120
280
|
// Check for duplicates
|
|
121
281
|
const duplicate = this.checkDuplicate(baseUrl, authToken, model);
|
|
122
282
|
if (duplicate.isDuplicate) {
|
|
123
|
-
throw new
|
|
283
|
+
throw new DuplicateApiError(duplicate.existing);
|
|
124
284
|
}
|
|
125
285
|
|
|
126
286
|
// Encrypt the auth token before storing
|
|
@@ -129,6 +289,32 @@ class ApiManager {
|
|
|
129
289
|
throw new Error(`Failed to encrypt auth token: ${encryptedToken.error}`);
|
|
130
290
|
}
|
|
131
291
|
|
|
292
|
+
// Compute model env template values
|
|
293
|
+
const { getProvider } = require('./presets/providers');
|
|
294
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
295
|
+
const providerConfig = getProvider(provider);
|
|
296
|
+
let templateValues;
|
|
297
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
298
|
+
templateValues = providerConfig.modelEnvTemplate.getValues(modelValidation.value);
|
|
299
|
+
} else {
|
|
300
|
+
templateValues = {};
|
|
301
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = modelValidation.value;
|
|
302
|
+
templateValues.smallFastModel = modelValidation.value;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const modelEnvVars = {};
|
|
306
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
307
|
+
modelEnvVars[k] = templateValues[k] || '';
|
|
308
|
+
}
|
|
309
|
+
const _autoModelEnvVars = { ...templateValues };
|
|
310
|
+
|
|
311
|
+
const runtimeEnvVars = {};
|
|
312
|
+
const _runtimeEnvSources = {};
|
|
313
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
314
|
+
runtimeEnvVars[k] = '';
|
|
315
|
+
_runtimeEnvSources[k] = 'auto';
|
|
316
|
+
}
|
|
317
|
+
|
|
132
318
|
const newApi = {
|
|
133
319
|
id: Date.now().toString(),
|
|
134
320
|
name: name || `API-${this.config.apis.length + 1}`,
|
|
@@ -136,15 +322,18 @@ class ApiManager {
|
|
|
136
322
|
baseUrl: urlValidation.value,
|
|
137
323
|
authToken: encryptedToken.value,
|
|
138
324
|
model: modelValidation.value,
|
|
139
|
-
smallFastModel:
|
|
325
|
+
smallFastModel: templateValues.smallFastModel,
|
|
140
326
|
createdAt: new Date().toISOString(),
|
|
141
|
-
// Existing statistics fields
|
|
142
327
|
lastUsed: null,
|
|
143
328
|
usageCount: 0,
|
|
144
|
-
// New statistics fields
|
|
145
329
|
successCount: 0,
|
|
146
330
|
failCount: 0,
|
|
147
|
-
lastError: null
|
|
331
|
+
lastError: null,
|
|
332
|
+
modelEnvVars,
|
|
333
|
+
_autoModelEnvVars,
|
|
334
|
+
runtimeEnvVars,
|
|
335
|
+
_runtimeEnvSources,
|
|
336
|
+
customEnvVars: {},
|
|
148
337
|
};
|
|
149
338
|
|
|
150
339
|
this.config.apis.push(newApi);
|
|
@@ -287,7 +476,7 @@ class ApiManager {
|
|
|
287
476
|
if (!validIds.includes(value)) {
|
|
288
477
|
throw new Error(`Unknown provider: ${value}. Valid: ${validIds.join(', ')}`);
|
|
289
478
|
}
|
|
290
|
-
|
|
479
|
+
return this.updateApiProvider(apiId, value).api;
|
|
291
480
|
}
|
|
292
481
|
case 'baseUrl': {
|
|
293
482
|
const urlValidation = validateBaseUrl(value);
|
|
@@ -330,13 +519,143 @@ class ApiManager {
|
|
|
330
519
|
// Apply update
|
|
331
520
|
api[field] = value.trim();
|
|
332
521
|
if (field === 'model') {
|
|
333
|
-
|
|
522
|
+
const { getProvider } = require('./presets/providers');
|
|
523
|
+
const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
|
|
524
|
+
const providerConfig = getProvider(api.provider);
|
|
525
|
+
let templateVals;
|
|
526
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
527
|
+
templateVals = providerConfig.modelEnvTemplate.getValues(value.trim());
|
|
528
|
+
} else {
|
|
529
|
+
templateVals = {};
|
|
530
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateVals[k] = value.trim();
|
|
531
|
+
templateVals.smallFastModel = value.trim();
|
|
532
|
+
}
|
|
533
|
+
if (api._autoModelEnvVars) {
|
|
534
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
535
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
536
|
+
if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) {
|
|
537
|
+
api.modelEnvVars[k] = templateVals[k] || '';
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) {
|
|
541
|
+
api.smallFastModel = templateVals.smallFastModel;
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
// No snapshot means all fields are auto — overwrite all
|
|
545
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
546
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
547
|
+
api.modelEnvVars[k] = templateVals[k] || '';
|
|
548
|
+
}
|
|
549
|
+
api.smallFastModel = templateVals.smallFastModel;
|
|
550
|
+
}
|
|
551
|
+
api._autoModelEnvVars = { ...templateVals };
|
|
334
552
|
}
|
|
335
553
|
|
|
336
554
|
this.saveConfig();
|
|
337
555
|
return api;
|
|
338
556
|
}
|
|
339
557
|
|
|
558
|
+
updateModelEnvVar(apiId, key, value) {
|
|
559
|
+
const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
|
|
560
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined model env key`);
|
|
561
|
+
if (typeof value !== 'string') throw new Error('model env value must be a string');
|
|
562
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
563
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
564
|
+
const api = this.config.apis[index];
|
|
565
|
+
const autoValue = api._autoModelEnvVars && typeof api._autoModelEnvVars[key] === 'string'
|
|
566
|
+
? api._autoModelEnvVars[key]
|
|
567
|
+
: '';
|
|
568
|
+
api.modelEnvVars[key] = value === '' ? autoValue : value;
|
|
569
|
+
this.saveConfig();
|
|
570
|
+
return api;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
updateRuntimeEnvVar(apiId, key, value, options = {}) {
|
|
574
|
+
const { PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue } = require('./validators');
|
|
575
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined runtime env key`);
|
|
576
|
+
const validation = validateRuntimeEnvValue(key, value);
|
|
577
|
+
if (!validation.valid) throw new Error(`Invalid value for ${key}: ${validation.error}`);
|
|
578
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
579
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
580
|
+
if (options.source === 'auto') {
|
|
581
|
+
this.config.apis[index].runtimeEnvVars[key] = '';
|
|
582
|
+
this.config.apis[index]._runtimeEnvSources[key] = 'auto';
|
|
583
|
+
} else if (options.source === 'manual') {
|
|
584
|
+
this.config.apis[index].runtimeEnvVars[key] = value;
|
|
585
|
+
this.config.apis[index]._runtimeEnvSources[key] = 'manual';
|
|
586
|
+
} else {
|
|
587
|
+
this.config.apis[index].runtimeEnvVars[key] = value;
|
|
588
|
+
this.config.apis[index]._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
|
|
589
|
+
}
|
|
590
|
+
this.saveConfig();
|
|
591
|
+
return this.config.apis[index];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
setCustomEnvVar(apiId, key, value) {
|
|
595
|
+
const { validateEnvKey } = require('./validators');
|
|
596
|
+
const kv = validateEnvKey(key);
|
|
597
|
+
if (!kv.valid) throw new Error(`Custom env key "${key}" is reserved or invalid`);
|
|
598
|
+
if (typeof value !== 'string') throw new Error('Custom env value must be a string');
|
|
599
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
600
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
601
|
+
this.config.apis[index].customEnvVars[key] = value;
|
|
602
|
+
this.saveConfig();
|
|
603
|
+
return this.config.apis[index];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
deleteCustomEnvVar(apiId, key) {
|
|
607
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
608
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
609
|
+
delete this.config.apis[index].customEnvVars[key];
|
|
610
|
+
this.saveConfig();
|
|
611
|
+
return this.config.apis[index];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
updateApiProvider(apiId, newProviderId) {
|
|
615
|
+
const { getProvider, getAllProviders, detectProvider } = require('./presets/providers');
|
|
616
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
617
|
+
const validIds = getAllProviders().map(p => p.id);
|
|
618
|
+
if (!validIds.includes(newProviderId)) throw new Error(`Unknown provider: ${newProviderId}`);
|
|
619
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
620
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
621
|
+
const api = this.config.apis[index];
|
|
622
|
+
const newProvider = getProvider(newProviderId);
|
|
623
|
+
const warnings = [];
|
|
624
|
+
if (!newProvider.models.includes(api.model)) {
|
|
625
|
+
warnings.push({ code: 'MODEL_NOT_IN_PROVIDER', messageArgs: { model: api.model, providerName: newProvider.name } });
|
|
626
|
+
}
|
|
627
|
+
if (detectProvider(api.baseUrl) !== newProviderId) {
|
|
628
|
+
warnings.push({ code: 'BASE_URL_NOT_UPDATED', messageArgs: { baseUrl: api.baseUrl } });
|
|
629
|
+
}
|
|
630
|
+
if (api._runtimeEnvSources) {
|
|
631
|
+
for (const [key, source] of Object.entries(api._runtimeEnvSources)) {
|
|
632
|
+
if (source === 'auto' && PREDEFINED_RUNTIME_KEYS.includes(key)) api.runtimeEnvVars[key] = '';
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
let templateValues;
|
|
636
|
+
if (newProvider.modelEnvTemplate) {
|
|
637
|
+
templateValues = newProvider.modelEnvTemplate.getValues(api.model);
|
|
638
|
+
} else {
|
|
639
|
+
templateValues = {};
|
|
640
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = api.model;
|
|
641
|
+
templateValues.smallFastModel = api.model;
|
|
642
|
+
}
|
|
643
|
+
if (api._autoModelEnvVars) {
|
|
644
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
645
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
646
|
+
if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) api.modelEnvVars[k] = templateValues[k] || '';
|
|
647
|
+
}
|
|
648
|
+
if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) api.smallFastModel = templateValues.smallFastModel;
|
|
649
|
+
}
|
|
650
|
+
api._autoModelEnvVars = { ...templateValues };
|
|
651
|
+
if (detectProvider(api.baseUrl) !== newProviderId && warnings.some(w => w.code === 'MODEL_NOT_IN_PROVIDER')) {
|
|
652
|
+
warnings.push({ code: 'MIXED_PROVIDER_CONFIG', messageArgs: { providerId: newProviderId, baseUrl: api.baseUrl, model: api.model } });
|
|
653
|
+
}
|
|
654
|
+
api.provider = newProviderId;
|
|
655
|
+
this.saveConfig();
|
|
656
|
+
return { api, warnings };
|
|
657
|
+
}
|
|
658
|
+
|
|
340
659
|
/**
|
|
341
660
|
* Record a successful API launch
|
|
342
661
|
* @returns {Object|null} The updated API object or null
|
|
@@ -575,14 +894,16 @@ class ApiManager {
|
|
|
575
894
|
* Export configuration as plaintext JSON (already authenticated)
|
|
576
895
|
*/
|
|
577
896
|
exportConfigAuthenticated() {
|
|
578
|
-
// Create export data with plaintext API keys
|
|
579
897
|
const exportData = {
|
|
898
|
+
configVersion: 2,
|
|
580
899
|
version: this.config.version,
|
|
900
|
+
warning: 'This file contains plaintext API keys and custom environment variables. Handle with care.',
|
|
581
901
|
exportedAt: new Date().toISOString(),
|
|
582
902
|
apis: this.config.apis.map(api => {
|
|
583
903
|
const decrypted = decrypt(api.authToken);
|
|
904
|
+
const { _autoFilledModel, ...safe } = api;
|
|
584
905
|
return {
|
|
585
|
-
...
|
|
906
|
+
...safe,
|
|
586
907
|
authToken: decrypted.success ? decrypted.value : '***DECRYPTION_FAILED***'
|
|
587
908
|
};
|
|
588
909
|
}),
|
|
@@ -619,90 +940,203 @@ class ApiManager {
|
|
|
619
940
|
processImportData(configData) {
|
|
620
941
|
let imported = 0;
|
|
621
942
|
let skipped = 0;
|
|
943
|
+
const warnings = [];
|
|
944
|
+
const skippedItems = [];
|
|
622
945
|
|
|
623
946
|
if (!configData.apis || !Array.isArray(configData.apis)) {
|
|
624
947
|
throw new Error('Invalid configuration format - no APIs found');
|
|
625
948
|
}
|
|
626
949
|
|
|
950
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, RESERVED_ENV_KEYS } = require('./validators');
|
|
951
|
+
|
|
627
952
|
configData.apis.forEach(importApi => {
|
|
628
953
|
if (this.config.apis.length >= 99) {
|
|
629
|
-
screen.debug('Import skipped: maximum 99 APIs reached');
|
|
630
954
|
skipped++;
|
|
955
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Maximum 99 APIs reached' });
|
|
631
956
|
return;
|
|
632
957
|
}
|
|
633
958
|
|
|
634
|
-
// Validate the API configuration before importing
|
|
635
959
|
try {
|
|
636
|
-
// Validate Base URL
|
|
637
960
|
const urlValidation = validateBaseUrl(importApi.baseUrl);
|
|
638
961
|
if (!urlValidation.valid) {
|
|
639
|
-
|
|
640
|
-
skipped++;
|
|
641
|
-
return;
|
|
962
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: urlValidation.error }); return;
|
|
642
963
|
}
|
|
643
964
|
|
|
644
|
-
// Validate Auth Token (skip validation for placeholder tokens)
|
|
645
965
|
if (importApi.authToken !== '***REQUIRES_MANUAL_INPUT***') {
|
|
646
966
|
const tokenValidation = validateAuthToken(importApi.authToken);
|
|
647
967
|
if (!tokenValidation.valid) {
|
|
648
|
-
|
|
649
|
-
skipped++;
|
|
650
|
-
return;
|
|
968
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: tokenValidation.error }); return;
|
|
651
969
|
}
|
|
652
970
|
}
|
|
653
971
|
|
|
654
|
-
// Validate Model
|
|
655
972
|
const modelValidation = validateModel(importApi.model);
|
|
656
973
|
if (!modelValidation.valid) {
|
|
657
|
-
|
|
658
|
-
skipped++;
|
|
659
|
-
return;
|
|
974
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: modelValidation.error }); return;
|
|
660
975
|
}
|
|
661
976
|
|
|
662
|
-
// Check for duplicates using the same logic as addApi
|
|
663
977
|
const importToken = importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
|
|
664
978
|
const duplicate = this.checkDuplicate(importApi.baseUrl, importToken, importApi.model);
|
|
665
979
|
|
|
666
980
|
if (duplicate.isDuplicate) {
|
|
667
981
|
skipped++;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
982
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Duplicate configuration' });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Clean modelEnvVars: whitelist only
|
|
987
|
+
const cleanedModelEnvVars = {};
|
|
988
|
+
if (importApi.modelEnvVars) {
|
|
989
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
990
|
+
const v = importApi.modelEnvVars[k];
|
|
991
|
+
cleanedModelEnvVars[k] = (typeof v === 'string') ? v : '';
|
|
992
|
+
}
|
|
993
|
+
for (const k of Object.keys(importApi.modelEnvVars)) {
|
|
994
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(k)) {
|
|
995
|
+
warnings.push({ code: 'UNKNOWN_MODEL_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Clean runtimeEnvVars: whitelist + validate
|
|
1001
|
+
const cleanedRuntimeEnvVars = {};
|
|
1002
|
+
const cleanedRuntimeEnvSources = {};
|
|
1003
|
+
if (importApi.runtimeEnvVars) {
|
|
1004
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
1005
|
+
let v = importApi.runtimeEnvVars[k];
|
|
1006
|
+
if (typeof v !== 'string') v = '';
|
|
1007
|
+
if (v !== '' && !validateRuntimeEnvValue(k, v).valid) {
|
|
1008
|
+
warnings.push({ code: 'INVALID_RUNTIME_ENV_VALUE', apiName: importApi.name || 'Unknown', key: k });
|
|
1009
|
+
v = '';
|
|
1010
|
+
}
|
|
1011
|
+
cleanedRuntimeEnvVars[k] = v;
|
|
1012
|
+
const src = (importApi._runtimeEnvSources || {})[k];
|
|
1013
|
+
cleanedRuntimeEnvSources[k] = (src === 'manual' && v !== '') ? 'manual' : 'auto';
|
|
675
1014
|
}
|
|
1015
|
+
for (const k of Object.keys(importApi.runtimeEnvVars)) {
|
|
1016
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(k)) {
|
|
1017
|
+
warnings.push({ code: 'UNKNOWN_RUNTIME_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
676
1021
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
this.config.apis.push(newApi);
|
|
690
|
-
imported++;
|
|
691
|
-
|
|
692
|
-
// Set as active if this is the first API
|
|
693
|
-
if (this.config.apis.length === 1) {
|
|
694
|
-
this.config.activeIndex = 0;
|
|
1022
|
+
// Clean customEnvVars: skip reserved/predefined
|
|
1023
|
+
const cleanedCustomEnvVars = {};
|
|
1024
|
+
if (importApi.customEnvVars) {
|
|
1025
|
+
const allP = new Set([...RESERVED_ENV_KEYS, ...PREDEFINED_RUNTIME_KEYS, ...PREDEFINED_MODEL_ENV_KEYS]);
|
|
1026
|
+
for (const [k, v] of Object.entries(importApi.customEnvVars)) {
|
|
1027
|
+
if (allP.has(k)) {
|
|
1028
|
+
warnings.push({ code: 'CUSTOM_ENV_KEY_RESERVED', apiName: importApi.name || 'Unknown', key: k });
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (typeof v === 'string') cleanedCustomEnvVars[k] = v;
|
|
695
1032
|
}
|
|
696
1033
|
}
|
|
1034
|
+
|
|
1035
|
+
const tokenToEncrypt =
|
|
1036
|
+
importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
|
|
1037
|
+
const encryptedResult = encrypt(tokenToEncrypt);
|
|
1038
|
+
if (!encryptedResult.success) {
|
|
1039
|
+
skipped++;
|
|
1040
|
+
skippedItems.push({
|
|
1041
|
+
apiName: importApi.name || 'Unknown',
|
|
1042
|
+
reason: `Failed to encrypt auth token: ${encryptedResult.error}`
|
|
1043
|
+
});
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const encryptedToken = encryptedResult.value;
|
|
1047
|
+
|
|
1048
|
+
const newApi = {
|
|
1049
|
+
id: Date.now() + Math.random(),
|
|
1050
|
+
name: importApi.name || `Imported API ${this.config.apis.length + 1}`,
|
|
1051
|
+
baseUrl: urlValidation.value,
|
|
1052
|
+
authToken: encryptedToken,
|
|
1053
|
+
model: modelValidation.value,
|
|
1054
|
+
provider: importApi.provider || 'custom',
|
|
1055
|
+
smallFastModel: importApi.smallFastModel || importApi.model,
|
|
1056
|
+
createdAt: new Date().toISOString(),
|
|
1057
|
+
lastUsed: null,
|
|
1058
|
+
usageCount: 0,
|
|
1059
|
+
modelEnvVars: cleanedModelEnvVars,
|
|
1060
|
+
runtimeEnvVars: cleanedRuntimeEnvVars,
|
|
1061
|
+
_runtimeEnvSources: cleanedRuntimeEnvSources,
|
|
1062
|
+
customEnvVars: cleanedCustomEnvVars,
|
|
1063
|
+
_autoFilledModel: importApi._autoFilledModel,
|
|
1064
|
+
};
|
|
1065
|
+
if (importApi._autoModelEnvVars) {
|
|
1066
|
+
newApi._autoModelEnvVars = importApi._autoModelEnvVars;
|
|
1067
|
+
}
|
|
1068
|
+
if (importApi.successCount !== undefined) newApi.successCount = importApi.successCount;
|
|
1069
|
+
if (importApi.failCount !== undefined) newApi.failCount = importApi.failCount;
|
|
1070
|
+
if (importApi.lastError !== undefined) newApi.lastError = importApi.lastError;
|
|
1071
|
+
|
|
1072
|
+
this._normalizeApiFields(newApi);
|
|
1073
|
+
this.config.apis.push(newApi);
|
|
1074
|
+
imported++;
|
|
1075
|
+
if (this.config.apis.length === 1) this.config.activeIndex = 0;
|
|
697
1076
|
} catch (error) {
|
|
698
|
-
screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
|
|
699
1077
|
skipped++;
|
|
1078
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: error.message });
|
|
700
1079
|
}
|
|
701
1080
|
});
|
|
702
1081
|
|
|
703
1082
|
this.saveConfig();
|
|
704
|
-
return { imported, skipped };
|
|
1083
|
+
return { imported, skipped, warnings, skippedItems };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// --- Draft methods (no persistence, for pre-create config editing) ---
|
|
1087
|
+
|
|
1088
|
+
static buildApiDraft(provider, baseUrl, authToken, model, name) {
|
|
1089
|
+
const { getProvider } = require('./presets/providers');
|
|
1090
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
1091
|
+
const providerConfig = getProvider(provider);
|
|
1092
|
+
let templateValues;
|
|
1093
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
1094
|
+
templateValues = providerConfig.modelEnvTemplate.getValues(model);
|
|
1095
|
+
} else {
|
|
1096
|
+
templateValues = {};
|
|
1097
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = model;
|
|
1098
|
+
templateValues.smallFastModel = model;
|
|
1099
|
+
}
|
|
1100
|
+
const modelEnvVars = {};
|
|
1101
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) modelEnvVars[k] = templateValues[k] || '';
|
|
1102
|
+
const _autoModelEnvVars = { ...templateValues };
|
|
1103
|
+
const runtimeEnvVars = {};
|
|
1104
|
+
const _runtimeEnvSources = {};
|
|
1105
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) { runtimeEnvVars[k] = ''; _runtimeEnvSources[k] = 'auto'; }
|
|
1106
|
+
return { provider, baseUrl, authToken, model, name,
|
|
1107
|
+
smallFastModel: templateValues.smallFastModel,
|
|
1108
|
+
modelEnvVars, _autoModelEnvVars,
|
|
1109
|
+
runtimeEnvVars, _runtimeEnvSources, customEnvVars: {} };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
static applyDraftEnvChange(draft, section, key, value) {
|
|
1113
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, validateEnvKey } = require('./validators');
|
|
1114
|
+
if (section === 'model') {
|
|
1115
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`Unknown model env key: ${key}`);
|
|
1116
|
+
if (typeof value !== 'string') throw new Error('model env value must be string');
|
|
1117
|
+
draft.modelEnvVars[key] = (value === '') ? (draft._autoModelEnvVars[key] || '') : value;
|
|
1118
|
+
} else if (section === 'runtime') {
|
|
1119
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`Unknown runtime env key: ${key}`);
|
|
1120
|
+
const v = validateRuntimeEnvValue(key, value);
|
|
1121
|
+
if (!v.valid) throw new Error(`Invalid: ${v.error}`);
|
|
1122
|
+
draft.runtimeEnvVars[key] = value;
|
|
1123
|
+
draft._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
|
|
1124
|
+
} else if (section === 'custom') {
|
|
1125
|
+
const kv = validateEnvKey(key);
|
|
1126
|
+
if (!kv.valid) throw new Error(`Invalid custom key: ${kv.error}`);
|
|
1127
|
+
if (typeof value !== 'string') throw new Error('custom env value must be string');
|
|
1128
|
+
draft.customEnvVars[key] = value;
|
|
1129
|
+
}
|
|
1130
|
+
return draft;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
static deleteDraftCustomEnvVar(draft, key) {
|
|
1134
|
+
delete draft.customEnvVars[key];
|
|
1135
|
+
return draft;
|
|
705
1136
|
}
|
|
706
1137
|
}
|
|
707
1138
|
|
|
708
|
-
module.exports = ApiManager;
|
|
1139
|
+
module.exports = ApiManager;
|
|
1140
|
+
module.exports.DuplicateApiError = DuplicateApiError;
|
|
1141
|
+
module.exports.MODEL_CONFIG_LABELS = MODEL_CONFIG_LABELS;
|
|
1142
|
+
module.exports.RUNTIME_CONFIG_LABELS = RUNTIME_CONFIG_LABELS;
|